Flux Chamber¶

Written by Grace Lo & Rachel Yan

This integrated chamber system on the RP2040 operates in 2 phases, the air exchange and the data logging phase, while leveraging low-power sleep cycles to reduce power consumption and watchdog timer to ensure autonomous recovery, making the system suitable for long-term field deployment. The air exchange phase is a series of motor movements to open and close the chamber with sleep cycles in between to allow for nature to do its work and passively cycle the air in the measurement device. The data logging phase collects environmental data from both the Sensirion SCD30 (CO₂, temperature, humidity) and the Figaro NGM2611-E13 (methane), and writes this data with timestamps to a .csv file stored on an SD card. Between logging sessions, the RP2040 again enters a sleep state to reduce power consumption.

All base motor, SD card and sensor functionality is handled by their respective libraries, documented on the Stepper Motor, CO₂ Sensor, Methane Sensor and SD Card pages. The intent of this document is to show how these modules are integrated and how the watchdog and sleep/wake logic works.

  • Pin Connections
  • Customizable Parameters
  • Watchdog Timer
  • Low-Power Sleep Mode
  • Motor Movement
  • Data Logging
  • Time Drift

Pin Connections¶

Breadboarding best practices should be followed. Use the shortest possible wires for maximum clock speed, which when using solderless protoboards is capped at ~20MHz due to pin capacitance. The pin connection for each components is already documented on their respective pages and the integrated version simply connects all the components together.

Pin Connections

Customizable Parameters¶

There is a CUSTOMIZABLE PARAMETERS section starting at line 274 that the user can change to accomodate their specific deployment environment:

  • filename: the name of the file to write to
  • year, month, day, hour, min, sec: the date and time for the data logging to start
  • total_sampling_ms: total time to log data each cycle
    • must be at least sampling_interval_ms
  • sampling_interval_ms: time interval between logging the sensor readings
  • flush_chamber_ms: time interval for chamber to stay open/closed to filter out the old air/develop new air, respectively
    • must be at least 2x the time to fully open/close the chamber
  • debug_mode: enables optional serial printing of logging/sleeping information during runtime for debugging purpose

Code¶

All code is in the CornellFluxChamber Github repository. The file flux_chamber.c contains all core logic for motor and sensor initialization, sleep management, and data logging.

Header Files¶

The first lines of code in the C source file include header files. Don't forget to link these in the CMakeLists.txt file!

The first set of header files are standard C headers and the Raspberry Pi Pico standard library for general use. pico/stdlib.h is what the SDK calls a "High-Level API." These high-level API's "provide higher level functionality that isn’t hardware related or provides a richer set of functionality above the basic hardware interfaces." The architecture of this SDK is described at length in the SDK manual. All libraries within the SDK are INTERFACE libraries. pico/stdlib.h in particular pulls in a number of lower-level hardware libraries, listed on page 397 of the C SDK guide, trimming down the number of included header files.

#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"

The remaining header files pull in additional API's for special functionality, the first of which is the FatFs library adapted for SD cards.

#include "sd_card.h"
#include "ff.h"

The next set of header files are for protothread utilities. There is a custom header file included in the provided git repository that defines the protothread functions.

#include "pico/multicore.h"
#include "pt_cornell_rp2040_v1_3.h"

Next are the libraries that are included to give access to API's associated with the Pico unique ID, datetime, and Real Time Clock on the RP2040. These libraries will be used when logging data to the file.

#include "pico/unique_id.h"
#include "hardware/rtc.h"

There are also libraries for both the CO₂ and the methane sensor. These libraries will be used to initialize and read data from the sensors.

// SCD30 co2 sensor
#include "scd30_i2c.h"
#include "sensirion_common.h"
#include "sensirion_i2c_hal.h"

// methane sensor
#include "hardware/adc.h"

For further optimizations, there are libraries dedicated to the watchdog timer and the sleep/wake process. The sleep library, in particular, is provided in Rasberry Pi's pico-extras repository.

#include "hardware/watchdog.h"
#include "pico/sleep.h"

main()¶

