Atmel power control for a Lionel train

Relay Control Software

The main loop of the AVR program (Listing 1, which appears at the end of the article) waits for a complete command to arrive via the UART (serial port). All of the UART communication is handled by interrupts, so a flag is set when a command is ready; then, the AVR decodes it and updates its GPIO accordingly. The surrounding electronics take care of turning on the relay, then the AVR waits for the next command to be received.

Listing 1

AVR.c

001 #include <inttypes.h>
002 #include <stdlib.h>
003 #include <string.h>
004 #include <avr/io.h>
005 #include <avr/interrupt.h>
006 #include <avr/sleep.h>
007 #include <util/delay.h>
008
009 char cBlocks [ 16 ];
010 char cSerial = 0;
011 char cSerialFlag = 0;
012 char sSerialString [ 16 ];
013 char cSerialIndex = 0;
014
015 ISR ( USART0_RX_vect )
016 {
017    cSerial = UDR0;
018    cSerialFlag = 1;
019 }
020
021 void serialInput()
022 {
023    switch ( cSerial )
024    {
025       case '{':
026       {
027          cSerialIndex = 0;
028       }
029       break;
030       case '}': processSerial();break;
031       default:
032       {
033          sSerialString [ cSerialIndex ] = cSerial;
034          cSerialIndex ++;
035          sSerialString [ cSerialIndex ] = 0;
036       }
037    }
038    cSerialFlag = 0;
039 }
040
041 void processSerial()
042 {
043    if ( sSerialString [ 0 ] == 'B' )
044    {
045       switch ( sSerialString [ 1 ] )
046       {
047          case '1': cBlocks [ 1 ] = sSerialString [ 2 ] - 48;
048          case '2': cBlocks [ 2 ] = sSerialString [ 2 ] - 48;
049          case '3': cBlocks [ 3 ] = sSerialString [ 2 ] - 48;
050          case '4': cBlocks [ 4 ] = sSerialString [ 2 ] - 48;
051          case '5': cBlocks [ 5 ] = sSerialString [ 2 ] - 48;
052          case '6': cBlocks [ 6 ] = sSerialString [ 2 ] - 48;
053          case '7': cBlocks [ 7 ] = sSerialString [ 2 ] - 48;
054          case '8': cBlocks [ 8 ] = sSerialString [ 2 ] - 48;
055          case '9': cBlocks [ 9 ] = sSerialString [ 2 ] - 48;
056          case 'A': cBlocks [ 10 ] = sSerialString [ 2 ] - 48;
057          case 'B': cBlocks [ 11 ] = sSerialString [ 2 ] - 48;
058          case 'C': cBlocks [ 12 ] = sSerialString [ 2 ] - 48;
059      }
060
061      cSerialIndex = 0;
062      sSerialString [ 0 ] = 0;
063    }
064 }
065
066 void setBlocks()
067 {
068    // Block 1
069    if ( cBlocks [ 1 ] == 0 )
070    {
071       PORTA &= ~( 1 << PA0 | 1 << PA1 );
072    }
073    else if ( cBlocks [ 1 ] == 1 )
074    {
075       PORTA |= ( 1 << PA0 );
076       PORTA &= ~( 1 << PA1 );
077    }
078    else if ( cBlocks [ 1 ] == 2 )
079    {
080       PORTA |= ( 1 << PA1 );
081       PORTA &= ~( 1 << PA0 );
082    }
083
084    // Block 2
085    if ( cBlocks [ 2 ] == 0 )
086    {
087       PORTA &= ~( 1 << PA2 | 1 << PA3 );
088    }
089    else if ( cBlocks [ 2 ] == 1 )
090    {
091       PORTA |= ( 1 << PA2 );
092       PORTA &= ~( 1 << PA3 );
093    }
094    else if ( cBlocks [ 2 ] == 2 )
095    {
096       PORTA |= ( 1 << PA3 );
097       PORTA &= ~( 1 << PA2 );
098    }
099
100    // Block 3
101    if ( cBlocks [ 3 ] == 0 )
102    {
103       PORTA &= ~( 1 << PA4 | 1 << PA5 );
104    }
105    else if ( cBlocks [ 3 ] == 1 )
106    {
107       PORTA |= ( 1 << PA4 );
108       PORTA &= ~( 1 << PA5 );
109    }
110    else if ( cBlocks [ 3 ] == 2 )
111    {
112       PORTA |= ( 1 << PA5 );
113       PORTA &= ~( 1 << PA4 );
114    }
115
116    // Block 4
117    if ( cBlocks [ 4 ] == 0 )
118    {
119       PORTA &= ~( 1 << PA6 | 1 << PA7 );
120    }
121    else if ( cBlocks [ 4 ] == 1 )
122    {
123       PORTA |= ( 1 << PA6 );
124       PORTA &= ~( 1 << PA7 );
125    }
126    else if ( cBlocks [ 4 ] == 2 )
127    {
128       PORTA |= ( 1 << PA7 );
129       PORTA &= ~( 1 << PA6 );
130    }
131
132    // Block 5
133    if ( cBlocks [ 5 ] == 0 )
134    {
135       PORTB &= ~( 1 << PB0 | 1 << PB1 );
136    }
137    else if ( cBlocks [ 5 ] == 1 )
138    {
139       PORTB |= ( 1 << PB0 );
140       PORTB &= ~( 1 << PB1 );
141    }
142    else if ( cBlocks [ 5 ] == 2 )
143    {
144       PORTB |= ( 1 << PB1 );
145       PORTB &= ~( 1 << PB0 );
146    }
147
148    // Block 6
149    if ( cBlocks [ 6 ] == 0 )
150    {
151       PORTB &= ~( 1 << PB2 | 1 << PB3 );
152    }
153    else if ( cBlocks [ 6 ] == 1 )
154    {
155       PORTB |= ( 1 << PB2 );
156       PORTB &= ~( 1 << PB3 );
157    }
158    else if ( cBlocks [ 6 ] == 2 )
159    {
160       PORTB |= ( 1 << PB3 );
161       PORTB &= ~( 1 << PB2 );
162    }
163
164    // Block 7
165    if ( cBlocks [ 7 ] == 0 )
166    {
167       PORTB &= ~( 1 << PB4 );
168       PORTC &= ~( 1 << PC0 );
169    }
170    else if ( cBlocks [ 7 ] == 1 )
171    {
172       PORTB |= ( 1 << PB4 );
173       PORTC &= ~( 1 << PC0 );
174    }
175    else if ( cBlocks [ 7 ] == 2 )
176    {
177       PORTC |= ( 1 << PC0 );
178       PORTB &= ~( 1 << PB4 );
179    }
180
181    // Block 8
182    if ( cBlocks [ 8 ] == 0 )
183    {
184       PORTC &= ~( 1 << PC1 | 1 << PC2 );
185    }
186    else if ( cBlocks [ 8 ] == 1 )
187    {
188       PORTC |= ( 1 << PC1 );
189       PORTC &= ~( 1 << PC2 );
190    }
191    else if ( cBlocks [ 8 ] == 2 )
192    {
193       PORTC |= ( 1 << PC2 );
194       PORTC &= ~( 1 << PC1 );
195    }
196
197    // Block 9
198    if ( cBlocks [ 9 ] == 0 )
199    {
200       PORTC &= ~( 1 << PC3 | 1 << PC4 );
201    }
202    else if ( cBlocks [ 9 ] == 1 )
203    {
204       PORTC |= ( 1 << PC3 );
205       PORTC &= ~( 1 << PC4 );
206    }
207    else if ( cBlocks [ 9 ] == 2 )
208    {
209       PORTC |= ( 1 << PC4 );
210       PORTC &= ~( 1 << PC3 );
211    }
212
213    // Block 10
214    if ( cBlocks [ 10 ] == 0 )
215    {
216       PORTC &= ~( 1 << PC5 | 1 << PC6 );
217    }
218    else if ( cBlocks [ 10 ] == 1 )
219    {
220       PORTC |= ( 1 << PC5 );
221       PORTC &= ~( 1 << PC6 );
222    }
223    else if ( cBlocks [ 10 ] == 2 )
224    {
225       PORTC |= ( 1 << PC6 );
226       PORTC &= ~( 1 << PC5 );
227    }
228
229    // Block 11
230    if ( cBlocks [ 11 ] == 0 )
231    {
232       PORTD &= ~( 1 << PD4 | 1 << PD5 );
233    }
234    else if ( cBlocks [ 11 ] == 1 )
235    {
236       PORTD |= ( 1 << PD4 );
237       PORTD &= ~( 1 << PD5 );
238    }
239    else if ( cBlocks [ 11 ] == 2 )
240    {
241       PORTD |= ( 1 << PD5 );
242       PORTD &= ~( 1 << PD4 );
243    }
244
245    // Block 12
246    if ( cBlocks [ 12 ] == 0 )
247    {
248       PORTD &= ~( 1 << PD6 | 1 << PD7 );
249    }
250    else if ( cBlocks [ 12 ] == 1 )
251    {
252       PORTD |= ( 1 << PD6 );
253       PORTD &= ~( 1 << PD7 );
254    }
255    else if ( cBlocks [ 12 ] == 2 )
256    {
257       PORTD |= ( 1 << PD7 );
258       PORTD &= ~( 1 << PD6 );
259    }
260
261 }
262
263 int
264 main ()
265 {
266    // UART setup
267    UCSR0A = ( 1 << U2X0 );
268    UCSR0B = ( 1 << RXCIE0 ) | ( 1 << RXEN0 );
269    UCSR0C = ( 1 << UCSZ00 ) | ( 1 << UCSZ01 );
270    UBRR0L = 12;      // 9600 baud
271
272    // GPIO setup
273    DDRA = 255;    // All pins outputs
274    DDRB = ( 1 << PB0 ) | ( 1 << PB1 ) | ( 1 << PB2 ) | ( 1 << PB3) | \
              ( 1 << PB4 );  // Pins 0-4 outputs (6 & 7 are programming pins)
275    DDRC = 255;    // All pins outputs
276    DDRD = ( 1 << PD1 ) | ( 1 << PD3 ) | ( 1 << PD4 ) | ( 1 << PD5 ) | \
              ( 1 << PD6 ) | ( 1 << PD7 ); // UART0 transmit, pins 4 - 7
277
278    cBlocks [ 1 ] = 0;
279    cBlocks [ 2 ] = 0;
280    cBlocks [ 3 ] = 0;
281    cBlocks [ 4 ] = 0;
282    cBlocks [ 5 ] = 0;
283    cBlocks [ 6 ] = 0;
284    cBlocks [ 7 ] = 0;
285    cBlocks [ 8 ] = 0;
286    cBlocks [ 9 ] = 0;
287    cBlocks [ 10 ] = 0;
288    cBlocks [ 11 ] = 0;
289    cBlocks [ 12 ] = 0;
290    setBlocks();
291
292    sei();
293    while ( 1 )
294    {
295       if ( cSerialFlag == 1 ) serialInput();
296       setBlocks();
297    }
298 }

