Close

Plethora of peripherals - using interrupts, timers and events in Arduino IDE

A project log for ATtiny 0 Series programming on the cheap

The newer range of ATtinys can be programmed with an Arduino and Atmel Studio 7. NOW ALSO VIA THE ARDUINO IDE!

Simon MerrettSimon Merrett 07/23/2019 at 01:160 Comments

Some people who have been following #Yapolamp recently may have noticed these two projects are blurring into each other a little. Particularly this log on getting efficient pulse timing using the ATtiny402.

This log is a bit of a handful but could serve as a helpful resource for someone who is coming to the ATtiny 0 series for the first time and has only used the Arduino IDE before.

For context, it's worth knowing that this code is an interim program for the next version of #Yapolamp. It is a low powered torch, based on the efficient LED-driving principles of the #TritiLED project. It has two main modes, which are created in this code. 

The first mode is an "always on" mode. It needs to pulse the LEDs at about 30Hz so that they look like they are dimly on and you could find the torch in the dark if you needed to. 30Hz is good enough so that it doesn't look like it's flashing unless you look really carefully. We need the circuit to use as little power as possible to make the supercapacitors powering it last as long as possible and to ensure it will be glowing and showing its location whenever it may be needed.

The second mode is "fully on", which is as you would imagine, getting a decent amount of brightness out of the LEDs without wasting any of the charge in the supercapacitors.


I'm using the ATtiny402, a chip with lots to offer for this application. The main features we're going to look at this time are its Timer Counter B0 (TCB0), its real time clock (RTC), its interrupts, events and low power STANDBY mode.

In contrast to e.g. the ATtiny85 and older ATtinies, the new 0 and 1 series have different peripherals and you often have to use a different peripheral than you would have in the ATtiny85 to achieve the same effect. For example, to wake from sleep after a period of time with the ATtiny85, most of us would have used a WDT or watchdog timer. In the new ATtinies, that's not what a WDT is for and so it doesn't get involved in waking from sleep, only resetting if your code fails to run properly. To wake from low power modes, there are several ways we can use (great - we have choice) and they are based on the RTC.

Timer B 

TCB0 is similar to the timer counters you will perhaps be familiar with from the older AVRs. I haven't explored it in depth but one feature that makes it stand out for LED driving is its "single shot" mode where, based on a trigger (which I'll come back to) you can set a timer going for one single run, without repeating when it reaches the end. We can also connect this to a hardware pin and none of this depends on the cpu to execute the pulse. However, it does need a clock running so if you were to try using this in a low power mode like standby, make sure both TCB0 and the 20MHz internal oscillator are set up for standby operation (bear in mind OSC20M has something like 125uA current cost to run in standby, so you must think carefully if you want to use TCB0 in low power modes).

Autocancelling peripherals

Unlike the older ATtinies, it seems that the new 0 series is very good at automatically turning off peripherals, such as ADC etc, when you enter low power modes such as STANDBY. I may end up regretting that statement but I have noticed good power performance is achieved without a plethora of commands to deactivate e.g. ADC before sleeping.

Interrupts

Interrupts are fairly straightforward. You must take care to clear the flags from within the Interrupt Service Routine (ISR) for predictable results (I observed continuous firing without the specific interrupt conditions because I tried clearing the flag elsewhere, and I have seen other people have similar issues on the AVRfreaks forum). 

RTC

The RTC interrupts are worth exploring because here we have a choice. We can run the RTC overflow (OVF) interrupt to give us one ISR after a certain period and we can run the Periodic Interrupt Timer (PIT) to get a separate ISR based on the RTC's clock but at a different time than the RTC_OVF interrupt. Nice! Don't forget to make your variables volatile if they are going into an Interrupt Service Routine.

A big mistake I made on timers and RTC was due to my experience of using old AVR timers. I wanted to vary the duration of a timer before an interrupt would fire. In older AVRs I would have done this by varying the value of the compare register. CMP or compare in the 0 series is a valid register but it only helps you interrupt at a certain point within the timer's period. If you want to effectively shorten or lengthen that period duration, you want to edit the period register, or PER (as in RTC.PER).

Events

The events system on the new 0 and 1 series chips is great. You can use it as glue to take the input or output of one peripheral and send it to another peripheral, without getting the CPU involved. This works really nicely for fast response times. An example from #Yapolamp is that I wanted the next LED pulse to start as soon as the inductor driving the LEDs had stopped discharging through them. I was able to sense the inductor voltage using an input pin and use a falling edge on that pin to trigger the next single shot pulse from TCB0, using events only - not even an interrupt! It can also do it asynchronously, making response times fast and reducing cpu burden. Events have so much potential and deserve many logs to themselves. I definitely encourage you to look into them and see what you can achieve with minimal use of the CPU.

