Lab Guide: Arduino and AVR Programming

Here are some miscellaneous notes on programming AVR microcontrollers.  Recall that the Arduino library is an abstraction layer written on top of the AVR library.  For Arduino-specific items I’ve also noted the equivalent for raw AVR code when appropriate.

Digital and Analog I/O

(more info and more info)

Digital I/O is useful for debugging and for controlling external hardware.  We don’t usually use analog input in this class, but it can be useful sometimes for reading certain sensors or analog circuitry.

  • First thing: set pin mode using Arduino’s pinMode function.  Pins are set to input by default.  In AVR code, the pin mode can be set using the data direction registers (DDRx).
  • Write output pin values using the digitalWrite function.  In AVR code, pins can be set by writing to the PORTx registers; one bit in the register corresponds to a pin.
  • Pins can be in several states:
    • High (output): outputting logic 1 (Vcc, a.k.a. positive voltage).
    • Low (output): outputting logic 0 (ground, a.k.a. zero voltage).
    • Conflict (output): one pin is outputting logic 1 on a wire and another pin is outputting logic 0 on the same wire.  This is dangerous since there’s a short circuit between the two pins.
    • Floating (input): not driven to any voltage, could be anything (often read on a multimeter as 1.2 V to 2.5 V).  This is the default for an input pin.  If you have an LED on the pin it might turn on really dim.
    • Pullup (input): the input pin is connected internally to Vcc through a resistor.  Thus the pin is read as logic 1 instead of floating when nothing is connected to it, but can safely be set to logic 0 by an external source (the resistor prevents a short circuit between Vcc and ground)
  • Output pins set to logic high can be used as Vcc, and can source up to 40 mA of current.  Higher-current devices should be powered through a transistor or by connecting them directly to Vcc.
  • Output pins set to logic low can be used as ground, and can sink up to 40 mA of current.
  • Analog I/O pins can be used as digital I/O pins, with their Arduino pin numbers starting after the last digital pin number.  For example on the Uno, digital pins go up to D13, so analog pin A0 is the same as D14, A1 = D15, A2 = D16, etc.
  • AnalogWrite doesn’t generate an analog signal, it generates a digital signal that goes on and off such that the average voltage it outputs over time is between 0 and Vcc (this is called a PWM signal).  You can put a capacitor on the pin to filter it into a (noisy) analog signal.  The atmega chips don’t support true analog output.  This can be accomplished in AVR code by configuring a timer for fast PWM output.
  • AnalogWrite generates a fairly low-frequency digital signal that can be annoyingly audible if it’s used to control DC motors.  You can hack the init() function to adjust the frequency upwards, making it inaudible (this also makes it easier to filter into a less-noisy analog signal).
  • Here are a couple of excellent diagrams that show the mapping between Arduino and AVR pin numbers, and also show some of the alternate pin functions that are broken out on the Arduino boards:

Debugging

Unfortunately there’s no direct way to debug code running on an Arduino board, since Arduinos (other than the Due) don’t break out the AVR’s debug pins.  There are a couple techniques that are useful for indirect debugging.

  • One good tool is LED debugging.  Turn on an LED after the line of code in question, and if the LED (e.g. the one connected to Arduino pin 13) turns on then you know that the line was executed.
  • Don’t use LED debugging at more than one point in your code, since you can’t tell whether it was line A or line B that turned the LED on.
  • You can add extra LEDs.  The LED must have a resistor in series with it, or it will burn out.  For example, you can set pin D5 to output low (so it’s at ground), and stick an LED and resistor between it and pin D4.  Set D4 to output low to turn the LED off and high to turn it on.
  • You can also print debug messages to the serial port.  However it takes a long time to write to UART, so this can disrupt the timing of your program and should be avoided unless you know that it’s okay.
  • Logic analyser (see below)

Interrupts

AVR supports a bunch of different kinds of interrupt, but they can be classified into two types: internal and external.

  • Internal interrupts are triggered by timers, the ADC unit, the UART and SPI units, and so on.  They indicate that some event has occurred, like the timer counter matches a certain value, or the analog conversion has finished, or the UART module just received a byte.  If you’re using Arduino code it will usually take care of the internal interrupts for you.
  • External interrupts are triggered by external devices.  For example, the radio triggers an external interrupt when it receives a packet.  An external interrupt can be triggered on a pin’s falling edge (transition from 1 to 0), rising edge (0 to 1), toggle (either edge) or low level (pin set to 0).
    • Use Arduino’s attachInterrupt() function to configure an external interrupt.
    • Another type of interrupt, called a pin change (PC) interrupt, occurs when one of a group of pins change value. PC interrupts are less flexible and more trouble to configure than external interrupts.  You’d use PC interrupts when you need to generate interrupts from more external sources than the MCU has external interrupts.
  • Interrupts run in special functions called ISRs.  When using Arduino code you probably won’t need to define an ISR, but keep in mind which functions are being called from an ISR.  Functions hooked to external interrupts using attachInterrupt() run inside the ISR, which means they shouldn’t take a long time to run (see the section below on Timing).
  • Any global variable that is used in an ISR must be declared as volatile.
  • If an interrupt is enabled in its mask register, the corresponding ISR must be implemented.  The default entry for unimplemented ISRs in the interrupt vector jump table is the same as the reset vector.  In other words, if an enabled interrupt occurs without the interrupt being implemented, it will restart the program.  ISRs can be empty, or implemented to do nothing using the EMPTY_INTERRUPT macro.
  • The C standard library functions are not re-entrant, so they should not be called inside of interrupts.
  • Interrupts will be disabled automatically whenever an interrupt is executing.  If in the course of running an ISR you call a function that waits on a different interrupt, that function will never return because interrupts are disabled inside the ISR.
  • If an interrupt occurs while another interrupt is executing, the incoming interrupt will be executed once the first one finishes. If the second interrupt’s interrupt condition is asserted more than once while the first interrupt is executing, then it will only run once when the first finishes.
  • Simultaneous interrupts are executed in the order they appear in the AVR’s interrupt vector table.
  • Critical sections can be protected by disabling interrupts inside an ATOMIC_BLOCK, or by clearing the global interrupt flag using the cli() macro.  But consider what effect disabling interrupts will have on the interrupts that your system uses.  For example, if you are receiving data over the serial port at 100,000 bps then you’re getting a byte every 100 μs.  If interrupts are disabled for more than 200 μs while some data are incoming, then at least one byte will be lost because your program missed the “UART data ready” interrupt.
  • By default interrupts involve a context switch, which means that any general purpose CPU registers that the interrupt uses are pushed to the stack.  Depending on the compiler, it’s possible that all 32 registers are pushed to the stack before the interrupt is run and then popped after the interrupt ends.  Keep this timing and stack overhead in mind when designing interrupts.
  • Steps to set up an interrupt in AVR code:
    • Define the ISR in your program, e.g. ISR(TIMER1_COMPA_vect) {}
    • Enable global interrupts (the I bit in SREG) by calling the sei() macro defined in avr/interrupt.h
    • Enable the specific interrupt by setting its mask bit in the appropriate configuration register, e.g. the EIMSK register for external interrupts.
    • If it’s an external or pin change interrupt, make sure the pin in set to input and isn’t left floating.

Timing

One thing we always have to be aware of in microcontroller programming is timing.  Fortunately we can be fully aware of it, because we have full control over timing and there’s no complex operating system to interfere with the system’s predictability.  N.b. that Arduino will run a bunch of timers in the background that can interfere with your carefully planned timing.

  • Be aware of how long an operation is going to take.  If possible, allow the system to do the operation in the background while your program is handling other stuff.  This can be accomplished using time-triggered scheduling or an RTOS.
  • Interrupts can be triggered from many sources, both internal to the microcontroller and external.  Consider what will happen to your code’s timing if an interrupt happens to fire while it’s running.
  • Return from interrupts, and from functions that are called by interrupts, as quickly as possible.  While an interrupt is running it is impossible for another, possibly timing-critical, interrupt to run.  Don’t write to the UART or call busy-wait functions (like util/delay.h’s _delay_ms() function) inside of an interrupt.  UART can be generated from inside an interrupt as long as it’s asynchronous.
  • You might have to use a pulse-width modulation (PWM) signal at some point.  Arduino provides the analogWrite function to generate a PWM signal easily, but it only lets you control the signal’s duty cycle.  Its frequency is pre-set to around 500 Hz.  If you need to control the frequency, you’ll have to modify the configuration registers for the timer that is being used to generate the PWM.  Several things can run off of one timer, so adjusting a timer for PWM might have side effects on other time-based Arduino functions, like other analogWrite() calls, the Servo library, and the delay() and millis() functions.
  • This timer calculator can be used to calculate appropriate values to generate a given frequency.
  • To configure a timer for normal operation in AVR code, simply set the prescaler bits in the timer control register to a  non-zero value.  The default value is 0, which means that the timer is disabled.  Setting the bit field to 1 enables the timer at a frequency equal to the system clock (usually 16 MHz).  Setting the bits to higher values causes the timer to tick at a rate equal to the system clock divided by the corresponding prescaler value.  When a timer n is running, the TCNTn register will increment at every tick.
  • The timer units can be configured to generate interrupts whenever TCNTn equals a certain value set in the output compare registers, or when they overflow.  They can also be configured to generate a PWM signal by setting the timer control registers’ WGM bit field to 0xF.  The duty cycle of the PWM is controlled with the output compare registers.
  • AVR comes with 8-bit and 16-bit timers.  Be aware of integer overflow.

