Registers, datasheets and… stuff
Note: All the files here
- The ATtiny24/44/84 datasheet
- Programming the board
The ATtiny24/44/84 datasheet
In this section I will detail some of my learnings not only during this week, but during the whole fabacademy about interrupts. In the Section 9 of the ATtyiny24/44/84 datasheet, one can find information about how the interrupts are triggered and handled.
Now, the Datasheet does not give us much information about the interrupt itself, and for a newbie like me, it is better to summarize some basics before delving into the datasheet’s maze.
What is an Interrupt?
An interrupt is basically an event that requires immediate attention by the microcontroller. When an interrupt occurs the microcontroller pauses its current task and attend to the interrupt by executing an Interrupt Service Routine (ISR) at the end of the ISR the microcontroller returns to the task it had pause and continue its normal operations.
In order for the microcontroller to respond to an interrupt event the interrupt feature of the microcontroller must be enabled along with the specific interrupt. This is done by setting the Global Interrupt Enabled bit and the Interrupt Enable bit of the specific interrupt.
The interrupt action and triggering must be enabled before sending an Interrupt Request to the Handler (ISR - Interrupt Service Routine). There are three registers associated with this: an interrupt enable register (associated with the interrupt itself), an interrupt flag, which is set whenever the interrupt event occurs, even if the interrupt is not enabled; and finally, a global interrupt enable. All three have to be activated in order to release an Interrupt Request.
In the case of AVR microcontrollers, the Global Interrupt Enable bit is called the I-bit, and is in the 7th position Status I/O Register (called SREG for short).
What happens when the interrupt is triggered
If all the conditions above are fullfiled, once the interrupt signal is triggered, the microcontroller follows the following sequence:
- Finishes the execution of the current instruction, saves what it was up to on the stack and clear up the I bit (no interrupt allowed, although this is not entirely true)
- The code that is associated with the interrupt is then loaded, executed until a RETI instruction is found (Return from Interrupt)
- The address from the stack stored before is reloaded and the bit I is re-enabled
- The execution of the program continues
More details from the datasheet
The interrupt handling is a very complex matter. I do not pretend to make a tutorial or guide out of this, but I will do my best to explain here what I believe is best to know for someone that is interested about this topic.
When an interrupt occurs, we have said that we turn off the I bit in order to prevent other interrupts to happen. This can be also modified by software and re-enable it during the current interrupt execution in case, for example, we need to allow for nested interrupts i.e: interrupts that occur when an interrupt is running. This is non trivial, but basically, we should keep in mind that the code will return to the position of the PC (program counter) that is found more recently in the stack, and this will occur when RETI is executed. Also, we have to remember that everytime that RETI is executed, the I bit is automatically set.
Types of interrupts
The datasheet explains two types of interrupts: the ones that are triggered by an interrupt flag, and the ones that don’t need it, and are triggered as long as the interrupt condition is present.
For the first type, the routine is fairly obvious: something happens, we stop what we are doing, execute the interrupt sequence and come back. As well, during these types of interrupts, in case of another interrupt being triggered but not enabled by the I bit, the interrupt flag is set and remembered until we are done with the current execution and the I bit is set back.
One can enable and disable interrupts with CLI and SEI instructions. These are hard limits for the interrupt execution and will not allow anything to be triggered during the time beteween CLI and SEI.
The interrupts are handled in the AVR microcontroller by the so called interrupt vectors. These are ordered by priority in the following table:
These can be grouped into external and internal interrupts. External are the ones that come from a PIN change, and are reflected into the PCINT0 and PCINT1 vectors. Internal are the ones like a Watchdog, timer/counters or analog comparators and some of them are explained briefly below (I find them particularly interesting):
Watchdog: it is a way to interrupt and reset program execution in order to prevent a code from getting caught on a loop. It will set a counter (that should be reset every loop iteration) and trigger an action once it reaches the defined timer. This counter is independent to any other clock source to the microcontroller and that makes it capable of taking care of the normal operation, without allowing the program to halt
Timer/counter: in the case of the ATtiny, it’s an 8-bit timer/counter that allows usage of the CPU clock to basicly, count, and raise an interrupt whenever that’s achieved. Since these numbers can be very high at normal execution speeds, the section counts on the so called prescaler, which is nothing else than a number that skips ticks, by factors of 8, 64, 256, 1024…
Analog comparator interrupt: it’s an interrupt triggered when the conditions in the analog comparator are attained. But what is an analog comparator? Well, put simply, it is a device that compares two analog signals and outputs the largest, or a flag. This can be set to compared to analog signals read in the input pins and to trigger an interrupt when it happens.
In the case of the external interrupts, we have to use the interrupt vectors provided by the manufacturer. That is, each pin is associated to a group of pins to be monitored and whenever something changes in one of them, the ISR will be triggered (of course, if enabled). In the case of the ATtiny, this is translated into two vectors:
- PCINT0 vector: monitors INT0 pin or any of the PCINT11:0 pins
- PCINT1 vector: monitors any of the PCINT11:8 pins
This is pretty confusing, starting from the naming. The PCINTX can be either interrupt vectors, or PINs. The overlapping is just a convention to make the pin layout easier, but it is confusing indeed.
Now, since we cannot know which pins are contributing to the interrupt, we have to check the PCMSK0 and PCMSK1 registers, which monitor which PINS have triggered the corresponding interrupt vector.
Finally, there is a special PIN: the INT0. This pin is monitored by the PCINT0 vector and can be set as falling, rising edge or low level to be triggered. All this information is set in the MCU control register (or MCUCR, because we like acronyms a lot).
I think that’s it for my learning about Interrupt! Maybe more coding in the machine week!
Programming the board
Using Arduino Libraries
Below the code in the Arduino IDE or PlatformIO, the code is pretty neat, reading the BUTTON in PIN 2 and lighting up the LED in PIN 7:
Using C and the ATtiny44 registers
In this section, I will try to derive the same functionality in the code above, step by step, by using no Arduino Libraries and the ATtiny I/O registers.
Firstly, we won’t be using the Arduino.h, therefore the setup() and loop() functions won’t be there to support. This would be what the Arduino Library does for us in this case:
Secondly, we need to include the following libraries:
These will handle the communication and the needed registers for IO, as well the functionality to stop the delay. These are included into “<>” because they are located in:
Now, we should define were the LED is, in this case the PA7. This can be done with a Macro with #define and it’s either done via binary definition:
Or using a bitwise operation with left bit displacement of a 1:
Also, we should define where the LED is, in this case PORTA, being it’s I/O register DDRA:
Next, in the setup() area, we should define the LED pin as input, by changing the DDR register (as specified in the Datasheet, it needs to be set to 1 in the desired PIN). We have to first initialize the PIN value:
As for now, we have set the LED pin as an output, and it should not be lighting up. If we go for the button, we have to read the PINA data (the status of the PINs in PORTA defined as output, Sect 10.1.4 in the datasheet) and see if the one we want is high. First then, we define the button PIN:
And now, we can check the status of the button and light up the PIN accordingly, checking if the PINA contains a 1 in the PA2 address:
Finally, to make things more clear, we can use Macros as such:
Being the final code:
Now, we can program the board via either one of the following:
Arduino IDE: Sketch -> Upload
➜ pio run ... =============================== [SUCCESS] Took 1.95 seconds =============================== ➜ Platformio pio run -t program ...
PROJECT=led_button SOURCES=$(PROJECT).c MMCU=attiny44 F_CPU = 20000000 CFLAGS=-mmcu=$(MMCU) -Wall -Os -DF_CPU=$(F_CPU) $(PROJECT).hex: $(PROJECT).out avr-objcopy -O ihex $(PROJECT).out $(PROJECT).c.hex;\ avr-size --mcu=$(MMCU) --format=avr $(PROJECT).out $(PROJECT).out: $(SOURCES) avr-gcc $(CFLAGS) -I./ -o $(PROJECT).out $(SOURCES) program-avrisp2: $(PROJECT).hex avrdude -p t44 -P usb -c avrisp2 -U flash:w:$(PROJECT).c.hex program-avrisp2-fuses: $(PROJECT).hex avrdude -p t44 -P usb -c avrisp2 -U lfuse:w:0x5E:m
And as seen in the previous week… it works!: