Fab Academy 2019

Progress Documentation - Christian Schmidt

Embedded Programming

Making your boards do things

This week, I programmed my version of the hello-world board. I wrote C code and created makefiles that compile the code and flash it to the target board using avrdude and my ISP. Additionally, I translated one of my programs into assembly. I'm a computer science major, so I have quite some experience writing and debugging programs. Although these programs seldom run on a microcontroller, and my knowledge on registers, bit juggling, and interrupts is a bit rusty, I had no major problems with this assignment.

Here's the list of stuff I used for this assignment:

The programming process

The overall programming process for AVR devices consists of compiling the code, converting the binary to a hex file, and uploading the hex file to the MCU with a programmer (for example the FabISP that was built in the electronics production week). Since these tasks are repetitive and cumbersome, one usually creates a script that executes the necessary commands. I've written a Makefile for this purpose which can be adapted fairly easy to program other AVR devices. It can be found in the download section, together with the rest of the code for this week. Programming a board is then as easy as connecting the programmer to the board and the pc, and then running the Makefile.

Reading the manual

An integral part of programming for embedded systems is reading the datasheet of whatever microcontroller one is programming. The datasheet specifies ports, pin descriptions, additional pin functions, special registers, clock sources, memory usage, sleep modes, available interrupt sources, buses, comparators and converters, electrical characteristics, ... in short, everything you need to know about the chip you're going to program. There is usually no need to read the whole document end-to-end, but you should be familiar enough with the contents to know about the functionality you want to use.

After reading the manual for the ATTiny44, on which my board is based, I know, for example, that for each pin on the microcontroller, there are three registers that control the basic behaviour of that pin:

  • DDR: Data Direction Register, controls whether the pin is an input or output pin.
  • PORT: depending on the value of DDR, enables/disables the internal pull-up resistor or drives the pin high/low.
  • PIN: Stores the values of pins; can be used to toggle the value of the PORT register.

DDRA7, for example, which is the seventh bit of the PORTA 8-bit register, controls whether pin A7 is an input or output pin; if we write a 1 to this bit, it is configured as an output pin, and if we write a 0, it's configured as an input pin. Similarly, if we write a 1 to PORTA7, we can drive the pin to a high voltage level (if DDRA7 is set to 1, i.e. A7 is an output pin) or we can enable the internal pull-up resistor (if DDRA7 is set to 0). Keep in mind that these are operations on single bits, but the registers are 8 bit wide, so we need to use bit masks to set/unset single bits.

Another important piece of information on these registers is that some combinations of register values require an intermediate step when, for example, turning an input pin to an output pin. This seems to be mostly the case when an input pin with disabled internal pull-up resistor is turned into an active output pin: an intermediate step of input with enabled pull-up or of low output is required.

Other important sections (at least for the following four code samples) are about interrupts, clock timings, and sleep modes.

Makefile

The makefile contains targets to compile the source code, program the microcontroller, and set the fuses so that the crystal is used as external clock. Do not set the fuses if your board has no external clock, or you will brick it. The project to compile can be set on the command line; make MAIN=interrupt program, for example, will compile interrupt.c and program the board. As always, it can be found in the download section.

CC=avr-gcc
CFLAGS=-mmcu=$(MMCU) -Wall -Os -DF_CPU=$(F_CPU)

MMCU=attiny44
PROGRAMMER=usbtiny
F_CPU=20000000

MAIN=blink
SOURCES=$(MAIN).c

all: $(MAIN).hex

# Implicit rule to create hex files from source files
%.hex: %.out
	# $< is the first dependency
	# $@ is the left side of the rule
	avr-objcopy -O ihex $< $@
	avr-size --mcu=$(MMCU) --format=avr $<

%.out: $(SOURCES)
	$(CC) $(CFLAGS) -I./ $< -o $@

program: $(MAIN).hex
	avrdude -p t44 -P usb -c $(PROGRAMMER) -U flash:w:$<

fuses: $(MAIN).hex
	avrdude -p t44 -P usb -c $(PROGRAMMER) -U lfuse:w:0x5E:m