AVR Architecture

AVR uses a Harvard architecture, which means that it has separate segments of memory for read-only program instructions and read-write data.

  • The program section, held in Flash memory, is accessed by 16-bit addresses pointing to 16-bit op-codes (therefore the maximum memory size is 128 KB; AVR MCUs with 256 KB program spaces use 17-bit addresses).
  • The program memory cannot be written to, except by instructions located in the bootloader section at the end of program memory.  You can, however, compile large data tables into the program space and read them into your program so that they don’t consume the limited data memory.
  • The data section, held in SRAM, is accessed by 16-bit addresses pointing to 8-bit words.  It can be read or written by the program.
  • AVR is an 8-bit architecture, but avr-gcc’s int type size is 16 bits.  I recommend using the stdint.h header, which contains typedefs to define explicit integer widths, e.g. uint8_t for an 8-bit unsigned int.  uint8_t should be your default integer type if you don’t need something bigger.
  • There’s no operating system, so there’s no such thing as a segmentation violation.
  • If you call a null function pointer, it will not try to execute whatever’s at address 0 in SRAM. You can’t execute data in SRAM because Harvard architecture.
  • If you call a null function pointer, it will execute what’s at address 0 in Flash memory, which happens to be the reset interrupt vector.  The board will be reset, but this is a bad way to restart your program since it won’t reinitialize any of the processor’s registers (instead, use the watchdog timer to perform a clean software reset).
  • If you return from your main function it will do one of two things: if there’s no code after the main function the processor will run a whole bunch of nops, since empty Flash words ares set to a value of 0xFFFF, the nop op-code.  Eventually it will hit the Arduino bootloader (if there is a bootloader) at the end of program memory, at which point the bootloader will call the application space reset vector.  If there’s code after the main function then the processor will hit code from some other function, which will eventually call the ret or reti function to return.  Since the function wasn’t actually called and there’s no frame pointer on the stack to return to, the program will pop an empty frame pointer, which will cause it to jump to 0, at which point it will restart your program.
  • Likewise if you run out of stack space then as soon as you try to return from a function a frame address of 0 will be popped off the stack and it will… wait for it… restart your program.  If you’re running out of stack space then something has gone horribly wrong in your code.  Are you calling a recursive function?  Go sit in the corner and think about what you’ve done.
  • AVR doesn’t have a floating point unit.  Avoid using floats and doubles in your code.  They’ll take a long time to process, and will add several KB of overhead to the program size.  Instead, use fixed-point arithmetic (everybody who took SENG 440 just shivered; remember, fixed-point just means integers).
  • When doing fixed-point arithmetic, be aware of order of operations.  If you do (x / 1000) * 256 you’ll lose precision compared to if you do (x * 256) / 1000.  Calculate the maximum number of bits you’ll need to hold the result of a multiplication, and use a uint16_t or uint32_t to store the result if necessary.
  • Be cautious when dividing into 32-bit integers.  16-bit and 8-bit works fine, I’ve found that 32-bit division produces unexpected results sometimes.