The code is written in C and compiled with GCC specifically for the Atmel ATmega164P. In this section, I'll walk you through the code line by line.

Lines 1-7. Includes in C bring in external libraries. Some of these are the same libraries you'd use in an x86 program, whereas others are specific to the microcontroller itself.

Lines 9-13. I've defined a few global variables so that the entire program will have access to them. In C, when a variable is defined outside of a function, it's global. All of my variables start with a lowercase letter (i.e., c or s). This is a programming convention I learned when I first started programming to denote the type of variable I'm currently using: c for chars, s for strings (arrays of characters), and so on. This convention is especially helpful in microcontroller code because variables are often used as bit fields or other storage where the type itself could be important. The C language doesn't require this convention; it's purely for human readability.

Starting with line 9, cBlocks [ 16 ] is where the current block assignments are stored, 0 is off, 1 is cab 1, and 2 is cab 2. The [ 16 ] on the end indicates it is an array of 16 values. cSerial is the most recent character received from the UART, and cSerialFlag is set to 1 when a character is received.

sSerialString [16 ] stores the assembled characters coming from the UART (i.e., the command to be processed once it is fully received), and cSerialIndex is the position in sSerialString where the next received character will go.

Memory on a microcontroller is at an absolute premium. The ATmega164P used in this project only has 1KB available, total. To conserve as much as possible, variables are defined as small as possible. In C, a char is an 8-bit unsigned number (i.e., decimal values from 0 to 255) and is the smallest size variable type that can be defined. It gets its name from the fact that it can store a single ASCII character.