clean:
	rm *.hex

Making the led blink

The hello-world program of the microcontroller world: blink with an LED. This is one of the simplest working programs, and it's quite short:

#include <avr/io.h>
#include <util/delay.h>

//Port A data register
#define LED_PORT PORTA
//Port A data direction register
#define LED_DIR DDRA
//Port A input pins
#define LED_PINS PINA
//Pin number where LED is connected
#define LED_PIN (1 << PA3)

int main(void){
  //Enable pull-up resistor after tristate (see datasheet 10.1.3)
  LED_PORT |= LED_PIN;
  //Configure pin A3 (i.e. the one for the LED) as output
  LED_DIR |= LED_PIN;

  while(1){
    _delay_ms(50);
    //Toggle LED
    LED_PORT ^= LED_PIN;
  }
}

First, we import two headers: avr/io.h for the declarations of the PORTA, DDRA, and PINA registers, and util/delay.h> for the implementation of _delay_ms, which lets us wait for a specified number of milliseconds. Then, we define a couple of macros so that we don't have to remember where we connected the LED. Macros are substituted literally, so beware of subtle bugs. Usually, they can be made safe by enclosing their contents in parentheses. Finally, we can write our main function, which consists of the initialization of the LED pin and an endless loop of waiting and then toggling the value of the output pin. This makes the LED blink. Hurray!

Integrating the button

Now, what about the button? It's very simple to use as well. Here, it's important to enable the internal pull-up resistor of pin A7, otherwise we would get a short cut to ground whenever the button is pressed. The overall program is the same, except for a couple of additional macros and a conditional statement in the while loop. This condition ensures that the LED is turned off unless the button is pressed; then, it will blink fast until the button is released.

#include <avr/io.h>
#include <util/delay.h>

//Port A data register
#define LED_PORT PORTA
//Port A data direction register
#define LED_DIR  DDRA
//Port A input pins
#define LED_PINS PINA
//Pin number where LED is connected
#define LED_PIN  (1 << PA3)
//Same for the button
#define BTN_PORT PORTA
#define BTN_DIR  DDRA
#define BTN_PINS PINA
#define BTN_PIN  (1 << PA7)

int main(void){
  //Enable pull-up resistor after tristate (see datasheet 10.1.3)
  LED_PORT |= LED_PIN;
  //Configure pin A3 (i.e. the one for the LED) as output and turn it off
  LED_DIR |= LED_PIN;
  LED_PORT &= ~LED_PIN;

  //Enable pull-up resistor, since the button is connected to ground
  BTN_PORT |= BTN_PIN;
  //Configure button pin as input
  BTN_DIR &= ~BTN_PIN;

  while(1){
    _delay_ms(10);
    if(!(BTN_PINS & BTN_PIN)){
      //Toggle LED
      LED_PORT ^= LED_PIN;
    } else {
      LED_PORT &= ~LED_PIN;
    }
  }
}

Reacting via interrupts

Now, continuously looping and waiting for something to happen (also called polling) is not very efficient. Do we have to run an endless loop only to detect that there is nothing to do? Or, when writing more sophisticated programs, do we have to continuously check that maybe in this iteration the button was pressed and react accordingly? Soon, we would end up with unreadable and not very efficient spaghetti code.

But there is hope! We may use interrupts, which are special signals generated, for example, when the logic level of a pin changes, and that interrupt normal program flow to execute so-called interrupt service routines (ISRs). This way, we can react to the button being pressed without having to check if its pressed every iteration.

The code is similar to the code in the preceding section - the button switches between a blinking and a turned off LED. This time, we use the ISR macro to define an interrupt routine for pin change interrupts at port 0 (i.e. port A, where the button is connected). ISRs should be short, because they block normal execution and other interrupts, so we only change a global variable (indicating whether to blink or not) and turn the LED off. I also noticed that the _delay_ms function I was using did not have the proper timing until I set the clock division to 1.

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

// Port A data register
#define LED_PORT PORTA
// Port A data direction register
#define LED_DIR  DDRA
// Port A input pins
#define LED_PINS PINA
// Pin number where LED is connected
#define LED_PIN  (1 << PA3)
// Same for the button
#define BTN_PORT PORTA
#define BTN_DIR  DDRA
#define BTN_PINS PINA
#define BTN_PIN  (1 << PA7)

// Pin change interrupt enable bit for PCINT7..0
#define BTN_INTERRUPT     (1 << PCIE0)
// PCINT corresponding to the button's pin
#define BTN_INTERRUPT_PIN (1 << PCINT7)

volatile int blink = 0;

ISR(PCINT0_vect) {
  blink ^= 1;
  LED_PORT &= ~LED_PIN;
}

int main(void) {
  LED_PORT |= LED_PIN;
  LED_DIR |= LED_PIN;

  BTN_PORT |= BTN_PIN;
  BTN_DIR &= ~BTN_PIN;

  // Set the clock division to 1 to operate at 20MHz
  CLKPR = (1 << CLKPCE);
  CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0);

  // Enable interrupts for port A
  GIMSK |= BTN_INTERRUPT;
  // Enable pin change interrupts for pin A7
  PCMSK0 |= BTN_INTERRUPT_PIN;

  // Enable interrupts - sets the interrupt enable bit
  sei();

  LED_PORT &= ~LED_PIN;

  while(1){
    // Wait for interrupt to change value of blink
    if(blink){
      LED_PORT ^= LED_PIN;
      _delay_ms(100);
    }
  }
}

Granted, we still have to loop to make the LED blink. But at least we do not need to check the button repeatedly, so that's a win. However, we can do even better by going to sleep.

Sleeping when there's nothing to do

If we just want to turn the LED on & off when the button is pressed, we do not need to waste CPU cycles by mindlessly looping until the interrupt fires. Instead, we can make use of the power save utilities of the ATTiny44: shut everything down that's not absolutely necessary, wake up on interrupt, do what's necessary, and go back to sleep. Activating the various power saving states involves a lot of bit juggling, which gets tiring very quickly, so we use the header <avr/sleep.h> instead, which provides macros for the most common microcontrollers.

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>

// Port A data register
#define LED_PORT PORTA
// Port A data direction register
#define LED_DIR  DDRA
// Port A input pins
#define LED_PINS PINA
// Pin number where LED is connected
#define LED_PIN  (1 << PA3)
// Same for the button
#define BTN_PORT PORTA
#define BTN_DIR  DDRA
#define BTN_PINS PINA
#define BTN_PIN  (1 << PA7)

// Pin change interrupt enable bit for PCINT7..0
#define BTN_INTERRUPT     (1 << PCIE0)
// PCINT corresponding to the button's pin
#define BTN_INTERRUPT_PIN (1 << PCINT7)

static volatile int pressed = 0;
ISR(PCINT0_vect) {
  if(!pressed){
    LED_PORT ^= LED_PIN;
  }
  pressed = ~pressed;
}

int main(void) {
  LED_PORT |= LED_PIN;
  LED_DIR |= LED_PIN;

  BTN_PORT |= BTN_PIN;
  BTN_DIR &= ~BTN_PIN;

  // Set the clock division to 1 to operate at 20MHz
  CLKPR = (1 << CLKPCE);
  CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0);

  // Enable interrupts for port A
  GIMSK |= BTN_INTERRUPT;
  // Enable pin change interrupts for pin A7
  PCMSK0 |= BTN_INTERRUPT_PIN;

  // Enable interrupts - sets the interrupt enable bit
  sei();

  LED_PORT &= ~LED_PIN;

  MCUCR |= (1 << SM1) | (1 << SM0);

  //sleep_mode();

  while(1){
    // Wait for interrupt
    sleep_mode();
  }
}

There! Now, we use as little power as possible and do only what's necessary, when it's necessary.

Translating sleep.c into assembly

As a last exercise for this assignment, I translated the last iteration of my C code to assembly language. I have little to no experience writing assembly code, so I wanted to check how assembly compares to a high level language like C. The first problem I stumbled upon was that there are actually a couple of different assembly dialects and compilers. For sake of simplicity (no additional programs), I decided to use the assembler that is built into avr-gcc. However, that is not the compiler used by Neil (I think he uses gavrasm), so I could not compile the example programs from the Fab Academy pages. Also, I needed a list of available instructions. In order to use interrupts, I needed to include the <avr/io.h> header, just like in C (as far as I'm aware, this works only in avr-gcc). Additionally, every I/O port and configuration register has to be enclosed in the _SFR_IO_ADDR() macro.

#include <avr/io.h>

; declarations for better readability
.equ LED_PORT, _SFR_IO_ADDR(PORTA)
.equ LED_DIR, _SFR_IO_ADDR(DDRA)
.equ LED_PIN, PA3
.equ BTN_PORT, _SFR_IO_ADDR(PORTA)
.equ BTN_DIR, _SFR_IO_ADDR(DDRA)
.equ BTN_PIN, PA7
.equ BTN_INTERRUPT, (1 << PCIE0)
.equ BTN_INTERRUPT_PIN, (1 << PCINT7)
; naming registers we use
.equ tmp, 17
.equ tmp1, 18
.equ pressed, 19

  .section .text

; jump to main program
rjmp main

  ; declaring the pin change interrupt routine
  .global PCINT0_vect
PCINT0_vect:
  cp pressed, 0
  brne skip
  ; if the button is already pressed, toggle LED
  in tmp, LED_PORT
  eor tmp, tmp1
  out LED_PORT, tmp
skip:
  ; toggle internal button state
  eor pressed, tmp1
  reti

  ; declaring the main routine
  .global main
main:
  ; initialize LED pin (i.e. output high)
  sbi LED_PORT, LED_PIN
  sbi LED_DIR, LED_PIN

  ; initialize button pin (i.e. input, pullup enabled)
  sbi BTN_PORT, BTN_PIN
  cbi BTN_DIR, BTN_PIN

  ; configure clock division to operate at 20MHz
  ldi tmp, (1 << CLKPCE)
  ldi tmp1, (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0)
  out _SFR_IO_ADDR(CLKPR), tmp
  out _SFR_IO_ADDR(CLKPR), tmp1

  ; enable interrupts for the port of the button
  in tmp, _SFR_IO_ADDR(GIMSK)
  sbr tmp, BTN_INTERRUPT
  out _SFR_IO_ADDR(GIMSK), tmp

  ; enable interrupts for the pin of the button
  in tmp, _SFR_IO_ADDR(PCMSK0)
  sbr tmp, BTN_INTERRUPT_PIN
  out _SFR_IO_ADDR(PCMSK0), tmp

  ; globally enable interrupts
  sei

  ; turn LED off
  cbi LED_PORT, LED_PIN

  ; select "standby" sleep mode
  in tmp, _SFR_IO_ADDR(MCUCR)
  sbr tmp, (1 << SM1) | (1 << SM0)
  out _SFR_IO_ADDR(MCUCR), tmp

  ; initialize register with bit mask for pin A3
  sbr tmp1, (1 << LED_PIN)
  loop:
    ; set sleep enable bit
    in tmp, _SFR_IO_ADDR(MCUCR)
    sbr tmp, (1 << SE)
    out _SFR_IO_ADDR(MCUCR), tmp

    ; go to sleep
    sleep

    ; clear sleep enable bit
    in tmp, _SFR_IO_ADDR(MCUCR)
    cbr tmp, (1 << SE)
    out _SFR_IO_ADDR(MCUCR), tmp

    ; jump back 
    rjmp loop

The program is fairly "hacky", as I used a register instead of a global variable to manage the internal button state. And I skipped all of the stack management that is normally needed in interrupt service routines; it's not necessary for this short program.

The assembly program needs less space (122 bytes vs. 168 bytes for the C equivalent), but is around 50% longer and definitely less readable than its C counterpart. For more complicated programs, these differences might get even more pronounced; it surely is possible to hand-tune assembly code so it's more efficient and smaller in size, but unless that is absolutely necessary, I will stick with C.


Downloads