The main program starts with calling stdio_init_all(). This function initializes stdio to communicate through either UART or USB, depending on the configurations in the CMakeLists.txt file.

Then it launches core 1, which is the second core of the RP20240. The RP2040 chip has 2 cores that can run independently of each other, sharing peripherals and memory with each other. Core 0 is automatically launched in main, while core 1 needs to be explicitly launched with the calls:

multicore_reset_core1();
multicore_launch_core1(&core1_main);

Protothreads are extremely lightweight, stackless threads designed for memory-constrained systems; the library is written entirely as C macros by Adam Dunkels. Traditionally, protothreads are used to execute tasks in parallel on the RP2040. It is interesting to note that, although the Raspberry Pi Pico supports running protothreads on both cores, we found that the logging system seemed to function correctly only if we are running it on core 1. This behavior was observed consistently and likely relates to the fact that core 1 is being called manually and the initialization of the logging system needs this extra time to start properly.

Given that the system follows a sequential order, all steps (eg. motor movement, sensor reading and SD card writing) are contained in one protothread that is assigned to Core 1. Core 0 does not run any application logic and exists solely to start the multicore system and the scheduler.

The thread scheduler SCHED_ROUND_ROBIN ensures the single protothread on Core 1 runs for the duration of the application. If there are additional protothreads assigned to core 1, they will each be allocated equal CPU time in the order they are initialized.


Protothread¶

Initializations¶

The protothread begins with a section of CUSTOMIZABLE PARAMETERS intended to be adjusted based on the deployment environment. The next step is the hardware intializations and variable declarations involved with each corresponding subsystem, starting with the Real Time Clock.

// Initialize date & time
char datetime_buf[256];
char *datetime_str = &datetime_buf[0];
rtc_init();
datetime_t t = {
    .year = year,
    .month = month,
    .day = day,
    .hour = hour,
    .min = min,
    .sec = sec};
rtc_set_datetime(&t);

Next the SD card is initialized and mounted.

// Initialize SD card
if (!sd_init_driver())
{
    printf("ERROR: Could not initialize SD card\r\n");
    while (true);
}

// Mount drive
fr = f_mount(&fs, "0:", 1);
if (fr != FR_OK)
{
    printf("ERROR: Could not mount filesystem (%d)\r\n", fr);
    while (true);
}

Then the CO₂ sensor and its npn switch is intialized.

// Initialize KN3904 (npn) transistor for power
gpio_init(11);
gpio_set_dir(11, GPIO_OUT);
gpio_put(11, 1);        // turn on power to SCD30

// Initialize SCD30 Sensor
float co2 = 0.0;
float temp = 0.0;
float hum = 0.0;
int16_t error = NO_ERROR;
sensirion_i2c_hal_init();
init_driver(SCD30_I2C_ADDR_61);
// make sure the sensor is in a defined state 
// (soft reset does not stop periodic measurement)
scd30_stop_periodic_measurement();
scd30_soft_reset();
sensirion_i2c_hal_sleep_usec(2000000);
uint8_t major = 0;
uint8_t minor = 0;
error = scd30_read_firmware_version(&major, &minor);
if (error != NO_ERROR) {
    printf("Error executing read_firmware_version(): %i\n", error);
}
if (debug_mode) {
    printf("firmware version major: %u minor: %u\n", major, minor);
}
// The 0 parameter disables ambient pressure compensation (can be replaced 
// with actual pressure value in mBar if needed).
error = scd30_start_periodic_measurement(0);
if (error != NO_ERROR) {
    printf("Error executing start_periodic_measurement(): %i\n", error);
}

The ADC for the methane sensor is also initialized.

adc_init();
adc_gpio_init(26);      // Make sure GPIO is high-impedance, no pullups etc
adc_select_input(0);    // Select ADC input 0 (GPIO26)

Finally, the GPIO pins for the stepper motor and switches are initialized. In addition, the enable pin immediately cuts the current to disable the motor, only powering the motor when necessary.