So, actual characters are not necessarily being stored – sometimes they are, but not always. A char is just the most appropriate data type to store the values effectively without wasting memory.

Lines 15-19. Serial Interrupt Handler. Line 15 defines a special type of function, an Interrupt Service Routine (ISR). It looks similar to a regular function and in fact acts like one as well. The difference is that instead of being called by my code, the processor itself calls it when an event occurs – in this case, when a character is received by the AVR serial port. This event is specified by USART0_RX_vect.

In line 17, UDR0 is where the UART hardware stores the character it has just received. I copy it into my own variable cSerial because UDR0 is overwritten as soon as the next character is received. Line 18 sets cSerialFlag to 1 so that the main loop of the program knows to handle the new character.

Interrupt routines literally interrupt the normal flow of the program so they must run as quickly as possible. That's why all I do is store the new character and set a flag indicating that it is ready. I'll handle it later in the program. When interrupts finish, the program will go back to where it was just before the interrupt took over. Eventually the main loop will check to see whether the flag is set, and handle it appropriately.

Lines 21-39. Serial Character Handler. The function serialInput() deals with incoming serial characters. It is called by the main loop when the serial flag is set. Note here that the curly braces in single quotes ('{ ' and '}') are not added to the final string. They are "control characters" rather than data, so they aren't stored. Technically, the ASCII chart defines characters specifically for this type of use, so why not use them? By using printable characters like brackets they are more easily interpreted as a start and a finish. I can also print or display them, and they'll show up properly rather than generating an unprintable character error.

