08 - Embedded programming

Assignment

  • Group assignment:

    • Compare the performance and development workflows for different microcontroller families
    • Document your work (in a group or individually)
  • Individual assignment:

    • Read the datasheet for the microcontroller you are programming
    • Program the board you have made to do something, with as many different programming languages and programming environments as possible.

Goals

What I think I already know

  • I have programmed AVR microcontrollers in the past for various hobby projects. I like to use the native avr libraries and not the Arduino frameword as it gives more feeling with the uC I am working with. For current projects I use ESP8266/ESP32 with the ESPhome framework.

What I want to learn

  • Working with the new ATtiny “0” and “1” series microcontrollers and the new header files from Microchip.

What I learned

  • The redesign of the AVR libraries by Microchip is a big improvement. It is focussing more on an object-oriented style of programming.
  • The new attinys have an Event system that can be used for signaling state changes (like RTC tick, ADC ready or I/O pin change) between peripherals, without waking up the main processor. This can be used for very power-efficient functions.
  • The datasheets written by Microchip are less clear than the ones written by Atmel. The pseudo-code snippets in the Atmel datasheets were very helpful in understanding the peripherals. They are gone.

Project - group assignment

As a group we slightly deviated from the assignment and focussed on programming using the Arduino board, IDE and libraries. The Arduino IDE and framework does a great job in abstracting away the differences between various microprocessor boards. We found more value in sharing and experimenting with different ways of programming the peripherals.

Arduino

Arduino” is a very well established name in electronics.
Hard-core engineers may look dismissive, but it is undeniable that the 2003 project from the Interaction Design Institute Ivrea (IDII) has given easy access to microcontrollers for the masses and enabled numerous spin-offs. Brands like AdaFruit and SparkFun would look very different if Arduino and its demand for demand for sensors and actuators had not been around.
Even in professional production environments the safety-certified Controllino has made its appearance.

Arduino is a synonym for six things.

  1. Arduino is a PCB with an Atmel microcontroller for sensing and controlling the physical world
  2. Arduino is a PCB shape and pin header definition which allocates specific functions to specific locations on the header
  3. Arduino is an Integrated Development Environment (IDE) for writing code
  4. Arduino is a software library based on the Wiring framework for abstracting different microcontrollers
  5. Arduino is a toolchain (including AVRdude) for compiling code and uploading it to the microcontroller
  6. Arduino is a bootloader on the microcontroller for setting up registers and easy uploading of code

The Arduino Uno is one of the venerable Arduino boards that started this revolutionary movement.

Wiring

The Arduino library is based on the Wiring framework.
This framework offers standard functions that translate to lower level C code. In stead of

DDRD |= (1<<6);
PORTD |= (1<<6);
PORTD &= (~1<<6);

ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1);
ADMUX = 0;
ADCSRA |= (1<<ADSC);
while (ADCSRA & (1<<ADSC));
variable = ADCW;

you can write the far more descriptive

pinMode(D6, OUTPUT);
digitalWrite(D6, HIGH);
digitalWrite(D6, LOW);

pinmode(A0, INPUT);
variable = analogRead(A0);

board
For the group assignment we used the Arduino IDE to program a venerable Arduino Uno. This board has an ATmega328p, a processor I plan to use for my final project (as I have a number of them laying around).

We started with blinking a LED on pin 9 and using variables for the on and off times.

int LED_pin = 9;
int OnTime = 100;
int OffTime = 100;

void setup() {
    pinMode(LED_pin, OUTPUT);
}

void loop() {
    digitalWrite(LED_pin, HIGH);
    delay(OnTime);
    digitalWrite(LED_pin, LOW);
    delay(OffTime);
}

We then moved through various steps to a software PWM.

int LED_pin = 9;
int Frequency = 1000/50;
int DutyCycle = 10;

void setup() {
    pinMode(LED_pin, OUTPUT);
}

