A client asked me to write a logging library for a project using the Arduino SDK. Since the library will be open-source, I‘m running an experiment. I will document the design, development, and feedback cycles on this project. I hope to expose readers to a real development process, feedback on a design, and how to adjust based on that feedback.
In the previous article, we added a new strategy for logging to an SD card for Teensyduino boards.
Today, we will address two requests from the client:
- Verify and implement support for the Arduino Mega platform
- Identify and log the “reset cause” during the boot process
Table of Contents:
Arduino Mega Support
Our client needs to use this library on both a Teensy 3.6 and an Arduino Mega with an SD card shield (the Wiznet W5500 Ethernet shield, to be exact). I’ve been primarily focused on the Teensy 3.6, so it’s time to break out my 10 year old Arduino Mega and get to work.
The first thing to do is to change the board settings in the Arduino IDE and try to compile the code. Unfortunately, it looks like I polluted the base class (ArduinoLogger.h
) with code that isn’t supported for AVR chips. The AVR compiler toolchain, at least within the Arduino IDE, doesn’t support most of the C++ standard library. So, our inclusion of the header-only std::forward
breaks the library for AVR chips.
This is another example of how frustrating of an experience creating “portable code” can be.
I think the best approach at the moment is to use the preprocessor and ifdef
statements. I don’t want to make two different base classes, since the majority of the code is the same. Since our base class is serving primarily as an abstraction for other classes, we can hide all of the ifdef
statements within this one file.
This approach, where we remove std::forward
calls for AVR platforms, may have performance/size impacts. I’ll need to study that further. Maybe there is a superior approach (please suggest one in the comments!). Or, perhaps we can just go straight to the log()
function, rather than adding another layer of indirection by calling the log level functions in the base class (e.g., critical()
, error()
).
Here’s the uncomfortable truth: my client is much more concerned about the Teensy 3.6 than the Arduino Mega overall, so I will take the simplest working approach for now. We can easily adjust the base class if performance on the Mega becomes a problem for the client.
We’ll trigger off the ARDUINO_ARCH_AVR
key definition for the preprocessor logic.
First, we need to conditionally include the utility
header:
#if !defined(ARDUINO_ARCH_AVR)
#include <utility>
#endif
In the logger base class, each of the log level function calls needs to exclude std::forward
for AVR chips:
template<typename... Args>
void critical(const char* fmt, const Args&... args)
{
#if defined(ARDUINO_ARCH_AVR)
log(log_level_e::critical, fmt, args...);
#else
log(log_level_e::critical, fmt, std::forward<const Args>(args)...);
#endif
}
We also need to make the same changes in the global instance enforcer class:
template<typename... Args>
inline static void critical(const char* fmt, const Args&... args)
{
#if defined(ARDUINO_ARCH_AVR)
inst().critical(fmt, args...);
#else
inst().critical(fmt, std::forward<const Args>(args)...);
#endif
}
With these changes, I can compile for the Arduino Mega and successfully run our AVR circular buffer example sketch.
Checkpoint
We’ve corrected a big problem with the previous version of our library. I want to stop here and tag the code with a new version number for release before I add additional features. Whenever I identify a problem in a released version of software, I like to correct the issues and make a release that includes only the fixes for that problem.
I created a tag to track this point in the project, so you can go look at the code as it is described in this section of the article.
git tag -m "Tagging 0.3.1 for re-enabling ATMEga2560 support." 0.3.1
git push --tags
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 199 bytes | 199.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To [github.com:embeddedartistry/arduino-logger.git](http://github.com:embeddedartistry/arduino-logger.git)
* [new tag] 0.3.1 -> 0.3.1
Now we can begin implementing the next new feature: logging the reset cause during the boot process.
Identifying Reset Cause
My client asked for a watchdog library recommendation in a separate exchange. Once watchdog resets were on the forefront of the client’s mind, they figured it would be useful to know whether a watchdog reset was triggered by the system. I agree wholeheartedly with the client, and I often log the reset cause on my own systems.
Most processors have a register or two that you can read to identify the cause of a boot. Each processor or processor family will report the information somewhat differently. Most will tell you at a minimum whether the chip experienced a power-on reset (normal boot), a hardware reset, a software reset, a brownout, or a watchdog reset.
As expected, both the Teensy and the Arduino Mega processor (ATMega2560) provide a reset cause register. Rather than incorporate a watchdog library into the logger, we’ll create custom logging strategies that read the registers and log the reboot cause. Two new strategies will be created, one for the Teensy and one for AVR chips.
Teensy
To start, we’ll copy the SDFileLogger
class into a new file, and we’ll rename it to TeensySDLogger
.
This logging strategy uses a begin()
function, which seems like the simplest candidate for logging the reset reason. There is the potential that the reset cause register has been read and cleared before begin()
is called. If that is the case, we can use a more sophisticated strategy involving reading the reason during the constructor (which should be called before setup()
), writing the reset cause to the internal buffer, and flushing the buffer to the log file created during begin()
.
We’ll create a new private member function called log_reset_reason()
and invoke that within begin()
.
void begin(SdFs& sd_inst)
{
fs_ = &sd_inst;
if (!file_.open(filename_, O_WRITE | O_CREAT))
{
errorHalt("Failed to open file");
}
// Clear current file contents
file_.truncate(0);
log_reset_reason(); // <----- New Addition
// Manually flush, since the file is open
if(counter)
{
if(counter != file_.write(buffer_, counter))
{
errorHalt("Failed to write to log file");
}
counter = 0;
}
file_.close();
}
This page told me how to read the reset cause on a Teensy. There are two registers and a variety of bits:
RCM_SRS1 & RCM_SRS1_SACKERR Stop Mode Acknowledge Error Reset
RCM_SRS1 & RCM_SRS1_MDM_AP MDM-AP Reset
RCM_SRS1 & RCM_SRS1_SW Software Reset
True when reboot with SCB_AIRCR = 0x05FA0004
RCM_SRS1 & RCM_SRS1_LOCKUP Core Lockup Event Reset
RCM_SRS0 & RCM_SRS0_POR Power-on Reset
True when power removed/reapplied
RCM_SRS0 & RCM_SRS0_PIN External Pin Reset
True when reboot due to software download
RCM_SRS0 & RCM_SRS0_WDOG Watchdog(COP) Reset
True when WDT reboots MCU
RCM_SRS0 & RCM_SRS0_LOC Loss of External Clock Reset
RCM_SRS0 & RCM_SRS0_LOL Loss of Lock in PLL Reset
RCM_SRS0 & RCM_SRS0_LVD Low-voltage Detect Reset
Also true when power removed/reapplied
These registers and bitmasks are defined in kinetis.h
. Looking through the header, I learn that RCM
is the Reset Control Module and that SRSx
are the System Reset Status registers. I also see that there are “sticky” SRS registers, perhaps these don’t get cleared on boot. If our attempts at reading the reset cause fail, then I’ll dig deeper into the “sticky” registers.
For now, I’ll proceed by reading SRS0
and SRS1
.
First, we must include the header:
#include <kinetis.h>
Next, we will implement log_reset_reason
. This function needs to read SRS0
and SRS1
and log all of the conditions which have a corresponding bit set. Since the log file is open during begin
, we’ll immediately flush the log statements to the log.
void log_reset_reason()
{
auto srs0 = RCM_SRS0;
auto srs1 = RCM_SRS1;
// Clear the values
RCM_SRS0 = 0;
RCM_SRS1 = 0;
if(srs0 & RCM_SRS0_LVD)
{
info("Low-voltage Detect Reset\n");
}
if(srs0 & RCM_SRS0_LOL)
{
info("Loss of Lock in PLL Reset\n");
}
if(srs0 & RCM_SRS0_LOC)
{
info("Loss of External Clock Reset\n");
}
if(srs0 & RCM_SRS0_WDOG)
{
info("Watchdog Reset\n");
}
if(srs0 & RCM_SRS0_PIN)
{
info("External Pin Reset\n");
}
if(srs0 & RCM_SRS0_POR)
{
info("Power-on Reset\n");
}
if(srs1 & RCM_SRS1_SACKERR)
{
info("Stop Mode Acknowledge Error Reset\n");
}
if(srs1 & RCM_SRS1_MDM_AP)
{
info("MDM-AP Reset\n");
}
if(srs1 & RCM_SRS1_SW)
{
info("Software Reset\n");
}
if(srs1 & RCM_SRS1_LOCKUP)
{
info("Core Lockup Event Reset\n");
}
}
Now, we need a new example sketch that tests this reset logic. To start, I’ll make a copy of the TeensySDLogger
example. We’ll rename the old TeensySDLogger
to SDFileLogger_Teensy
to reflect the actual strategy being tested in the sketch.
Before we touch the code, we should make these clarifications within the README
:
* [SDFileLogger_Teensy](examples/SDFileLogger_Teensy)
- Demonstrates the use of the SDFileLogger on a Teensy board using SDIO in FIFO mode
* [TeensySDLogger](examples/TeensySDLogger)
- Demonstrates the use of the TeensySDLogger on a Teensy board using SDIO in FIFO mode. This logger will detect the reboot reason and log that to the file when `begin()` is called.
Now, in the TeensySDLogger example we need to update the include statement:
#include "TeensySDLogger.h"
And the logger declaration:
static TeensySDLogger Log;
Since we’re using a standard interface, nothing else in the example needs to change. I program the board, let it run, check the log file, and see:
<I> [579 ms] External Pin Reset
Great! But we want to make sure it really works, so we’ll add in some code to test that a watchdog reset is correctly identified. To do that, we’ll use Adafruit’s Sleepydog library.
After a watchdog reset is triggered, this is added to the log:
<I> [325 ms] Watchdog Reset
I’ll call that working. Time to move on to AVR support.
AVR
I don’t currently have an SD card slot setup for the Arduino Mega, so I’ll test that platform by adding new support to the AVR circular buffer strategy.
I used this webpage to learn about reset reason capabilities. The ATMega2560 doesn’t provide as much insight as the Teensy’s processor, but it’s still good enough for our purposes.
We’ll read the MCUSR
(MCU status register) and check for power-on reset, external reset, brown-out reset, or watchdog reset.
First, we need the proper include:
#include <avr/wdt.h>
Our AVR circular buffer strategy does not use a begin()
function, so in this case we’ll add a new resetCause
API.
void resetCause()
{
auto reg = MCUSR;
if(reg & (1 << WDRF))
{
info("Watchdog reset\n");
}
if(reg & (1 << BORF))
{
info("Brown-out reset\n");
}
if(reg & (1 << EXTRF))
{
info("External reset\n");
}
if(reg & (1 << PORF))
{
info("Power-on reset\n");
}
}
Next, I’ll update the existing AVR circular buffer example so that it invokes this new API:
void setup() {
Serial.begin(115200);
while(!Serial) delay(10);
// wait for Arduino Serial Monitor (native USB boards)
// This call is optional. It will log the reboot reason (e.g. WDT reset, power-on reset)
PlatformLogger::inst().resetCause();
logdebug("This line is added to the log buffer from setup\n");
}
Testing the code, everything looks ok… except that a watchdog reset seems to lock up my board until I manually power cycle it. I don’t see any serial output either.
I have a really old ATMega2560, so it might be that I have an older bootloader which doesn’t play nicely with the watchdog. Something I will dig into another day, since I don’t have an AVR programmer on hand to flash the bootloader.
Checkpoint
Since we’ve implemented a new feature that can be released and tested, it’s time to make a new version.
I created a tag to track this point in the project, so you can go look at the code as it is described in this section of the article.
git tag -m "Tagging 0.3.2 with support for reset cause detection." 0.3.2
git push --tags
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 200 bytes | 200.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To [github.com:embeddedartistry/arduino-logger.git](http://github.com:embeddedartistry/arduino-logger.git)
* [new tag] 0.3.2 -> 0.3.2
Putting it All Together
While I originally named the Watchdog reset version 0.3.2, I realize now that I should have gone to 0.4.0 because I added new functionality and a new API. That’s a good point of feedback for me. It’s also important to note how much we can benefit from detached reflection on our own performance. By waiting for a short time before reviewing my own work, I identified something simple that I can improve. (Although, in this case, I ended up continuing with the 0.3.x train in the next article. I realized this point of feedback after I tagged those changes.).
In the next article, we’ll finally tackle the client’s original goal of supporting a logger which rotates between files on the SD card.
Further Reading
- Teensy 3.6 Dev Board
- Arduino Logging Library
- Building a Flexible Logging Library for Arduino, Part 1
- Building a Flexible Logging Library for Arduino, Part 2
- Building a Flexible Logging Library for Arduino, Part 3
- Building a Flexible Logging Library for Arduino, Part 5
- SleepyDog Library
- AVR Reset Sources
- Watchdog Timer for Teensy 3.1 and 3.2