The switch statement in line 23 checks for a couple specific characters and then uses a default action to handle anything else. For example, line 25 looks for the curly brace character ({), which indicates the start of a new command, then line 27 resets cSerialIndex (signaling where the next character is put) to 0, or the start of the string. Likewise, line 30 looks for the matching curly brace (}) which, if found, indicates the command is complete, so it should be processed.

The default line in a switch statement (line 31) says "if nothing else has happened, then do this." In my case, if I haven't found the characters I'm looking for, I just want to add the incoming character to the string (line 33), then I do all of the housekeeping associated with it.

In line 34 I increment cSerialIndex so the next character is written to the next slot in the array; then, line 35 writes a 0 (null) to the new slot, denoting the end of the string. The next time around the loop this will be overwritten with the new character, and the entire process repeats. In this way, the string is always terminated properly. Line 38 then clears cSerialFlag once the most recent serial input has been handled.

Lines 41-64. To process the serial command, processSerial() takes a complete command and interprets it. The command was stored by multiple passes of serialInput() into sSerialString, so that's what I'm working with here.

Line 43 looks at the first character in the string to see what type of command it is. Although only one type currently exists, this allows for future expansion without major restructuring of the code. If the character is a B, I want to assign a block.

Lines 44-59 then process the serial string by setting up a switch statement in line 45 to interpret the second character of the command string. That's the block to which I want to assign a throttle. Lines 47-58 are almost identical, so I'll look at them together.

At first glance, it might seem like the case statements are redundant, checking for 1 and then hard-coding a 1 as the cBlocks index. However, remember that it is the character 1, not the value 1. More on this momentarily.

When you look down toward the end of this block, you'll notice that numbers change to letters! To keep the block selection a single character, I used the hex values A, B, and C for 10, 11, and 12.

In each case statement, cBlocks is being assigned to the third character in sSerialString minus 48. So, what's happening?

The third character in sSerialString is going to be a 0, 1, or 2. These are printable characters, not their decimal equivalents. A 0 character is actually stored as the number 48. Similarly, a capital A is the value 65. In this way, I'm starting with the character value (48, 49, or 50 for 0, 1, or 2) and subtracting 48 from it; the value I end up with is the decimal equivalent of the character.

In lines 61 and 62, I reset cSerialIndex and sSerialString because the command has been processed.

Lines 66-261. The setBlocks() function walks down the cBlocks array and sets the GPIO to match the block requested. Each block is identical, except for the actual GPIO pins in use, so I'll just look at the first one.

In line 69, I check to see whether the block is turned off. If so, I turn off both GPIO pins for this block (line 71). Lines 73-77 turn on the first GPIO pin and turn off the second when the first cab is selected, whereas lines 78-82 do the opposite when the second throttle is selected.

Because the GPIO for the relay outputs is spread across the entire chip, the GPIO pins sometimes fall in different ports (e.g., Block 7, lines 164-179). The code still functions identically in this case, but with separate lines to control each port.

Lines 263-298. The main() function is the main loop of the program and the entry point of the code. I set up all of my chip functions, initialize the variables, and then start an infinite loop to let the program run.

Lines 266-270. The UART (serial port) setup takes place first. All functions on a microcontroller are configured through registers. These are listed in the datasheet and the #include <avr/io.h> on line 4 defines them for my program, so I can use the names directly.

Because serial communication is completely dependent on accurate timing, line 267 enables the UART baud rate doubler to select the speed of the UART clock. Ultimately the microcontroller must check for arriving data at the selected baud rate. Depending on the clock speed of the microcontroller itself, the doubler can sometimes help eliminate error in the timing values.

Line 268 enables the UART receiver (RXEN0) and the character-received interrupt (RXCIE0). Each special function of a microcontroller is routed to a physical pin, but until the function is enabled, the pin functions as the GPIO.

