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, 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 switch out 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 sleep/wake logic works.
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.

Customizable Parameters¶
There is a CUSTOMIZABLE PARAMETERS
section starting at line 169 that the user can change to accomodate their specific deployment environment:
filename
: the name of the file to write toyear, month, day, hour, min, sec
: the date and time for the data logging to startlogging_period_ms
: time interval between logging the sensor readingsnum_of_measurements
: number of sensor readings per logging sessionsleep_time_ms
: time interval for chamber to stay open/closed to filter out the old air/develop new air, respectivelydebug_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.
Includes¶
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 196 of the C SDK guide.
#include <stdlib.h>
#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 "hardware/sync.h"
#include "hardware/timer.h"
#include "hardware/uart.h"
#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 "pico/util/datetime.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/gpio.h"
#include "hardware/adc.h"
Lastly, there is a library dedicated to the sleep/wake process. It is provided in Rasberry Pi's pico-extras repository.
#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 variable declarations for the sensor values (co2
, temp
,hum
) and date/time (datetime_buf
, datetime_str
).
The next step is the hardware intializations, starting with the Real Time Clock.
// Initialize date & time
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 is intialized and begins periodic measurement.
// Initialize SCD30 Sensor
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);
return error;
}
printf("firmware version major: %u minor: %u\n", major, minor);
//begins periodic measurement
error = scd30_start_periodic_measurement(0);
if (error != NO_ERROR) {
printf("error executing start_periodic_measurement(): %i\n", error);
return 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 are initialized. In addition, it immediately disables the motor using the enable pin to cut the current.
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
Low-Power Sleep Mode¶
To reduce power consumption and support long-duration field deployments, the system uses the RP2040's low-power sleep mode pico/sleep.h
. Between data logging sessions, the RP2040 enters a sleep state and wakes automatically using a hardware alarm timer.
At the beginning of each loop cycle, the code waits for the fifo to be drained for reliable output before entering sleep mode. Then the RP2040 switches to external crystal oscillator (XOSC) to save power, and sleeps for sleep_time_ms
amount of time defined in custom parameters.
uart_default_tx_wait_blocking();
sleep_run_from_xosc();
awake = false;
if (debug_mode) {
printf("Sleeping for %d seconds\n", sleep_time_ms/1000);
}
uart_default_tx_wait_blocking();
if (sleep_goto_sleep_for(sleep_time_ms, &alarm_sleep_callback)) {
// Empty for power efficiency
}
To wake up the RP2040 from sleep, we first defined a callback function, which triggers when the sleep time expires, and it powers up again by re-enablling the clocks and generators.
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);
}
// Re-enabling clock sources and generators.
sleep_power_up();
Motor Movement¶
When first deployed, the system begins by opening the chamber for the first round of air exchange. It stays open for sleep_time_ms
to air out the chamber, then closes and waits another sleep_time_ms
before moving into the data logging phase. 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 takes in arguments for the direction and number of steps the motor should take, on the order of thousands. Details on how move_motor
works is described on the Stepper Motor page. Here, we simply call the function and input the correct direction and number of steps, the current number of steps is 32000, which is approximately 1 minute long.
move_motor(1, 32000); // ccw for 32000 steps
... // sleep time
move_motor(0, 32000); // cw for 32000 steps
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, HUM, CO2, 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);
}
}
There will be num_of_measurements
of data each logging cycle, and it sleeps every logging_period_ms
in 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 has been updated to produce an Excel-compatible format. As such, it now opens and displays as #####, requiring the 1st column to be expanded to show the text.
An example of the terminal output (when debugging mode is on) and the corresponding CSV file is provided below:

