Logs in RAM
A good technique for tracking what happened through the execution of time-critical code is to maintain a simple log within RAM.
Depending on your system's capabilities, you can retrieve this by reading the RAM using a debugger or, less commonly, a command on your console interface.
There are several reasons to log to RAM, mostly relating to resource constraints, such as no physical connection to the outside or to minimize impact on timing.
No available output to stream to (no UART, console, or network link).
No non-volatile storage (flash, HDD).
Speed/timing restrictions
ISR code restrictions
simple executive, no multithreading.
You may journal events as they happen or produce a one-time dump on a crash (core dump).
Writing to RAM is faster than writing to a UART, so even if you have the ability for a console interface, you may want to conserve your bandwidth.
Writing to RAM in this way uses minimal resources, unlike using printf(), or even a putch(). You might simply log a one-byte code to the log buffer and increment the pointer, slowing down only to check on wrapping.
When logging to RAM you will want to be terse, catching the bare minimum information to do the job.
A RAM buffer log is typically implemented as a circular buffer, functioning as a FIFO (First In, First Out) system. This allows for extremely lightweight logging that will catch the last log entries before the system is stopped.
A linear buffer is great for catching a start-up sequence, but typically, you would be using a circular log buffer to catch the last few events. There are tricks to making things run smoother, like having the buffer split into an integer number of message slots and all messages be the same size. Adding a tick count to each message helps keep the sequence and when things happen relative to each other. There are considerations, such as ensuring that the buffer is mutex-protected or only written to from one thread.
Advantages:
Low memory usage
fast writes to RAM log
minimal invasiveness in code
Disadvantages:
terse content
limited details
shallow log
need a tool to access RAM to read out results
lost with power outage unless NVRAM is used
Fixed location in RAM
To find the log in RAM, you will want it to be in a predictable location.
Allocated on heap
If you allocate RAM at run time for a log, you need to find it to read it. With appropriate tags, this is possible. To find the tag, you would need to search all of your heap space. Reading RAM might be fast within a device, but you are reading this RAM via some other means, typically JTAG or SWD, which is noticeably slower.
Heap space is valuable; keep it for what you really need. You also may be working on a system so constrained that, either by physical limitation or coding standard, you do not have a heap. To a software engineer, that may sound strange. To a firmware engineer working on a microcontroller with a clock speed measured in KHz and RAM less than 1K, this approach makes a great deal of sense.
Static allocation in source code.
Static allocation gives more predictable results than heap allocation, especially for a log that will persist through the entire runtime.
To avoid heap issues or constraints, you can declare the log and allow static allocation.
We already touched on how precious heap space is in an embedded system.
Consider how you would log events before running your heap initialization code. When working within a framework or operating system, the availability of alloc
is almost a given. When working on a bare metal system, there is a time before alloc
is available.
You declare a data structure in your code. This example is in C, and shows file-local scope. This means the structure will always exist but is not globally visible.
#include "myRamLogger.h"
#define MAX_RECORD_COUNT 16
// Declare space for the log
static logRecord myRamLogRec[MAX_RECORD_COUNT];
Unfortunately, though these are outside your heap, you are still at the mercy of the build tools as to where this data will reside.
As you add static allocations in your code, the application's RAM memory map can get re-ordered, and structures can be re-located. To know where things were put, you must read the .map output from your compiler.
Thankfully, the named allocation myRamLog can be known to your debugger. Most debuggers can use the name of the structure to dump the memory.
A log in the middle of your application's RAM is at risk of corruption, especially in C. Even an off-by-one array index could potentially damage a log entry.
Declared section in linker
You can edit the linker script (often, but not always a *.ld file) to allocate memory statically. You need to create a new memory section to place the log.
An allocation beyond the application RAM keeps you at a fixed location across builds, allowing scripted tools to pull the log.
See your toolchain's manual for instructions on how to set up a new RAM data section in the linker script.
Editing the linker script is not something you should undertake without preparation. You must be familiar with the memory map of your target MCU. You need to determine the depth of your activity log and the size of your log records. Once you have calculated the required space, round this number up.
The new RAM section will typically be at either the top or bottom of RAM.
Wherever you place the section, it must be aligned properly. This alignment can vary, but boundaries of 1k often work well. You may find boundaries have to be aligned for chip bus architecture, such as 8, 16, 32, or 64-bit aligned. The data sheet and other reference material from your vendor will provide these details.
This method, I feel, is the most robust of the common implementations used in industry. It provides an easy-to-locate, fixed location external to normal code operation.
The complexities of linker scripts might make the alternate static allocation in source code an easier option. With contemporary debuggers, a file-local static variable should be accessible by name in any scope. There is great value in KISS (Keep It Simple, Stupid) principle.
Sample implementation
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Address must be reserved in linker script
#define LOG_ARRAY_ADDRESS 0x20000000
// Must be a power of 2 to use wrap mask
#define LOG_SIZE 32
#define LOG_WRAP_MASK (LOG_SIZE - 1)
#define LOG_STR_SIZE 80
struct log_entry {
unsigned int timestamp;
char message[LOG_STR_SIZE];
// You may add other fields here
};
struct log_array {
struct log_entry log[LOG_SIZE];
unsigned int next_write_index;
};
struct log_array *log_array_ptr = (struct log_array *)LOG_ARRAY_ADDRESS;
void init_log_array(void)
{
unsigned int i;
for (i = 0; i < LOG_SIZE; i++) {
log_array_ptr->log[i].timestamp = 0;
log_array_ptr->log[i].message[0] = '\0';
}
log_array_ptr->next_write_index = 0;
}
void log_message(char *message)
{
unsigned int index = log_array_ptr->next_write_index;
unsigned int time = get_time();
log_array_ptr->log[index].timestamp = time;
// Consider strncpy for safety or strlcpy for safety and speed.
strcpy(log_array_ptr->log[index].message, message);
index = (index + 1) & LOG_WRAP_MASK;
log_array_ptr->next_write_index = index;
}
Last updated