// Initialize Motor Pins
uint32_t elapsed_time_ms = 0; 
uint32_t sleep_time_ms = 0;
gpio_init(18);              // enable pin
gpio_set_dir(18, GPIO_OUT);
gpio_init(17);              // step pin
gpio_set_dir(17, GPIO_OUT);
gpio_init(16);              // direction pin
gpio_set_dir(16, GPIO_OUT);
gpio_put(18, 1);            // disable motor when not in use

// Initialize Switch Pins
gpio_init(20);              // limit switch
gpio_set_dir(20, GPIO_IN);
gpio_pull_up(20);
gpio_init(21);              // float switch
gpio_set_dir(21, GPIO_IN);
gpio_pull_up(21);

Watchdog Timer¶

The hardware watchdog timer is used to autonomously recover from potential system hangs by restarting the processor. The CO₂ Sensor page already covers the recovery procedure specifically for the SCD30 sensor, however watchdog is used as a catch-all for problems that may arise during long-term deployment.

The watchdog is enabled using watchdog_enable(delay_ms, 1) and pauses on the line of code that hangs when using the Pico's debug mode. Due to hardware limitations, the maximum delay on the RP2040 is 8388 milliseconds and is set to 8000 in the code, meaning that the watchdog must be "fed" (using watchdog_update()) within 8 seconds to prevent a system reboot. This can severly impact power consumption especially if the system has to wake up every 8 seconds during a long deep sleep set for a couple of minutes.

During testing, we found that the air exchange phase did not present system hang errors and given that this phase often comes with long deep sleep periods to allow for passive air exchange, we chose to disable the watchdog to minimize the amount of wake-ups during long deep sleeps. Therefore, the watchdog is only enabled during the data logging phase, and is promptly disabled (using watchdog_disable()) before transitioning to the air exchange phase. On the other hand, the data logging phase requires the system watchdog due to potential system hangs when interfacing with the sensors. This phase has the potential for long deep sleep periods, dependent on the user-defined sampling interval. In this case, there is an altered deep sleep function that wakes up every 7000 ms to feed the watchdog.


Low-Power Sleep Mode¶

The low-power sleep mode reduces power consumption and supports long-duration field deployments. Between motor movement and data logging entries, the RP2040 enters a sleep state and wakes automatically using a hardware alarm timer. The deep sleep functionality is wrapped in a deep_sleep (or the altered watchdog_deep_sleep) function.

When deep_sleep is called, the processor waits for the FIFO to be drained for reliable output before entering sleep mode. Then the RP2040 switches to the external crystal oscillator (XOSC) to save power and sleeps for sleep_time_ms.

void deep_sleep(uint32_t sleep_time_ms, bool debug_mode) {
    if (debug_mode) {
        printf("Sleeping for %i min %.2f sec\n", (int)sleep_time_ms / 60000, 
            (sleep_time_ms % 60000) / 1000.0);
    }

    uart_default_tx_wait_blocking();
    sleep_run_from_xosc();
    awake = false;
    uart_default_tx_wait_blocking();
    if (sleep_goto_sleep_for(sleep_time_ms, &alarm_sleep_callback)) {}
    sleep_power_up();
}

To wake up the RP2040 from deep sleep, the alarm_sleep_callback function triggers when the sleep time expires. Then it returns to the deep_sleep function and powers up the system again by re-enabling the clocks and generators. Note that while in deep sleep, the power to the methane and CO₂ sensors remain on to keep the sensor elements heated.

static bool awake;
static void alarm_sleep_callback(uint alarm_id) {
    uart_default_tx_wait_blocking();
    awake = true;
    hardware_alarm_set_callback(alarm_id, NULL);
    hardware_alarm_unclaim(alarm_id);
}

Motor Movement¶

The air exchange phase is set by flush_chamber_ms, where half of the time is allocated to opening the chamber and the other half is allocated to closing the chamber, with periods of rest in between to allow for the air to cycle. An enable pin on the motor driver enables or disables the current entering the motor. While not in use, the current is always disabled to conserve power and prevent the motor from overheating.