Enabling the interrupt tells the microcontroller that it should call the ISR that was set up when a character was received (lines 15-19).

Line 269 sets the character size of the serial frame. You might recognize something like 9600,N,8,1 (e.g., 9600 baud, no parity, eight data bits, and one stop bit). The combination of UCSZ00 and UCSZ01 sets the UART to eight data bits. No parity and one stop bit are the default options for the chip, so I don't have to do anything with them.

The next line sets the baud rate, but why 12 instead of 9600? The Atmel data sheet provides a formula for calculating baud rates, which includes the clock speed for the chip, whether the doubler is enabled, and the amount of error. All of this boils down to a clock rate counter which, in this case, is 12. Atmel also conveniently provides tables for common baud rates and clock speeds, so you don't have to worry about the math.

Lines 272-276. All pins on an Atmel start out as inputs. DDRA, DDRB, DDRC, and DDRD are 8-bit (character-sized) registers that define pin direction. If the corresponding bit is a 1, then that pin will be an output.

On line 273, I want all of PORTA to be an output, so I set DDRA to 255 (i.e., binary 11111111, or all outputs). Because I only want some of the pins to be outputs, I define them individually in line 274. PB0 through PB4 are pin names, so each set of parentheses says "put (shift) a 1 into the position for pin PBx." The logical OR (|) combines all of the pin setups. Lines 275 and 276 repeat the process for ports C and D.

Lines 278-297. When the program starts, I want all of the blocks to be off, so I initialize the block array first. Line 290 calls setBlocks() to set the relays.

Line 292 is probably the most crucial (if also the most cryptic) line in the program. sei() enables interrupts globally for the entire chip. Without this command, none of the interrupts will function and will fail silently with no indication of what's wrong.

The loop that runs everything is in lines 293-297. Line 293 starts an infinite loop, and line 295 checks for the serial flag. If it finds it, it calls serialInput(), then setBlocks() updates the relays to match the cBlocks array in line 296.

How does anything else happen, though? For example, no functions check the serial port. The interrupt routine was set up to take care of this. Because interrupts are enabled globally (line 292) and also for the UART receiver (line 268), the microcontroller jumps the program out of the main loop whenever a character is received. When it finishes, the main loop handles everything else.

Serial Communications

The AVR and the computer communicate via serial, which is one of the earliest digital communication formats, originating on mainframes when access was via dumb terminal or even teletype. Until USB came onto the scene, most computers had two or three serial ports in either the 9-pin or 25-pin varieties. Mice and modems were the most common peripheral to connect this way, but printers and custom hardware might use it as well. Today, you're lucky to find any serial ports on a computer, although they are readily available as expansion USB dongles.

Serial communication relies on very strict timing (Figure 7) to transmit its data – the baud rate, or bits per second that are transmitted. (See the "Serial Tips and Tricks" box.) To send data, the transmitting device sends a 1 followed by usually 7 or 8 bits of data – the character to transmit – at the specified baud rate. A 0 (the stop bit) finishes the sequence. This is repeated until all of the data has been sent. Once the start bit is received, it's the receiver's responsibility to time when to read each bit. If timing is off even slightly, the communication quickly becomes garbled.

Figure 7: Serial timing flow diagram to transmit the letter I. Note that the good timing lines up so each bit is received properly. The bad timing very quickly gets out of sync. Even if the ? bit were received as a 1 or 0, then it would decode as an "Enquiry" (0) or a "%" (1).

Serial Tips and Tricks

To avoid a lot of debugging headaches, I've assembled a few tips and tricks when working with serial communication.

  • Don't use a faster baud rate than necessary to make it easy for your microcontroller to keep up. If you are just sending a few characters at a time, then high speed just isn't necessary. Even at 2400 baud that's 300 characters every second.
  • Use an external crystal or an oscillator. The RC oscillator built into Atmel microcontrollers has a tendency to drift, especially at higher speeds. This will affect the timing of your serial data enough to corrupt it. Even worse, it can shift over a matter of seconds or minutes. Although your project works fine as soon as you turn it on, it might suddenly stop working a minute or two later.
  • Use interrupts to receive characters. A microcontroller literally has a one-track mind. If serial data arrives faster than your main loop can poll for new data, then it will simply be lost. An interrupt allows the serial communication to take priority and then return control to your program.
  • Sometimes, you have to wait between transmitting characters. If you're talking to another microcontroller or non-computer device, it might be helpful to spread out your data a bit. Although bit-timing is critical, you are not required to send characters at a certain rate. Spacing it out might help your receiver keep up.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Raspberry Pi Geek

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content