...
- Requires recompilation every time we want to look at something different - For embedded, this also means reflashing.
- Requires cleanup after the problem is found.
- Requires dump functions for anything more than an integer.
- Printf is huge - For embedded, adding printf support can eat up a lot of RAM and flash.
- Printf is slow - It takes a long time to actually format the string, then transmitting it through your debug output has an additional delay.
- This can actually block the main program for a noticeable amount of time.
- This will wreck havoc with any timing-sensitive operations such as race conditions.
- It requires a debug output - In Module 2: Hello World, we needed a UART-to-USB adapter to see the serial output.
- It can't handle crashes and relies on the debug output to be working.
Of course, like any tool, this has its place. If you're okay with the drawbacks, printf is great for convenient, human-readable continuous logging. We have a set of LOG
macros for human-readable print statements in our HAL, such as the one you used in Module 2: Hello World.
GPIO Debugging
A higher-speed variant of this technique on embedded platforms is toggling GPIOs at certain points and using an oscilloscope or logic analyzer to trace execution. I find this most useful for timing-sensitive debugging.
...
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
#include <stdint.h>
#include "soft_timer.h"
#include "gpio.h"
#include "interrupt.h"
#include "log.h"
#include "wait.h"
#define DEBUG_TEST_TIMEOUT_MS 50
typedef struct DebugTestStorage {
GpioAddress *leds;
size_t num_leds;
size_t current_led;
} DebugTestStorage;
static DebugTestStorage s_storage;
static void prv_init_leds(DebugTestStorage *storage) {
GpioAddress leds[] = {
{ .port = GPIO_PORT_C, .pin = 8 }, //
{ .port = GPIO_PORT_C, .pin = 9 }, //
{ .port = GPIO_PORT_C, .pin = 6 }, //
{ .port = GPIO_PORT_C, .pin = 7 }, //
};
GpioSettings led_settings = {
.direction = GPIO_DIR_OUT,
.state = GPIO_STATE_HIGH
};
storage->leds = leds;
storage->num_leds = SIZEOF_ARRAY(leds);
storage->current_led = 0;
for (size_t i = 0; i < SIZEOF_ARRAY(leds); i++) {
gpio_init_pin(&leds[i], &led_settings);
}
}
static void prv_timeout_cb(SoftTimerId timer_id, void *context) {
DebugTestStorage *storage = context;
// Toggle the current LED
GpioAddress *led = &storage->leds[storage->current_led];
LOG_DEBUG("Toggling LED P%d.%x\n", led->port, led->pin);
gpio_toggle_state(led);
// Next LED
storage->current_led = (storage->current_led + 1) % storage->num_leds;
soft_timer_start_millis(DEBUG_TEST_TIMEOUT_MS, prv_timeout_cb, &storage, NULL);
}
int main(void) {
interrupt_init();
soft_timer_init();
gpio_init();
prv_init_leds(&s_storage);
uint32_t buffer[20];
LOG_DEBUG("%ld LEDs set up\n", s_storage.num_leds);
soft_timer_start_millis(DEBUG_TEST_TIMEOUT_MS, prv_timeout_cb, &s_storage, NULL);
size_t i = 0;
while (true) {
wait();
buffer[i % SIZEOF_ARRAY(buffer)] += i;
i++;
}
return 0;
}
|
...
Woah, what happened? It seems like we hit a segfault, but GDB was able to catch it. Let's take a closer look at what caused that.
Info |
---|
It's also possible that you won't get a segfault, but it should produce the wrong output! |
First, we use backtrace to print the state of the stack. We can see that the segfault is occuring in prv_timeout_cb
, which is the timer callback we registered. We can use up
and down
to move between function scopes in the stack. In our case, we move up to prv_timeout_cb
. Since it's a segfault, we're probably attempting to access invalid memory. Looking at led
and storage
, we see that they're completely invalid. Since led
is derived from storage
, we should look at where storage
is defined.
...