It’s occurred to me that I somewhat jumped the gun when I wrote a blog post a while back about interrupts on AVR ATMEGA microcontrollers. Maybe it would be useful to take a step back.
This series is part of my learning process and I hope it will help others who, like me, are embarking on projects such as programming AVR chips. The way I learn things is to write about them as I go. Feel free to correct my mistakes. FYI, I program for AVR ATMEGAs using AtmelStudio 7 on Windows 10. I use the Atmel-ICE programmer to get the code on to the chip. If your toolchain is different you may need to take that into account.
When you first start learning about coding for Arduino boards, most tutorials get you going by telling you how to set a GPIO pin high or low, usually to flash an LED. But when working with AVR microcontrollers there’s a couple of more basic concepts you need to grasp before you start messing with pins – registers and macros.
So much of what you do with an AVR is achieved via setting or reading register values. Essentially, these are locations in memory. Flip a bit or a whole byte in one of these locations and you control the behaviour of the chip. Conversely, reading what values have been set in these locations by other processes will tell you what’s going on.
Dealing with memory locations may sound scary, but the fact is you don’t have to. Atmel has created a whole library of standard include files that use macros (ie, defines) to set up easily remembered names. When you install Atmel Studio, for example, the standard libraries are created for you. They define a great many macros, and often there are alternate names for the same thing – you just use whichever suits you best.
As well as the memory locations of registers, many standard values are given names via these macros. These can seem redundant – I mean, the following defines in portpins.h might seem a bit over the top.
/* Port Input Pins (generic) */ #define PIN7 7 #define PIN6 6 #define PIN5 5 #define PIN4 4 #define PIN3 3 #define PIN2 2 #define PIN1 1 #define PIN0 0
Seems a tad pedantic, doesn’t it? But it’s not. The truth is your code becomes much more readable and understandable when you use these macros.
This isn’t the only way to refer to a specific pin. You might want your code to show that you’re referring to pin 5 on Port B. The Atmel macros have defined PB5 and PORTB5 for that purpose. When you look into the code you’ll find that these are also defined as the integer 5 just like PIN5. So the following lines of code are, in fact, identical:
PORTB = PORTB | (1<<5); PORTB = PORTB | (1<<PIN5); PORTB = PORTB | (1<<PB5); PORTB = PORTB | (1<<PORTB5);
That code sets pin 5 of Port B high, in case you’re wondering. More on that in a future AVR post.
There is a potential trap here, though. These names make the code comprehensible but also mask the values we’re dealing with. I said earlier that some macros are memory addresses – in fact, they’re pointers to memory addresses. The PORTB we used in the example above is one such case. This has little significance in the simple line of code in this case. But it becomes important if you start doing things like passing port names as parameters to functions. We’ll deal with that in a forthcoming AVR post too.
Often, individual bits within a register will have their own names. You’ve seen that in one sense in the names of the pins above. But let’s look at something a little more sophisticated.
For the I2C (Two-Wire) interface there’s a register called TWCR (two-wire interface control register). With the ATMEGA 328P, seven of this register’s eight bits are used to perform actions on the interface (the other bit is unused) and every one has its own name. For example, to read the I2C interface using acknowledge you need to set the ‘interrupt’ bit (bit 7, named TWINT, value 128), the ‘enable’ bit (bit 2, or TWEN, value 4) and the ‘enable acknowledge’ bit (bit6, aka TWEA, value 64).
Now you could do this by finding out the actual address of this register and writing the value 196 to it (128 + 64 + 4). But in six month’s time when you re-read the code, you’re going to be entirely baffled by that line. Instead, what you do is use:
TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA);
Not only do you not have to remember (or look up) the values for anything, your code will also be a lot more portable should you refactor it for a different Atmel microcontroller. And the code is explicit about which bits are being set.
So the bottom line here is to read the Atmel documentation for your microcontroller to learn the names of all the registers and the values you set in them. Using these macro names ultimately makes life easier and your code simpler to maintain.