void loop() {
    digitalWrite(LED_pin, HIGH);
    delay(Frequency * DutyCycle/100);
    digitalWrite(LED_pin, LOW);
    delay(Frequency * (100 - DutyCycle)/100);
}

Then we continued by adding a button

int LED_pin = 9;
int BUTTON_pin = 3;
int Frequency = 1000/50;
int DutyCycle = 10;

void setup() {
    pinMode(LED_pin, OUTPUT);
    pinMode(BUTTON_pin, INPUT_PULLUP);
}

void loop() {
    if (digitalRead(BUTTON_Pin) == LOW) {
        digitalWrite(LED_pin, HIGH);
        delay(Frequency * DutyCycle/100);
        digitalWrite(LED_pin, LOW);
        delay(Frequency * (100 - DutyCycle)/100);
    }
}

and finally we added a potentiometer to control the duty cycle (brightness) of the LED.

int LED_pin = 9;
int BUTTON_pin = 3;
int ANALOG_pin = A0;
int Frequency = 1000/50; // 1000 milliseconds / 50 Hertz = 20 ms wait time
int DutyCycle = 10;      // the percentage "on"
int AnalogValue = 0;

void setup() {
    pinMode(LED_pin, OUTPUT);
    pinMode(BUTTON_pin, INPUT_PULLUP);
    pinMode(ANALOG_pin, INPUT);
}

void loop() {
    if (digitalRead(BUTTON_pin) == LOW) {
        AnalogValue = analogRead(ANALOG_pin);
        DutyCycle = map(AnalogValue, 0, 1023, 0, 99);

        digitalWrite(LED_pin, HIGH);
        delay(Frequency * DutyCycle/100);
        digitalWrite(LED_pin, LOW);
        delay(Frequency * (100 - DutyCycle)/100);
    }
}

//Sketch uses 1538 bytes (4%) of program storage space. Maximum is 32256 bytes.
//Global variables use 11 bytes (0%) of dynamic memory, leaving 2037 bytes for local variables. Maximum is 2048 bytes.

This code has lots of places for optimalisation. The static definitions should not be int (and take up an int amount of memory) but #define definitions that will be substituted by the compiler at compile time. The other variables are now defined as global variables. In this sketch it is not too much of a problem, but when working with functions and interrupts, these must be pushed and popped from the stack. This takes up valuable clock cycles.
A more optimized sketch would look like this

#define LED_PIN       9
#define BUTTON_PIN    3
#define ANALOG_PIN    A0
#define FREQUENCY     1000/50

void setup() {
    pinMode(LED_PIN, OUTPUT);
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    pinMode(ANALOG_PIN, INPUT);
}

void loop() {
    int DutyCycle = 10;      // the percentage "on"
    
    while(1) {
        if (digitalRead(BUTTON_PIN) == LOW) {
            DutyCycle = map(analogRead(ANALOG_PIN), 0, 1023, 0, 99);

            digitalWrite(LED_PIN, HIGH);
            delay(FREQUENCY * DutyCycle/100);
            digitalWrite(LED_PIN, LOW);
            delay(FREQUENCY * (100 - DutyCycle)/100);
        }
    }
}

// Sketch uses 1478 bytes (4%) of program storage space. Maximum is 32256 bytes.
// Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes.
NOTE For this sketch, this saves 60 bytes. This might seem trivial, but when working with memory-constrained processors (like the attiny), every byte counts…

Project - stroke the hedgehog

< tada.wav >

The plan is to use PWM to make the LED “glow”, like the hedgehog is snoring. Pushing the button will turn on the LED, like waking him. Moving your hand over the photo transistor at a certain frequency will set the LED back to glowing, like stroking the hedgehog to sleep.

The attiny412 is new family of uControllers that stems from the acquisition of Atmel by Microchip. It has the AVR core, but lots of peripheral enhancements.

The code header files of the new family have a new way of addressing registers. The “old” way is to use one label like PORTA_DDR. The “new” way is to use properties of a label like PORTA.DIR. This moves the code to a more object oriented way. I like it!

