Building a Flexible Logging Library for Arduino, Part 4

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:

  1. Verify and implement support for the Arduino Mega platform
  2. Identify and log the “reset cause” during the boot process

Table of Contents:

  1. Arduino Mega Support
    1. Checkpoint
  2. Identifying Reset Cause
    1. Teensy
    2. AVR
    3. Checkpoint
  3. Putting it All Together
  4. Further Reading

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

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.