I connected up the 12V input to my bench PSU and switched on. The power LED came on which meant that the 5V regulator was working. Of course there was no output from the NXA66 because the relay that controls power to it was switched off. What I did next was to check that the AtMega328P would talk to me through the ISP header. It did, so now I’m good to go with writing the firmware.
The basic aims of the firmware are:
When the device is powered up it should restore its most recent settings from the onboard EEPROM. If the output enable switch is in the ‘on’ position then the output should be immediately enabled. This enables the device to continue where it left off in the event of an unexpected reboot or power outage.
The upper LED should show the voltage level reported by the INA226. The lower LED should show the current reported by the INA226.
There are two switches and a rotary encoder with an integrated button. Their functionality is as follows:
Since the NXA66 does not like switching levels while the power is on we will use the relay to cut the power before setting the new level and switching the relay back on.
The rotary encoder and its integrated button will be used to navigate through a single-level menu of configuration options displayed on the upper display. Pressing the button will bring up the first menu item. Turning the knob will ‘scroll’ through the options. Pressing the button again will enter that menu option and then the knob can be used to adjust the configured value and the knob will confirm it and save the new value to EEPROM. Doing nothing for an idle time of 10 seconds will abort the menu process and go back to the main voltage/current display.
Rudimentary but intelligible letters can be displayed on the 4-digit LED display and that will be enough for me to provide the menu navigation. The configurable options will be:
It didn’t take long to write the firmware and I’m quite pleased with the result. It’s all open-source of course and you can view it here on github. In the bin
directory you can find hex files that correspond to each release. These can be uploaded directly to the AtMega328p using the USBASP programmer.
avr-size nxa66.elf | tee nxa66.siz
text data bss dec hex filename
7486 180 191 7857 1eb1 nxa66.elf
Looking at the compiled size it appears that I could have fitted it into an AtMega8 but there’s no price advantage for me to do that since they both cost about the same in single units. That’s one thing I really like about the AVR 8-bit instruction set – the code density is so high that you get a lot of functionality into a small amount of flash. This firmware would have been at least double the size if implemented in the ARM 16-bit thumb instruction set.
The MCU operates on its internal 8MHz oscillator. The fuse values required for that are programmed using avrdude:
$ avrdude -c usbasp -p m328p -e -U lfuse:w:0xe2:m -U hfuse:w:0xde:m
The main loop of the firmware polls the INA226 every 200ms and updates the display. The configuration menu, if active, also runs in the main polling loop. Everything else runs asynchronously using interrupts:
INT0
pin. When the pin changes state we get an interrupt and set the onboard LED accordingly. Click here to see the code for that.Programming using interrupts can greatly increase the efficiency of your firmware, indeed many of my STM32 firmware implementations are entirely interrupt driven. That is, the main loop does absolutely nothing at all. Programming using interrupts does require additional care and attention though. Some of the most important that spring to mind are:
The volatile
qualifier must be used on data that is to be shared with code executed in a different context. This will stop the compiler re-ordering instructions or caching writes that could mess things up. Don’t use volatile
unless you need it though because it restricts the optimiser’s attempts to make you look good.
You cannot read or write a variable in the main loop that is accessed in an interrupt context if that variable is wider than the MCU can write in an atomic instruction. For the AVR that means you cannot share an int
because it’s 16 bits wide and requires two instructions to write. An interrupt can occur between the two instructions and leave the variable in an incorrect state. To get around this on the AVR, either disable interrupts while a wide variable is accessed or use an 8-bit flag to indicate a ‘locked’ state.
Don’t spend ages in an interrupt service routine if other things could be held up by you. In an environment without hierarchical interrupts everything else is suspended and important interrupts such as those that keep time will not be running.
You will not be able to use functionality that depends on other interrupts being serviced. For example, polling a millisecond timer counter in an interrupt routine isn’t going to work because the code that updates the counter cannot run.