I will try to write the code completely interrupt driven.

Programming environment

I use VS code and PlatformIO as a programming environment. The new attiny’s are supported by default, but the UPDI programmer requires a python program

pip install pyupdi

and a change in the platformio.ini file.

[env:ATtiny412_pyupdi_upload]
platform = atmelmegaavr
framework = arduino
board = ATtiny412
upload_speed = 115200
upload_flags =
    -d
    tiny412
    -c
    $UPLOAD_PORT
    -b
    $UPLOAD_SPEED
upload_command = pyupdi $UPLOAD_FLAGS -f $SOURCE

Uploading code is a breeze and very fast.

I also have a stand-alone mEDBG UPDI programmer and it can be used with avrdude out of the box.

[env:ATtiny412_mEDBG_upload]
platform = atmelmegaavr
framework = arduino
board = ATtiny412
upload_speed = 115200
upload_flags =
    -d
    tiny412
    -c USB
    -b
    $UPLOAD_SPEED
upload_command = pyupdi $UPLOAD_FLAGS -f $SOURCE

Adding a stand-alone mEDBG xplained UPDI programmer (as found on experimental AVR boards) requires some extra configuration in avrdude.conf:

programmer
  id    = "xplainedmini_updi";
  desc  = "Atmel AVR XplainedMini in UPDI mode";
  type  = "jtagice3_updi";
  connection_type = usb;
  usbpid = 0x2145;
;

Then the programmer can be used in platformio.ini:

[env:ATtiny412_mEDBG_upload]
platform = atmelmegaavr
board = ATtiny412
framework = arduino
upload_protocol = custom
upload_flags =
    -C
    /etc/avrdude.conf
    -p
    t412
    -P
    usb
    -c
    xplainedmini_updi
    -E
upload_command = avrdude $UPLOAD_FLAGS -U flash:w:$SOURCE:i

I decided NOT to use the Arduino library, but to use the atmegatiny library directly.

Hello-World Board

The Hello-World board was created in week 6. It has the following components attached to the microcontroller:

physical pin logical pin component
6 PA0 UPDI
4 PA1 LED
5 PA2 SENSOR
7 PA3 BUTTON
2 PA6 TX
3 PA7 RX
1 - VCC
8 - GND

Code

The program has two distinct phases, “sleeping” and “awake”. A button will wake the hedgehog, rhythmically moving a hand over the sensor will put it to sleep. The pseudo-code program will look like

#include <library.h>

initialize(){
    // initialize stuff
}

interrupt_button() {
    // start debounce timer
}
interrupt_stroke_timer {
    // if sensor is active and previous value is inactive, stroke_count++
}

main() { 
    while (1) {
        // sleep phase
        // while not button pressed, change pwm duty cycle back and forth
        // awake phase
        // while stroke_count < 4, do nothing
    }
}

In the sleeping phase, the LED will glow as if the hedgehog is snoring.
For this a timer with PWM output with varying duty cycle is required and a notification that the button is pressed. In the awake phase, the LED will shine brightly and the amount of strokes (changes from light to dark) is counted. Enough strokes will put the hedgehog back in the sleeping phase.

LED

The attiny412 has a timer (TCA) that can generate a PWM output, but according to one datasheet on the Microchip site, it looks like its output (WO3) is only attached to pin A3 (see pinout on page 14). This is confusing, as according to another datasheet on the Microchip site, it looks like its output is attached to pins A1, A2, A3 and A7 (see pinout on page 14). This is also consistent with the block diagram on page 184. The latter document is more recent (2020), so I will stick with that one for now.

Warning
Another confusing description in the datasheet is the use of the PORTA.CTRLE register. This register can be used to select alternative functions for the pins of the attiny. However, the enabling of TCA and attaching it to the output pin will already do this for you. If you do both the enabling of the output pin and setting PORTA.CTRLE, there will be no output. This cost me long evenings…