The motor code is wrapped in the function move_motor that has since been augmented from the basic design on the Stepper Motor page to stop the motor based on a switch rather than a defined number of steps. This function now takes arguments for the direction (0 to open and 1 to close the chamber) and flush_chamber_ms and runs the motor until a switch has been triggered. When the chamber is opening, it checks the limit switch secured to the top of the structure and when the chamber is closing, it checks the float switch secured to the moving support of the structure. If there are any problems with the motor and it cannot reach a switch within half of flush_chamber_ms (usually due to insufficient power at the end of the battery's life cycle), the code will exit on its own and proceed with the next step of the sequence.


Data Logging¶

At the start of each logging cycle, the system creates or opens a .csv file on the SD card. If the file does not exist, a new one will be created. If the file already exists, new data will be appended to the file. The contents of the file are divided into two sections - header and data. The header includes the Pico ID so that each system can be identified uniquely when many flux chambers are deployed. It also defines each of the columns. At present, the columns are DATETIME,TEMP (°C),HUM (%RH),CO2 (ppm),CH4.

FIL f_dst;
pico_unique_board_id_t PICO_ID;

if (FR_OK != f_open(&f_dst, filename, FA_WRITE | FA_OPEN_APPEND)) {
    printf("Cannot create '%s'\r\n", filename);
}
else {
    UINT wr_count = 0;
    char *NEW_LINE = "\n";

    // write the header once
    if (first) {
        first = false; 

        // Write UTF-8 BOM (for excel to interpret special characters)
        BYTE bom[] = {0xEF, 0xBB, 0xBF};
        f_write(&f_dst, bom, sizeof(bom), &wr_count);

        // Write pico id
        pico_get_unique_board_id(&PICO_ID);
        // Buffer for hexadecimal string
        char id_str[PICO_UNIQUE_BOARD_ID_SIZE_BYTES * 2]; 
        for (int i = 0; i < PICO_UNIQUE_BOARD_ID_SIZE_BYTES; i++) {
            // Append each byte in hexadecimal format to the buffer
            sprintf(&id_str[i * 2], "%02X", PICO_ID.id[i]);
        }
        printf("ID is: %s \n", id_str);
        f_write(&f_dst, "Pico ID,", strlen("Pico ID,"), &wr_count);
        f_write(&f_dst, id_str, strlen(id_str), &wr_count);
        f_write(&f_dst, NEW_LINE, strlen(NEW_LINE), &wr_count);

        // Write header
        const char *DATA_HEADER = "DATETIME,TEMP (°C),HUM (%RH),CO2 (ppm),CH4\n";
        f_write(&f_dst, DATA_HEADER, strlen(DATA_HEADER), &wr_count);
    }
}

The data logging phase is set by total_sampling_ms, with sampling_interval_ms between each entry. Each log entry retrieves and writes the date/time, the data from the CO₂ and methane sensor, and creates a new line after the current log is completed.

The date/time column uses an Excel-compatible format. As such, it now opens and displays as #####, requiring the 1st column to be expanded to show the value.

An example of the terminal output (when debug mode is on) and the corresponding CSV file is provided below:

Serial Terminal CSV File


Time Drift¶

Over long deployments, the start time of each cycle can drift due to compounding effects of fractional time loss from motor time variance, additional sensor recovery time, blocking sensor reads, etc. So strict timing was maintained to ensure periodic flushing of the chamber air. For the motor portion of the code, the time taken to move the motor was measured and subtracted from flush_chamber_ms/2 to leave the remaining for low-power sleep.

if (elapsed_time_ms < flush_chamber_ms/2) {
    sleep_time_ms = flush_chamber_ms/2 - elapsed_time_ms;
    deep_sleep(sleep_time_ms, debug_mode);
}

For the data logging portion of the code, the elapsed time is measured before every measurement iteration to predict if the time will be up by the end of the next logging entry. If there is any remaining time left over that is not enough for a full entry, the system will simply sleep to ensure the logging period finishes after exactly total_sampling_ms.

while (((time_us_32() - start_us) / 1000) + sampling_interval_ms < total_sampling_ms) {
    // log data
}

if (((time_us_32() - start_us) / 1000) < total_sampling_ms) {
    deep_sleep(total_sampling_ms - ((time_us_32() - start_us) / 1000), debug_mode);
}