The Basic Stamp can do many things, but -- in general -- it can only do one thing at a time. That's why we make coprocessors that extend the Stamp's capabilities. Of course, we only make so many coprocessors. Sometimes you might want to consider rolling your own. Naturally, if you use a different processor, you might be tempted to use it for the entire task at hand. However, there are two considerations here. First, the Basic Stamp really is easy to use and makes debugging fairly simple. Second, if you make a chip that works with a Basic Stamp, people can use it even though they couldn't actually program the processor you've used to make your coprocessor.
This month I'll marry our APP-IV microprocessor with a Basic Stamp to improve serial I/O handling. This is similar to the SX-based SSIB I developed for the Parallax SX assembly language course. However, while that used the SX's assembly language, this serial buffer will use C.
The APP-IV is made to work with the open source GNU C compiler which is a robust and powerful development environment. You don't need a programmer because all of our APP kits program themselves. You can find a tutorial at http://tutor.al-williams.com/gcc1.htm. The core processor is an Atmel ATmega 8 which is a powerful CPU with 8K of program space, 1K of RAM, and 512 bytes of EEPROM. The chip can run at 16 MIPs, but the APP-IV has it loafing at 10 MIPs. You download your program over the serial port, so development is very similar to what you are used to with the Basic Stamp.
Of course, just having a C compiler is not enough. You also need good library support. Luckily, the compiler has a great library included and also there is a fantastic open source library (AVRLib) available at http://hubbard.engr.scu.edu/embedded/avr/avrlib/. This library has a bewildering array of functions include code for GPS, FAT file system, I2C, LCD, servos, and even MP3!
In fact, I've used AVRLib's interrupt-driven RS232 code to provide a 64 byte buffer for the Stamp. The chip reads the incoming data stream and buffers it until the Stamp is ready to read it. Of course, the Stamp has to read often enough to keep the buffer from emptying (although it would be easy to make the buffer larger, there will always be some upper limit to how many characters you can store).
Wow! C programming, interrupts, and circular buffers. This must be difficult, right? Nope. Because the library does all the hard work, the code is actually quite simple. In fact, here it is:
#include <avr/io.h> #include "app4.h" #include "buffer.h" #include "uart.h" /* Main program */ int main (void) { u08 byte; uartInit(); uartSetBaudRate(9600); // default anyway while (1) // do forever { while ((PINB&1)!=0); // Stamp not ready if (!uartReceiveByte(&byte)) continue; // nothing to send uartSendByte(byte); // the problem here is that the code continues, but the // Stamp may turn off its handshake after this character // so we need to wait for the character time here while ((UCSRA&0x40)!=0); // goes true when tx buffer empty } }
This isn't nearly as intimidating as it might look at first. The main function, as you would expect, is the main part of the program (the only part, in this case). The call to uartInitand uartSetBaudRate set up the serial port. All the interrupt handling and buffering occurs in these library routines!
The main loop waits to see if the Stamp is ready by watching bit 0 of port B. When the bit goes low, the chip checks for a character from the receive buffer. If there is no character ready, the loop restarts. Otherwise, the chip sends the character using uartSendByte.
The only tricky part is that the Stamp may want to stop transmission after a character is sent, so the chip can't buffer the transmit data. Since the chip has a small hardware buffer, the last line of the program waits until the actual transmit register is empty before it continues on the loop. That way the chip won't start sending more data while the Stamp tries to stop the transmission. In practice, you might want to add a bit more delay here just to be safe.
The Stamp part is pretty simple too. In practice, you connect the incoming serial data to the APP-IV's serial port and connect pin 3 (the APP-IV's transmit pin) directly to the Stamp. Of course, the APP-IV's real serial port is also connected to pin 3, but that's not a problem. Finally port B pin 0 connects to the Stamp's handshake lines. I used P15 for the serial input on the Stamp and P14 for the handshake line.
To test, I wrote a simple program that accepts a decimal number from the serial port. It then flashes an LED that number of times and delays 3 seconds. Of course, while the flashing and waiting goes on, the Stamp can't listen to the serial port. But if it is connected to the buffer chip, you can continue to input command numbers while the flashing is in progress. Here's the code:
'{$STAMP BS2} '{$PBASIC 2.5} LED PIN 8 ' Test LED TTYIN PIN 15 ' Serial input FLOWP PIN 14 ' Flow control Baud CON 84 ' 9600 baud ct VAR word i VAR Word LOW 8 Top: ' Use these two lines to test without buffer chip 'DEBUG "?" 'SERIN 16,84,[DEC ct] ' This line is for the buffer chip SERIN TTYIN\FLOWP,Baud,[DEC ct] FOR i=1 TO ct ' blink LED correct # of times HIGH LED PAUSE 250 LOW LED PAUSE 250 Next PAUSE 3000 ' wait 3 seconds GOTO Top
You can uncomment the SERIN line for port 16 (the debug terminal) and comment the SERIN line that refers to TTYIN so you can test the program with the debug terminal. You'll see that any input you send while the LED is blinking is ignored.
However, if you connect your PC to the APP-IV's serial port and restore the program to its original form, you'll see that you can stack up commands and the serial buffer will hold them until the Stamp is ready to read data. You can use Hyperterminal (with no handshaking) to send data. As a byproduct, you'll see what you typed echoed to the terminal when the chip sends it (this is simply because you have the PC connected to pin 3 of the APP-IV through the MAX232 circuit so it sees the same data the Stamp sees).
That was almost too simple, wasn't it? Programming in C can be quite addictive. As a more substantial example, consider a Morse code beacon. Ham radio operators (and others) use Morse code to identify unattended radio stations and for several other purposes.
This beacon requires a tone to make the beeping noise that forms the code. There are several ways to do this, but I decided to let an interrupt generate a tone on an output pin at all times. Then the main program simply changes this pin from an input (no tone) to an output (tone) for the length of time desired.
The program is a bit more substantial then the last one, but still easy to understand:
#include <avr/io.h> #include "app4.h" #include "app4delay.h" #include <avr/pgmspace.h> #include <avr/signal.h> #include <avr/interrupt.h> /* Define RELEASE to get rid of RS232 stuff */ #ifndef RELEASE // I'm only using RS232, stdio for debugging #include <stdio.h> #include "app4uart.h" #define DEBUG(s) s #else #define DEBUG(s) #endif #include <string.h> #include <ctype.h> #include "app4io.h" /* dit delay for morse code */ #define DITDELAY 50 /* mS */ /* Output on PORTD.7 */ #define OUTPORT D #define OUTBIT 7 // Set up timer to run at /(256*8) void timer_init(void) { TCCR0=2; // 10MHz clock direct prescaled by 8 TIMSK|=1; // enable sei(); // enable all interrupts } // Interrupt handler // this will be about 4.8kHz so divide by 4 INTERRUPT(SIG_OVERFLOW0) { static int divider=0; if (divider++%4) return; // divide by 4 to get about 1200Hz PORTB^=1; // toggle pin (only matters if output is set) } // Create a tone (not really, it just gates it on and off) void tone(int dlay) { DDRB|=1; delay_ms(dlay*DITDELAY); DDRB&=~1; } /* Send a Morse code element (.- or space) */ void send(char c) { switch (c) { case '-': HIGH(OUTPORT,OUTBIT); tone(3); LOW(OUTPORT,OUTBIT); break; case '.': HIGH(OUTPORT,OUTBIT); tone(1); LOW(OUTPORT,OUTBIT); break; case ' ': delay_ms(4*DITDELAY); break; } delay_ms(DITDELAY); /* inter element space */ } // Send a character (made from dots/dashes) from program memory void sendc(PGM_P s) { int c; for (c=pgm_read_byte(s);c;c=pgm_read_byte(++s)) { DEBUG(printf("element@%d: %c\n",s,c)); send(c); } } // Send an ASCII character void sendchar(int c) { DEBUG(printf("Send %c\n",c)); switch (toupper(c)) { // I only put the letters I needed in to save space in the listings // You can make the other letters as you need them case ' ': sendc(PSTR(" ")); break; case 'A': sendc(PSTR(".-")); break; case 'C': sendc(PSTR("-.-.")); break; case 'D': sendc(PSTR("-..")); break; case 'E': sendc(PSTR(".")); break; case 'G': sendc(PSTR("--.")); break; case 'K': sendc(PSTR("-.-")); break; case 'N': sendc(PSTR("-.")); break; case 'Q': sendc(PSTR("--.-")); break; case 'R': sendc(PSTR(".-.")); break; case 'W': sendc(PSTR(".--")); break; case '5': sendc(PSTR(".....")); break; } send(' '); } /* Main program */ int main (void) { // Put beacon message in program memory PGM_P *message=PSTR("CQ CQ CQ de WD5GNR WD5GNR WD5GNR K"); #ifndef RELEASE UartInit(BAUD_19200); UartSetStdio(); printf_P(PSTR("\014Debug: CW Beacon starting\n")); #endif LOW(OUTPORT,OUTBIT); // Make LED an output // We use the timer to generate a tone // and then we turn the output bit on to gate the tone on // and off to silence it timer_init(); while (1) { int c; // Send each character in message PGM_P ptr=message; for (c=pgm_read_byte(ptr);c;c=pgm_read_byte(++ptr)) { sendchar(c); } delay_ms(30000); // wait 30 seconds } return 0; }
There are only two noteworthy areas of the code. First, the message string is stored in program memory to conserve RAM. This is easy to do with some help from the normal library that ships with the compiler. The PGM_P type is a pointer to a string in program memory and the PSTR macro stores a constant string in the program space. Then a few special functions (like pgm_read_byte and printf_P) handle these special pointers. Without these pointers, your program would have to contain code to initialize RAM to these constants values which wastes program space and precious RAM memory. The way the code is written the strings just appear as constants in the program space which is relatively efficient.
The other interesting item is how easy it is to handle timer interrupts. Here is all the interrupt-related code:
// Set up timer to run at /(256*8) void timer_init(void) { TCCR0=2; // 10MHz clock direct prescaled by 8 TIMSK|=1; // enable sei(); // enable all interrupts } // Interrupt handler // this will be about 4.8kHz so divide by 4 INTERRUPT(SIG_OVERFLOW0) { static unsigned divider=0; if (divider++%2) return; // divide by 2 to get about 2400Hz (half cycle) PORTB^=1; // toggle pin (only matters if output is set) }
The timer_init function sets everything up. In this case, the program sets up an 8 bit counter to count at 10MHz divided by 8. So the timer code executes every time a 1.25MHz clock counts to 256 (about 4883Hz). The line that starts with TIMSK enables the timer interrupt and the sei function enables interrupts globally.
When the timer interrupt occurs, the function that executes is INTERRUPT(SIG_OVERFLOW0). Since 4.8kHz is a little high for the tone, the code simply counts up and returns if the count is not an even multiple of 2 (the % operator in C is like the // operator in PBasic). If the count is a multiple of 2, the code toggles an output on PORTB (^ is the exclusive or operator). This results in a half cycle of about 2400Hz and a 1200Hz tone.
So interrupts in C are pretty easy even without direct library support. The rest of the program is just data manipulation and delays.
Site contents © 1997-2018 by AWC, Houston TX (281) 334-4341