void LED_init(){
    // Initialize the PWM of timer A and use it on pin A1

    // PORTA.DIR is the direction register for I/O port A
    // where a "0" (default) means input, a "1" means output.
    //   PIN1_bm is the bitmask for PIN 1 (hardware pin 4)
    PORTA.DIR |= PIN1_bm;

    TCA0.SINGLE.PER = PWM_CYCLE_PERIOD;
    TCA0.SINGLE.CMP1 = 0;

    // TCA0.SINGLE.CTRLA is a control register for TCA
    //    TCA_SINGLE_CLKSEL_DIV16_gc divides the clock si300gnal by 16
    //    TCA_SINGLE_ENABLE_bm enables the timer
    TCA0.SINGLE.CTRLA |= TCA_SINGLE_CLKSEL_DIV16_gc |
                         TCA_SINGLE_ENABLE_bm;

    // TCA0.SINGLE.CTRLB is a control register for TCA
    //   TCA_SINGLE_CMP0EN_bm enables the comparator
    //   TCA_SINGLE_WGMODE_SINGLESLOPE_gc selects the single slope PWM
    TCA0.SINGLE.CTRLB |= TCA_SINGLE_CMP1EN_bm |
                         TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
}

SENSOR

Some I/O pins of the attiny412 can be connected to the internal Analog-to-Digital Converter (ADC). This is done with a multiplexing register. The hedgehog has a photo transistor connected to pin 2. The photo transistor is connected between the pin and ground, so low light will result in high ADC values (and vice versa).

void SENSOR_init() {
    // Initialize the ADC with photo sensor and use it on pin A2

    // PORTA.DIR is the direction register for I/O port A
    // where a "0" (default) means input, a "1" means output.
    //   PIN2_bm is the bitmask for PIN 2 (TX) (hardware pin 5)
    PORTA.IN &= ~(PIN2_bm);

    // PIN2CTRL is the control register for pin 2,
    //    PORT_PULLUPEN_bm disables the pull up resistor,
    PORTA.PIN2CTRL &= ~(PORT_PULLUPEN_bm);

    // ADC0.CTRLA is a control register for the ADC
    //   ADC_ENABLE_bm enables the ADC
    //   ADC_RESSEL_10BIT_gc sets the ADC resolution to 8 bit
    ADC0.CTRLA = ADC_ENABLE_bm |
                 ADC_RESSEL_8BIT_gc;

    // ADC0.CTRLC is a control register for the ADC
    //   ADC_PRESC_DIV4_gc sets the ADC prescaler to 4
    //   ADC_REFSEL_VDDREF_gc sets the reference voltage to uC power voltage
    ADC0.CTRLC |= ADC_PRESC_DIV4_gc |
                  ADC_REFSEL_VDDREF_gc;
    
    // ADC0.MUXPOS is the selection register for the ADC multiplexer
    //   ADC_MUXPOS_AIN2_gc selects PIN 2
    ADC0.MUXPOS  |= ADC_MUXPOS_AIN2_gc;

    ADC0.INTCTRL |= ADC_RESRDY_bm;
}

ISR(ADC0_RESRDY_vect) {
    // ADC measurement is ready

    // Clear interrupt flag by writing '1'
    ADC0.INTFLAGS = ADC_RESRDY_bm;

    gbADCready = 1;
    giADCvalue = ADC0.RES;
}

BUTTON

Every I/O pin of the attiny412 can generate an interrupt. This interrupt is shared by every pin, so in the interrupt routine, I have to determine which pin generated the interrupt. For the hedgehog, the button is connected to pin 3.

The board does not have a hardware debouncer for the button, so when connecting it to an interrupt routine it will probably fire a number of times in the noisy transition from “mo contact” to “contact”. A 10nF capacitor typically fixes this. Since only the first state change of the button is used, the multiple interrupts are not a problem.