Communication

  • Mostly you’ll be using serial I/O, also called UART, to print stuff to a computer.  Arduino has a nice wrapper for UART, which is automatically instantiated as the Serial object and is included by Arduino.h.
  • The Serial object buffers transmissions.  If you call Serial.write when the transmit buffer is full then it will wait until the buffer empties.  The buffer is 64 bytes by default, you can expand it in HardwareSerial.cpp if you want.
  • Arduino doesn’t implement a printf-like function.  You can use snprintf to write into a buffer and then Serial.print() to write it to the serial port.  Alternately you can implement your own variable-parameter print function using vsnprintf and Serial.print, like this:
    size_t print(const char* fmt, ...)
    {
    	char buffer[256];
    	va_list args;
    	size_t size;
    	va_start(args, fmt);
    	size = vsnprintf(buffer, sizeof(buffer)-1, fmt, args);
    	va_end(args);
     
    	Serial.print(buffer);
    	return size;
    }
  • The UART baud rate frequency is only approximated by the AVR system clock, it’s not necessarily perfect.  I superstitiously use a baud rate of 100,000 because that divides perfectly into the 16 MHz system clock.  If you used the more typical baud rate of 115,200 the system clock would generate the baud rate with an error of 2.1% to 3.5% (see Table 19-12 of the ATmega328p datasheet [big PDF]).  That corresponds to an error every 28 to 48 bits, so it’s fine because we’re using 8-bit UART and the module can resynchronize the clocks at the start of every 10-bit frame.  The UART should be able to handle error rates of up to 10%, but having that error in there bugs me so I use speeds that divide into the system clock, which has an error rate (due to crystal drift) of around 0.01% or better.
  • The Arduino IDE comes with a serial monitor that you can use to see the microcontroller’s serial output.  I prefer to use RealTerm, as it has a bunch of useful features.
  • Some devices, including the radio we use, communicate over SPI, a fast, synchronous protocol.  I wrote about how SPI works here.
  • Some devices, like the BlinkM LED, communicate over TWI, also called I2C and implemented in the Arduino Wire library.  Check out some past SENG 466 reports for more information on TWI (sorry, I’m not sure which one is the best).
  • To implement non-blocking UART in raw AVR code:
    • Create ring buffers for received bytes (Rx) and bytes to transmit (Tx)
    • Implement the Rx interrupt to read from the data register and append to the Rx buffer.
    • Implement the Tx interrupt to read from the Tx buffer and write the next byte to the data register
    • Implement functions to read the Rx buffer and to write to the Tx buffer.  The latter function will also have to initiate the transmission by writing to the data register if there is not currently a transmission in progress, since otherwise the Tx interrupt won’t fire.  This can be kind of tricky.
    • Set the Rx pin to input and the Tx pin to output
    • Enable the Rx and Tx interrupts
    • Enable the UART receiver and transmitter
    • Set the character size to 8 bits (the other configuration defaults are fine)
    • Set the baud rate register.  This can be calculated with a macro, or just hard-coded from the table in the ATmega datasheet (see the end of the USARTn chapter).
  • Blocking UART–polling the UART registers instead of using interrupts–can be easier to implement.  However, it has the huge disadvantage of tying up the processor for a long time, which can cause timing problems.
  • N.b. that when you’re connecting to a UART device, the microcontroller’s Tx pin is connected to the device’s Rx pin and vice versa.  That’s why you’ll often see the pins labelled as Tx-o and Rx-i for output and input, respectively.  The output of one device gets connected to the input of the other.

Logic Analyzer

The logic analyzer is an extremely useful tool.  It monitors up to 8 digital signals and samples them at up to 24 million samples per second each.  You can get the software for it here.  Please don’t take the logic analyzers from the lab!  We only have two of them.

  • The logic analyzer can measure digital signals of up to 12 MHz.  It shows the rising and falling edges on a pin very accurately and precisely.
  • You can use the software to measure the width of a single pulse and the frequency of a full cycle.  You can also measure the time difference between two edges on different signals.
  • The software includes a protocol analyzer, so you can hook it onto a UART line and it will interpret the waveform and display the values being transmitted.  It can also read SPI and TWI signals.
  • The grey wire is ground, not the black one.  Make sure the grey wire is connected when you use the logic analyzer, and that the leads are plugged into the analyzer the right way around, so that the grey wire is lined up with the ground symbol.  If you get this wrong then it can fry the logic analyzer.
  • The leads are ordered the same as resistor colour codes: lines 1 through 8 correspond to black, brown, red, orange, yellow, green blue, and purple.
  • You can measure how long something (e.g. a function call) takes by setting an unused pin high at the start and low at the end, and using the logic analyzer to measure the time that pin spends high.  If you’re running tasks, you can use it to profile the task running times, representing CPU utilization in a swimlane diagram.
  • On that note, see the page on time-triggered scheduling for an example of logic analyzer output when it’s used to measure task timing.

Other

  • The Arduino Mega 256 bootloader might not be able to program your code if it (i.e. your code) contains three consecutive exclamation points.  The symptom of the bug is that the programmer starts to time out partway through writing your program to Flash.

Leave a Reply

Your email address will not be published. Required fields are marked *

ERROR: si-captcha.php plugin: GD image support not detected in PHP!

Contact your web host and ask them to enable GD image support for PHP.

ERROR: si-captcha.php plugin: imagepng function not detected in PHP!

Contact your web host and ask them to enable imagepng for PHP.