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.
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
(incl. VAT)