void BUTTON_init() {
    // Initialize the I/O and use it on pin A3

    PORTA.DIR &= ~(PIN3_bm);
    // PIN3CTRL is the control register for pin A3,
    //    PORT_PULLUPEN_bm enables the pull up resistor,
    //    PORT_ISC_FALLING_gc only triggers the interrupt on the falling edge.
    PORTA.PIN3CTRL |= PORT_PULLUPEN_bm |
                      PORT_ISC_FALLING_gc;
}

ISR(PORTA_PORT_vect) {
    // An interrupt on PORT A occurred

    // Only continue if the BUTTON_PIN caused the interrupt
    if (PORTA.INTFLAGS & BUTTON_PIN) {
        // Clear interrupt flag by writing '1'
        PORTA.INTFLAGS &= BUTTON_PIN;

        if (gbState == STATE_SLEEPING) {
            gbState = STATE_AWAKE;
        }
    }
}

RTC

The attiny412 has a Real Time Clock (RTC) that can generate interrupts at fixed time intervals. There is a dedicated 32,768 Hz oscillator for this purpose. Prescaling (only use on tick in every n ticks) with 32 will give 1024 tick per second. This can be used for a very precise time signal. This RTC is used to detect the frequency of the strokes to put the hedgehog back to sleep.

void RTC_init() {
    // Initialize the Real Time Clock

    // Wait for all register to be synchronized
    while (RTC.STATUS > 0) {
        ;
    }

    // RTC.PER is the period register
    //   A period of 1024 will result in one interrupt per second
    RTC.PER = 1024; 

    // RTC.CLKSEL is the clock selection register,
    //   RTC_CLKSEL_INT32K_gc is the predefined value for the internal 32kHz oscillator.
    RTC.CLKSEL |= RTC_CLKSEL_INT32K_gc;

    // RTC.CTRLA is the control register for the RTC,
    //   RTC_PRESCALER_DIV32_gc is the Clock/32 divider (32768 / 32 = 1024 ticks per second), 
    //   RTC_RTCEN_bm will enable the RTC,
    //   RTC_RUNSTDBU_bm will run the RTC even if the uC is in sleep mode.
    RTC.CTRLA |= RTC_PRESCALER_DIV32_gc |
                 RTC_RTCEN_bm |
                 RTC_RUNSTDBY_bm;
    
    // RTC.INTCTL is the RTC interrupt control register,
    //    RTC_OVF_bm will enable the Overflow interrupt (when the counter is full and cycles to zero).
    RTC.INTCTRL |= RTC_OVF_bm;
}

ISR(RTC_CNT_vect) {
    // The RTC generated a timed interrupt

    // Clear flag by writing '1'
    RTC.INTFLAGS &= RTC_OVF_bm;

    // ADC0.COMMAND is the regiter for setting ADC commands
    //   ADC+STCONV_bm starts the conversion from A to D
    ADC0.COMMAND = ADC_STCONV_bm;
}

SERIAL

The attiny412 has a hardware serial port connected to pins 6 and 7. This will be used for debugging by sending the values of parameters so they can be seen on a computer screen. The serial input features are not used in this code.

void SERIAL_init() {
    // Initialize the serial port and use it on ports A6 and A7

    // USART0.BAUD determines the baudrate of the serial port
    //   The value is calculated using the formula (3333333 * 64 / (16 * BAUD_RATE)) + 0.5
    USART0.BAUD = 1389;
    // USART0.CTRLB is a control register for the serial port
    //   USARD_TXEN_bm enables the transmit function
    //   USARD_RXEN_bm enables the receive function
    USART0.CTRLB |= USART_TXEN_bm |
                    USART_RXEN_bm;
    // PORTA.DIR is the direction register for I/O port A
    // where a "0" (default) means input, a "1" means output.
    //   PIN6_bm is the bitmask for PIN 6 (TX) (hardware pin 2)
    //   PIN7_bm is the bitmask for PIN 7 (RX) (hardware pin 3)
    PORTA.DIR |= PIN6_bm;
    PORTA.DIR &= ~(PIN7_bm);
}

MAIN