Low Power

Before we get into the specific low power modes, just remember that even I can achieve 18uA without entering a sleep mode by swapping the 20MHz clock for the internal 32kHz oscillator. There are plenty of applications where this would be good enough. The other amazing thing is that you can change clock sources on the fly in software, so if you find a demanding task has arrived at your microcontroller, it can raise its clock and crunch through the task before setting itself back to 32kHz.

Nevertheless, we also have low power modes. I won't touch on POWER_DOWN here because I haven't tried it. The reason for this is that although power performance is good, it doesn't let me run any peripherals in the background and is relatively slow to wake up - not good for me with all these LEDs to blink so often. I opted for STANDBY. It has excellent general performance (0.71uA plus your peripherals according to the datasheet). You do need to configure some peripherals, such as RTC and TCB0, to keep working in STANDBY mode. Keep an eye out in the datasheet.

Do bear in mind that there are several parts of the microcontroller's memory that have protected access to stop us accidentally changing something critical without making sure we really mean it! There are some different ways to do "protected writes" of settings to these areas, hunt around in a "Getting Started with RTC" or something similar from Microchip for examples.  Clock settings are an example of a protected areas of memory. 

Bitwise operators

When you are setting up registers, do be careful to use |= or = appropriately. If you look in the datasheet and see that you might overwrite other register settings inadvertently, then you need to use |= to avoid wiping those when you set one or a few bits. Something that I didn't know is that there are very helpful shorthand names for the register locations and settings. These often look like "TBC0.CTRLA" or "RTC_OVF_bm". You can see how they are mapped to the registers in the datasheet but sadly the datasheet doesn't tell you what to type to address a particular register. So you will find examples very helpful for this. I also searched for and found the files "ATtiny402.adtf" and "iot402.h", which will both be available on your computer if you have Atmel Studio and the ATtiny device pack installed. If you have the megaAVR Arduino board pack installed in the Arduino IDE, you also probably have a version of "iot402.h" somewhere in your Arduino15 folder (it's hidden in user appdata in windows). Mine is in

C:\Users\[username]\AppData\Local\arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino5\avr\include\avr

These files open in a text editor like Notepad++ and should help if you want to address a register without having an example in front of you.

Those suffixes are important too: 

_bm means it's a bitmask;

_gc means it's a group configuration (often two 8 bit registers / 16 bits);

_bp means it's a bit position, within a register of say 8 bits;

|= can work with these but remember also that "= 1 << xyz_bp" can also help.

If you have several bits to write at the same time, you can OR them together, like:

PORTA.DIRSET = PIN1_bm | PIN2_bm | PIN3_bm;        to set three pins as outputs.


The code

Here's some prelim code that compiles nicely in the Arduino IDE but uses more direct register addressing and manipulation than Arduino functions. Hopefully you can see what it's doing but if not, here's the gist:

We set a single shot timer to generate the LED driver pulses. If running in fully on mode, the microcontroller is awake and pulses are rapidly generated by pin PA7 sensing the inductor voltage and driving the pulse with an event channel. Periodically, in much slower time, we have the RTC firing an interrupt. This is used to wake the system up when in "always on" mode and schedule another pulse via the reconfigured event generator on channel 0, to produce a very low power light (~20uA including LEDs).

Finally (in general terms) a PIT interrupt fires even less regularly than the RTC OVF that in "fully on" mode will recalculate the pulse duration for the LED driving TCB0 single shot pulse, based on a nifty reading of the supply voltage using no extra pins. I also use this PIT as a debounce - it has the right kind of period to allow this role without needing to employ a separate section of code or a pin interrupt. Given that we're not chasing fast events on the button, this is more than adequate.

Caveat. I'm sure this code is full of mistakes but I hope you may find some of the lines useful as examples for your own experimentation with these nifty microcontrollers.

/* This version manages to have two modes, "always on" (0) and "full on" (1).
 *   Sleep works in always on mode but not full on mode.
 *   Has auto pulse duration calculation for full on mode
 *   Uses events, pin-change interrupt, RTC OVF interrupt and TCB0 to generate modes
*/
#include <avr/sleep.h>
#include <avr/interrupt.h>

volatile uint8_t mode = 0;          // current options: "always on" (initial) and "fully on"
volatile uint8_t wasPushed = 0;     // button debouncing variable
volatile uint8_t isPushed = 0;      // button debouncing variable

ISR(RTC_CNT_vect)
{
  RTC.INTFLAGS = RTC_OVF_bm;        // Wake up from STANDBY. Just clear the flag (required) - the RTC overflow Event will handle the pulse
}

ISR(RTC_PIT_vect)
{
  RTC.PITINTFLAGS = RTC_PI_bm;      // Clear interrupt flag by writing '1' (required)
  modeToggle();                     // Handle modes changes in this function
  if (mode) adjustPulse();          // If in fully on mode, change pulse durations based on supply voltage
}

ISR(TCB0_INT_vect) {                // This interrupt fires when the timer reaches the CCMP value
  TCB0.CTRLA = 0 << TCB_ENABLE_bp;  // disable TCB0 until after an RTC overflow
  TCB0.INTFLAGS = TCB_CAPT_bm;      // clear the flag
}

void setup() {
  RTC_init();                         // Initialise the RTC timer
  EventSys_init();                    // Initialise the Event System
  ADC_init();                         // Initialise the ADC
  Pins_init();                        // Initialise the hardware pins
  TCB0_init();                        // Initialise Timer Counter B0
  set_sleep_mode(SLEEP_MODE_STANDBY); // set power saving mode as STANDBY, not POWER DOWN
  sleep_enable();                     // enable sleep mode
  sei();                              // turn on interrupts
  delay(20);                          // wait for clocks to stabilise (important) before...
  TCB0.CTRLB |= 1 << TCB_CCMPEN_bp;   // ... turning on the TCB0 waveform out (WO) via the pin PA6
}

void loop() {
  if (mode == 0) {                                  // in this case we are in low power always on mode
    PORTA.DIRCLR |= PIN1_bm;                        // become input and...
    PORTA.PIN1CTRL |= PORT_PULLUPEN_bm;             // ...pullup to save power
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_RTC_OVF_gc;     // feed the LED pulse with the RTC, about 32Hz so persistence of vision makes it look always on
    TCB0.INTCTRL |= 1 << TCB_CAPT_bp;               // stop the TCB0 interrupt
    sleep_cpu();                                    // go to sleep until the RTC wakes us
    TCB0.CTRLA |= 1 << TCB_ENABLE_bp;               // on waking, re-enable the TCB0 pulses
  }
  else {                                            // in this case, we are on full brightness
    PORTA.DIRSET |= PIN1_bm;                        // become OUTPUT
    PORTA.OUTCLR |= PIN1_bm;                        // provide a connection to GND for the pulse feedback divider
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN7_gc;  //enable event sys to take PA7 logic edge as an input
    TCB0.INTCTRL = 0 << TCB_CAPT_bp;                // disnable the TCB0 overflow interrupt to disable the single shot pulses
    TCB0.CTRLA |= 1 << TCB_ENABLE_bp;               // re-enable the TCB0 pulses
  }
}

void TCB0_init (void)
{
  TCB0.CCMP = 0x0017;                           /* Compare or Capture: 0x0017 works to start with. Is varied with voltage */
  //TCB0.CNT = 0x0100;                          /* no need to use but if we didn't want the single shot to fire straight away, we'd set this equal to TCB0.CCMP
  TCB0.CTRLB = 1 << TCB_ASYNC_bp                /* Asynchronous Enable: enabled. Gives a tighter pulse feedback loop */
               //| 1 << TCB_CCMPEN_bp           /* Pin Output Enable: enabled later on in setup. On the Waveform Out (WO) Pin*/
               //| 0 << TCB_CCMPINIT_bp         /* Pin Initial State: disabled. Only useful if you want to start in a particular state */
               | TCB_CNTMODE_SINGLE_gc;         /* Single Shot. This is key for generating controlled pulses */
  // TCB0.DBGCTRL = 0 << TCB_DBGRUN_bp;         /* Debug Run: disabled. Not required for this application */
  TCB0.EVCTRL = 1 << TCB_CAPTEI_bp              /* Event Input Enable: enabled. Rely on both pin input and RTC events to trigger it */
                | 0 << TCB_EDGE_bp              /* Event Edge: disabled. We only want one input edge to trigger a pulse */
                | 0 << TCB_FILTER_bp;           /* Input Capture Noise Cancellation Filter: disabled */
  TCB0.INTCTRL = 1 << TCB_CAPT_bp;              /* Capture or Timeout: enabled. This enables the CCMP match interrupt, which is needed in this application */
  TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc            /* CLK_PER (No Prescaling) */
               | 1 << TCB_ENABLE_bp             /* Enable: enabled. Turn the timer on! */
               | 1 << TCB_RUNSTDBY_bp;          /* Run Standby: enabled. Need this so pulses work in STANDBY power saving mode */
  //     | 0 << TCB_SYNCUPD_bp; /* Synchronize Update: disabled */
}

void EventSys_init(void)
{
  EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // set up TCB0 event input to take event channel 0
  EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_RTC_OVF_gc;      // start in power saving mode by triggering pulses with the RTC overflow event
}

void RTC_init(void)
{
  while (RTC.STATUS > 0) {
    ;                                   // Wait for all registers to be synchronized
  }
  RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;    // 32.768kHz Internal Crystal Oscillator (OSC32K)
  while (RTC.STATUS > 0) {
    ;                                   // Wait for all registers to be synchronized
  }
  RTC.CTRLA = RTC_PRESCALER_DIV1_gc | RTC_RTCEN_bm | RTC_RUNSTDBY_bm; // set no prescaler, enable RTC and set to run in standby
  while (RTC.STATUS > 0) {
    ;                                   // Wait for all registers to be synchronized
  }
  RTC.INTCTRL = RTC_OVF_bm;             // set up the overflow interrupt
  while (RTC.STATUS > 0) {
    ;                                   // Wait for all registers to be synchronized
  }
  RTC.PER = 0x01FF;                     // set up the overflow period in number of clock cycles
  RTC.PITINTCTRL = RTC_PI_bm;           // PIT Interrupt: enabled
  RTC.PITCTRLA = RTC_PERIOD_CYC4096_gc  /* RTC Clock Cycles 4096, resulting in 32.768kHz/4096 = 8Hz */
                 | RTC_PITEN_bm;                       /* Enable PIT counter: enabled */
}

void ADC_init() {
  VREF.CTRLA = VREF_ADC0REFSEL_1V1_gc;  /* Set the Vref to 1.1V*/
  /* The following section is directly taken from Microchip App Note AN2447 page 13*/
  ADC0.MUXPOS = ADC_MUXPOS_INTREF_gc;    /* ADC internal reference, the Vbg*/
  ADC0.CTRLC = ADC_PRESC_DIV4_gc        /* CLK_PER divided by 4 */
               | ADC_REFSEL_VDDREF_gc   /* Vdd (Vcc) be ADC reference */
               | 0 << ADC_SAMPCAP_bp;    /* Sample Capacitance Selection: disabled */
  ADC0.CTRLA = 1 << ADC_ENABLE_bp       /* ADC Enable: enabled */
               | ADC_RESSEL_10BIT_gc;    /* 10-bit mode */
  ADC0.COMMAND |= 1;                    // start running ADC
}

void Pins_init() {
  PORTA.DIRSET |= PIN1_bm; // output. for voltage divider GND. PIN0 is UPDI
  PORTA.DIRCLR |= PIN2_bm; // input. not used
  PORTA.DIRCLR |= PIN3_bm; // input. for button
  PORTA.DIRSET |= PIN6_bm; // output. for driver pulse
  PORTA.DIRCLR |= PIN7_bm; // input. for pulse feedback

  PORTA.OUTCLR |= PIN1_bm;                            // provide a connection to GND for the pulse feedback divider
  PORTA.PIN2CTRL = PORT_PULLUPEN_bm;                  // pullup for power saving
  PORTA.PIN3CTRL = PORT_PULLUPEN_bm | PORT_INVEN_bm;  // pullup and invert for button logic
  PORTA.PIN7CTRL = PORT_PULLUPEN_bm | PORT_INVEN_bm;  // pullup and invert for pulse feedback logic

}

void modeToggle() {
  isPushed = (PORTA.IN & PIN3_bm);  // read the input pin
  if (isPushed && !wasPushed) {     // if the pin is pushed but wasn't last time
    mode++;                         // increment the mode
    if (mode > 1) mode = 0;         // set the mode to zero if it goes too high
  }
  wasPushed = isPushed;             // update the comparator for next time
}

void adjustPulse() {
  if (ADC0.INTFLAGS) {                                                    // If there is a reading to see
    cli();                                                                // turn interrupts off temporarily
    float reading = ( 0x400 * 1.1 ) / ADC0.RES ;                          // calculate the Vcc value
    int i = (int)(146.3 - 35.541 * reading + 2.3676 * reading * reading); // calculate the pulse duration in TBC0 ticks
    unsigned int pulseOn = i;                                             // convert to unsigned integer
    TCB0.CCMP = pulseOn;                                                  // update the TCB0 compare match register
    TCB0.CNT = pulseOn - 1; /* test code */                               // update the TCB0 counter register so a pulse isn't fired out of sync (may not be required)
    sei();                                                                // turn interrupts back on
  }
  if (mode == 1) {                                                        // only set the ADC to run if in fully on mode
    ADC0.COMMAND = 1 << ADC_STCONV_bp;                                    // set ADC running for the next reading
  }
}

Back to #Yapolamp now. Good luck with your ATtinies! 

Discussions