The main program glues it all together. A completely interrupt-driven program would take too much time to research for now. This is something for future study.

int main(void) {
    // Main program

    // Declare variables
    int16_t iBrightness = 0;        
    bool bStrokeState = NEXT_HIGHER;
    uint8_t iNewADC = 0;
    uint8_t iOldADC = 0;
    uint8_t iStrokeCount = 0;

    // Initialize peripherals
    BUTTON_init();
    LED_init();
    RTC_init();
    SENSOR_init();
    SERIAL_init();
    SERIAL_sendString("The hedgehog is alive!\r\n");

    // Enable global interrupts
    sei();

    while (1) {
        // Loop forever
        while (gbState == STATE_SLEEPING) {
            // The hedgehog is in a sleeping state

            SERIAL_sendString("Zzzzz....\r\n");

            iBrightness = 0;
            while (gbState == STATE_SLEEPING && iBrightness < PWM_CYCLE_PERIOD) {
                TCA0.SINGLE.CMP1 = ++iBrightness;
                _delay_ms(1);
            }
            while (gbState == STATE_SLEEPING && iBrightness > 0) {
                TCA0.SINGLE.CMP1 = --iBrightness;
                _delay_ms(2);
            }
            while (gbState == STATE_SLEEPING && iBrightness < 384) {
                ++iBrightness;
                _delay_ms(1);
            }
        }

        while (gbState == STATE_AWAKE) {
            // The hedgehog is in an awake state

            // Set the LED to max brightness
            TCA0.SINGLE.CMP1 = PWM_CYCLE_PERIOD;

            if (gbADCready == 1) {
                // if a new ADC measurement is available

                // Acknowledge the processing of the ADC measurement
                gbADCready = 0;
                iNewADC = giADCvalue;

                SERIAL_sendString("Huh?... ");
                SERIAL_sendString("NewADC: "); SERIAL_sendInt(iNewADC); 
                SERIAL_sendString(" - OldADC: "); SERIAL_sendInt(iOldADC);
                SERIAL_sendString(" - Stroke: "); SERIAL_sendInt(iStrokeCount);
                SERIAL_sendString("\r\n");

                if (iNewADC > iOldADC) {
                    // If the photo sensor is obscured

                    if (bStrokeState == NEXT_HIGHER) {
                        // Was it not obscured before?
                        // Next time, it should be not obscured
                        bStrokeState = NEXT_LOWER;                    
                    } else {
                        // If the soothing rhythm is broken, reset all strokes
                        iStrokeCount = 0;
                    }
                }
                if (iNewADC < iOldADC) {
                    // If the photo sensor is not obscured

                    if ( bStrokeState == NEXT_LOWER) {
                        // Was it obscured before?
                        // Then this is a stroke
                        iStrokeCount++;
                        // Next time, it should be not obscured
                        bStrokeState = NEXT_HIGHER;
                    } else {
                        // If the soothing rhythm is broken, reset all strokes
                        iStrokeCount = 0;
                    }
                }
                // Store the ADC value for use next time
                iOldADC = iNewADC;

                if (iStrokeCount > REQUIRED_STROKES) {
                    // There were enough strokes

                    // No given strokes can be used next time
                    iStrokeCount = 0;

                    // Go to sleep...
                    gbState = STATE_SLEEPING;
                }

            } else {
                _delay_ms(1);
            }
        }
    }
}

Project files

The source code for the snoring hedgehog can be found here.

Reflections

What went right

  • The Hello-world board worked in one go. YaY
  • The VS Code and PlatformIO support programming (but not debugging) the new attiny’s out of the box.
  • The hedgehog is snoring happily.

What went wrong and how did I fix it

  • There are more versions of the same datasheets on the Microchip site. This is very confusing as they contain contradicting information. I went with the latest version, but this made me loose quite some time.

What would I do differently next time

  • I would add a VCC pin to the UPDI programmer. Now I need a bulky setup for power and programming.
  • I would like to spend more time on the new event mechanism the new attinys provide.