2026 ADI CodeFusion 竞赛-基于MAX32655的多通道数据记录仪
该项目使用了MAX32655FTHR和两路土壤湿度传感以及两路温湿度传感器,实现了一个多通道数据记录仪的设计,它的主要功能为:多通道数据记录仪可以最高1KHz采样率对数据进行采样,可以在OLED屏幕上显示实时测量结果,并可以同时将实时测量结果以CSV格式写入SD卡中,可以随时拔出SD卡导出其中记录数据以供分析。
标签
嵌入式系统
ADC
ADI
Pulsar2
更新2026-09-29
3

2026 ADI CodeFusion 竞赛-基于MAX32655的多通道数据记录仪

本项目所选任务为赛道三的任务1,多通道数据记录仪

1、项目描述

本项目使用MAX32655FTHR开发板搭配土壤湿度传感器、温湿度传感器与OLED屏幕实现了一个多通道数据记录仪。该多通道数据记录仪可以最高1KHz采样率对数据进行采样,可以在OLED屏幕上显示实时测量结果,并可以同时将实时测量结果以CSV格式写入SD卡中,可以随时拔出SD卡导出其中记录数据以供分析。

2、硬件介绍

本项目使用到的硬件主要为核心板MAX32655FTHR,实物图如下图所示。MAX32655FTHR 是一款快速开发平台,可帮助工程师使用MAX32655 Arm© Cortex®-M4F和Bluetooth® 5.2低功耗(LE)快速实施超低功耗无线解决方案。该电路板还包括MAX20303 PMIC以实现电池和电源管理。0.9 x 2.6英寸小尺寸双排接头与Adafruit Feather Wing外设扩展板兼容。该电路板包括各种外设,如数字麦克风、低功耗立体声音频编解码器、128MB QSPI闪存、micro SD卡连接器、RGB指示器LED和按钮。该开发板的核心CPU MAX32655微控制器(MCU)是一款先进的片上系统(SoC),采用Arm® Cortex®-M4F CPU,可高效执行复杂的函数和算法计算,额定温度范围为-40°C至+105°C。该SoC将功率调节和管理功能与单电感多输出(SIMO)降压稳压器系统集成于一体。板载新一代蓝牙® 5.2低功耗(LE)无线电,支持远程(编码)和高吞吐量模式以及医疗身体区域网络(MBAN)。该器件提供具有512KB闪存和128KB SRAM的大型板载存储器,并在一个32KB SRAM存储区上提供可选的纠错编码(ECC)功能。该32KB存储区也可保留在BACKUP模式中。


3、方案框图与设计思路

3.1、方案框图

本项目的方案框图如下图所示,以MAX32655FTHR开发板为核心,使用片内ADC驱动土壤湿度传感器,使用gpio实现1wire总线驱动DH11温湿度传感器,使用SPI挂载SD卡并接入FatFs文件系统以记录传感器检测数据,使用IIC驱动OLED屏幕实时显示检测值。


3.2、设计思路

本项目主要设计了4个模块,分别是:土壤湿度监测模块,空气温湿度监测模块,UI界面模块和数据记录模块。

3.2.1、土壤湿度检测模块

该模块主要完成ADC配置,包括 ADC 通道、量程的选择,并且需要记录将传感器放置在空气中和水中的ADC转换值进行标定,之后才能将检测到的ADC值转换为土壤湿度百分比。

3.2.2、温湿度检测模块

该模块主要使用gpio实现1wire总线驱动DH11温湿度传感器,每隔2秒读取一次当前空气的温湿度,记录数据的时候将这次的数值作为与上次读取之间的测量值记录,温湿度本身也不是变化很快的物理量,所以也没有太大影响。

3.2.3、UI界面

该模块主要设计了两个界面,测量值实时显示界面与记录器状态界面,可以使用按键切换界面切换。测量值实时显示界面展示了两路土壤湿度传感器的实时值、两路温湿度传感器的实时值、系统运行时间和当前写入的记录文件名。记录器状态界面会显示当前文件系统大小和剩余大小、已记录的文件数量和大小等。

3.2.4、数据记录模块

该模块在挂载了SD卡和FatFs文件系统后,将测量到的传感器数据以CSV格式写入文件中,每次开机或者文件记录到指定大小后自动进行文件轮转操作,以保证不会覆盖已有的文件。

4、软件介绍

4.1、软件流程图

本项目的软件流程图如下图所示,系统启动后先进行初始化,包括定时器、GPIO、SPI、IIC、OLED、土壤湿度传感器、温湿度传感器等等,之后挂载文件系统,如果挂载成功则开启自动记录功能,挂载失败则不开启,之后进入死循环中,通过定时器中断每1ms进行一次ADC转换,并计算出相应的土壤湿度,每200ms刷新一次OLED以显示传感器的实时检测值,每2秒读取一次DH11获取当前的空气温湿度值。


4.2、调试软件说明

本项目调试软件时,有两个需要注意的地方,一个是需要实现将SD卡格式化为FAT32格式,不然开发板可能识别不到SD卡,如果在开发板上格式化的话一方面速度比较慢,另一方面则是在电脑上可能识别不出来。另一个需要注意的地方是土壤湿度传感器需要事先进行标定,也就是需要将土壤湿度传感器放置于空气中时的ADC转换值和置于水中时的ADC转换值记录下来作为湿度0%和100%。

4.3、关键代码说明

4.3.1、土壤湿度检测

土壤湿度传感器可以直接使用ADC进行检测,所以显示ADC的初始化,然后配置一个1ms的定时器以1kHz进行土壤湿度检测。

int soil_init(void)
{
int err;

err = MXC_ADC_Init();
if (err != E_NO_ERROR) {
return err;
}

/* Force LSB justified data. The register powers up MSB justified and
* MXC_ADC_RevA_GetData() then shifts the data field by 6 - the correct
* shift for this 10 bit converter, but a different interpretation of the
* raw value than the rest of this file uses. Pin the alignment down so
* the raw values always mean "0..1023, LSB justified". */
MXC_ADC_SetDataAlignment(0);

/* Select the full scale input range that covers the sensor output swing.
* MXC_ADC_Init() already picked a conversion clock (~6 MHz); the internal
* reference is used by default. */
MXC_ADC_SetExtScale(APP_ADC_SCALE);

/* Touch both channels once so the driver muxes the analog inputs and the
* first real conversion of the sampler is not delayed. */
(void)MXC_ADC_StartConversion(APP_SOIL1_CH);
(void)MXC_ADC_StartConversion(APP_SOIL2_CH);


// soil_adc_selftest();


return E_NO_ERROR;
}

int soil_read_raw(mxc_adc_chsel_t channel)
{
if (channel > MXC_ADC_CH_1) {
return E_BAD_PARAM;
}
return MXC_ADC_StartConversion(channel);
}

uint16_t soil_raw_to_permille(uint16_t raw)
{
const int32_t dry = (int32_t)APP_SOIL_RAW_DRY;
const int32_t wet = (int32_t)APP_SOIL_RAW_WET;
int32_t permille;

/* raw == dry -> 0 %, raw == wet -> 100 % (dry > wet for capacitive probes) */
permille = ((dry - (int32_t)raw) * 1000) / (dry - wet);

if (permille < 0) {
permille = 0;
} else if (permille > 1000) {
permille = 1000;
}

return (uint16_t)permille;
}

uint16_t soil_read_permille(mxc_adc_chsel_t channel)
{
int raw = soil_read_raw(channel);

if (raw < 0) {
return 0xFFFFu;
}

return soil_raw_to_permille((uint16_t)raw);
}

static void sampler_isr(void)
{
int soil1;
int soil2;

MXC_TMR_ClearFlags(APP_TMR_SAMPLER);

soil1 = soil_read_raw(APP_SOIL1_CH);
soil2 = soil_read_raw(APP_SOIL2_CH);

if (soil1 < 0 || soil2 < 0) {
s_missed++;
} else {
s_last[0] = (uint16_t)soil1;
s_last[1] = (uint16_t)soil2;
}

s_ticks++;
timebase_tick_ms();

if (s_callback != 0) {
s_callback(s_last[0], s_last[1]);
}
}

4.3.2、温湿度检测

温湿度传感器是1wire单总线的,这里是直接使用GPIO实现单总线,然后使用单总线驱动DH11,需要注意的是读取的时候需要关闭中断响应,这期间不能响应ADC检测的1ms中断响应,不过DH11的读取时间比较短,影响不大。

/* 50 us at 50 ticks/us -> 2500.  A '0' is ~27 us and a '1' ~70 us high, so
* the decision threshold is placed at ~44 us. */
#define DHT11_ONE_THRESHOLD_US 44u

/* Every phase of the protocol is at most ~90 us; 200 us is a generous limit. */
#define DHT11_PHASE_TIMEOUT_US 200u

/* Start signal low time (datasheet: at least 18 ms). */
#define DHT11_START_LOW_US 20000u

typedef struct {
mxc_gpio_regs_t *port;
uint32_t mask;
} dht11_pin_t;

static const dht11_pin_t s_pins[DHT11_SENSOR_COUNT] = {
{ APP_DHT1_PORT, APP_DHT1_PIN },
{ APP_DHT2_PORT, APP_DHT2_PIN },
};

static uint32_t s_errors[DHT11_SENSOR_COUNT];
static dht11_sample_t s_last[DHT11_SENSOR_COUNT];
static uint8_t s_last_valid[DHT11_SENSOR_COUNT];

/* -------------------------------------------------------------------------- */

static void dht11_pin_drive_low(const dht11_pin_t *pin)
{
mxc_gpio_cfg_t cfg = { .port = pin->port,
.mask = pin->mask,
.func = MXC_GPIO_FUNC_OUT,
.pad = MXC_GPIO_PAD_NONE,
.vssel = MXC_GPIO_VSSEL_VDDIO,
.drvstr = MXC_GPIO_DRVSTR_0 };

MXC_GPIO_Config(&cfg);
MXC_GPIO_OutClr(pin->port, pin->mask);
}

static void dht11_pin_release(const dht11_pin_t *pin)
{
mxc_gpio_cfg_t cfg = { .port = pin->port,
.mask = pin->mask,
.func = MXC_GPIO_FUNC_IN,
.pad = MXC_GPIO_PAD_WEAK_PULL_UP,
.vssel = MXC_GPIO_VSSEL_VDDIO,
.drvstr = MXC_GPIO_DRVSTR_0 };

MXC_GPIO_Config(&cfg);
}

static int dht11_pin_get(const dht11_pin_t *pin)
{
return MXC_GPIO_InGet(pin->port, pin->mask) != 0;
}

/** Wait until the line reaches @p level. Returns E_NO_ERROR or E_TIME_OUT. */
static int dht11_wait_level(const dht11_pin_t *pin, int level)
{
uint32_t start = timebase_ticks();

do {
if (dht11_pin_get(pin) == level) {
return E_NO_ERROR;
}
} while (timebase_elapsed_us(start) < DHT11_PHASE_TIMEOUT_US);

return E_TIME_OUT;
}

/* -------------------------------------------------------------------------- */

int dht11_init(void)
{
unsigned int i;

for (i = 0; i < DHT11_SENSOR_COUNT; i++) {
dht11_pin_release(&s_pins[i]);
s_errors[i] = 0;
s_last_valid[i] = 0;
s_last[i].humidity_p10 = 0;
s_last[i].temperature_c10 = 0;
}

return E_NO_ERROR;
}

int dht11_read(dht11_id_t id, dht11_sample_t *sample)
{
const dht11_pin_t *pin;
uint8_t data[5] = { 0, 0, 0, 0, 0 };
uint32_t ticks_per_us;
uint32_t threshold_ticks;
unsigned int i;
int bit;
int err = E_NO_ERROR;

if (id >= DHT11_SENSOR_COUNT || sample == 0) {
return E_BAD_PARAM;
}

pin = &s_pins[id];
ticks_per_us = timebase_ticks_per_us();
threshold_ticks = DHT11_ONE_THRESHOLD_US * ticks_per_us;

/* The bit timing has sub-microsecond requirements, so the sampling
* timer (and everything else) must not preempt the sequence. The logger
* compensates the resulting gap with held samples. */
__disable_irq();

/* 1. Start signal */
dht11_pin_drive_low(pin);
timebase_delay_us(DHT11_START_LOW_US);
dht11_pin_release(pin);

/* 2. Sensor acknowledgement */
if (dht11_wait_level(pin, 0) != E_NO_ERROR) {
err = E_TIME_OUT;
} else if (dht11_wait_level(pin, 1) != E_NO_ERROR) {
err = E_TIME_OUT;
} else {
/* 3. 40 data bits, MSB first */
for (i = 0; i < 40u; i++) {
uint32_t rise;
uint32_t width;

if (dht11_wait_level(pin, 0) != E_NO_ERROR) {
err = E_TIME_OUT;
break;
}
if (dht11_wait_level(pin, 1) != E_NO_ERROR) {
err = E_TIME_OUT;
break;
}

rise = timebase_ticks();
if (dht11_wait_level(pin, 0) != E_NO_ERROR) {
err = E_TIME_OUT;
break;
}
width = timebase_ticks() - rise;

bit = (width > threshold_ticks) ? 1 : 0;
data[i >> 3] = (uint8_t)((data[i >> 3] << 1) | (uint8_t)bit);
}
}

/* Leave the line released (idle high) for the next transaction. */
dht11_pin_release(pin);
__enable_irq();

if (err == E_NO_ERROR) {
uint8_t sum = (uint8_t)(data[0] + data[1] + data[2] + data[3]);

if (sum != data[4]) {
err = DHT11_E_CHECKSUM;
}
}

if (err == E_NO_ERROR) {
/* The DHT11 reports a zero decimal byte; copy it defensively in case
* a compatible part reports a real fraction. */
s_last[id].humidity_p10 = (uint16_t)((data[0] * 10u) + (data[1] <= 9u ? data[1] : 0u));
s_last[id].temperature_c10 = (int16_t)((data[2] * 10) + (data[3] <= 9 ? data[3] : 0));
s_last_valid[id] = 1;
*sample = s_last[id];
} else {
s_errors[id]++;
}

return err;
}

4.3.3、UI界面

主要设计了两个界面,测量值实时显示界面与记录器状态界面,可以使用按键切换界面切换。测量值实时显示界面展示了两路土壤湿度传感器的实时值、两路温湿度传感器的实时值、系统运行时间和当前写入的记录文件名。记录器状态界面会显示当前文件系统大小和剩余大小、已记录的文件数量和大小等。

static void ui_format_hhmmss(uint32_t ms, char *buf, unsigned int len)
{
uint32_t total_s = ms / 1000u;
unsigned int h = (unsigned int)(total_s / 3600u) % 100u;
unsigned int m = (unsigned int)((total_s / 60u) % 60u);
unsigned int s = (unsigned int)(total_s % 60u);

(void)snprintf(buf, len, "%02u:%02u:%02u", h, m, s);
}

static void ui_draw_header(const logger_status_t *st)
{
oled_text(0, UI_LINE_0, "DATA LOGGER", 1, 1);

if (st->recording) {
/* blinking REC indicator */
if (s_blink) {
oled_fill_rect(APP_OLED_WIDTH - 30, UI_LINE_0, 5, 5, 1);
}
oled_text(APP_OLED_WIDTH - 20, UI_LINE_0, "REC", 1, 1);
} else {
oled_text(APP_OLED_WIDTH - 24, UI_LINE_0, "IDLE", 1, 1);
}

oled_hline(0, UI_LINE_0 + 8, APP_OLED_WIDTH, 1);
}

static void ui_format_soil(uint16_t raw, char *buf, unsigned int len)
{
uint16_t permille = soil_raw_to_permille(raw);

(void)snprintf(buf, len, "%u.%u %%", (unsigned)(permille / 10u),
(unsigned)(permille % 10u));
}

static void ui_draw_measurements(void)
{
uint16_t soil1 = 0;
uint16_t soil2 = 0;
dht11_sample_t dht1;
dht11_sample_t dht2;
logger_status_t st;
char line[24];
char soil[16];

sampler_get_last(&soil1, &soil2);
logger_get_status(&st);

ui_draw_header(&st);

ui_format_soil(soil1, soil, sizeof(soil));
(void)snprintf(line, sizeof(line), "S1 %-8s", soil);
oled_text(0, UI_LINE_1, line, 1, 1);

ui_format_soil(soil2, soil, sizeof(soil));
(void)snprintf(line, sizeof(line), "S2 %-8s", soil);
oled_text(0, UI_LINE_2, line, 1, 1);

if (dht11_get_last(DHT11_SENSOR_1, &dht1)) {
(void)snprintf(line, sizeof(line), "T1 %d.%dC H1 %u%%",
(int)(dht1.temperature_c10 / 10), (int)abs(dht1.temperature_c10 % 10),
(unsigned)(dht1.humidity_p10 / 10u));
} else {
(void)snprintf(line, sizeof(line), "T1 --.-C H1 --%%");
}
oled_text(0, UI_LINE_3, line, 1, 1);

if (dht11_get_last(DHT11_SENSOR_2, &dht2)) {
(void)snprintf(line, sizeof(line), "T2 %d.%dC H2 %u%%",
(int)(dht2.temperature_c10 / 10), (int)abs(dht2.temperature_c10 % 10),
(unsigned)(dht2.humidity_p10 / 10u));
} else {
(void)snprintf(line, sizeof(line), "T2 --.-C H2 --%%");
}
oled_text(0, UI_LINE_4, line, 1, 1);

ui_format_hhmmss(st.elapsed_ms, line, sizeof(line));
oled_text(0, UI_LINE_5, line, 1, 1);
if (st.recording && st.file_name[0] != '\0') {
/* "/LOG0001.BIN" -> "0001.BIN" */
oled_text(64, UI_LINE_5, &st.file_name[4], 1, 1);
}
}

static void ui_draw_status(void)
{
logger_status_t st;
storage_log_stats_t logs;
char line[32];

logger_get_status(&st);

ui_draw_header(&st);

(void)snprintf(line, sizeof(line), "FS %u/%u kB", (unsigned)(storage_free_bytes() / 1024u),
(unsigned)(storage_total_bytes() / 1024u));
oled_text(0, UI_LINE_1, line, 1, 1);

if (storage_log_stats(&logs) == E_NO_ERROR) {
(void)snprintf(line, sizeof(line), "FILES %u %u kB", (unsigned)logs.count,
(unsigned)(logs.bytes / 1024u));
} else {
(void)snprintf(line, sizeof(line), "FILES n/a");
}
oled_text(0, UI_LINE_2, line, 1, 1);

(void)snprintf(line, sizeof(line), "SAVED %u", (unsigned)st.records_written);
oled_text(0, UI_LINE_3, line, 1, 1);

(void)snprintf(line, sizeof(line), "DROP %u HELD %u", (unsigned)st.records_dropped,
(unsigned)st.gaps_filled);
oled_text(0, UI_LINE_4, line, 1, 1);

(void)snprintf(line, sizeof(line), "ETH %u %u", (unsigned)dht11_error_count(DHT11_SENSOR_1),
(unsigned)dht11_error_count(DHT11_SENSOR_2));
oled_text(0, UI_LINE_5, line, 1, 1);
}

/* -------------------------------------------------------------------------- */

int ui_init(void)
{
int err = oled_init();

if (err != E_NO_ERROR) {
printf("[ui] display not available (%d)\n", err);
} else {
printf("[ui] display ready, I2C bus at %u Hz\n", (unsigned)oled_bus_freq());
}

s_screen = UI_SCREEN_MEASUREMENTS;
s_last_refresh_ms = 0u - UI_REFRESH_MS; /* force a redraw on the next poll */
s_blink = 1;

if (oled_present()) {
ui_poll();
}

return err;
}

void ui_set_screen(ui_screen_t screen)
{
if (screen >= UI_SCREEN_COUNT) {
return;
}
if (screen != s_screen) {
s_screen = screen;
s_last_refresh_ms = 0u - UI_REFRESH_MS; /* force an immediate redraw */
}
}

ui_screen_t ui_get_screen(void)
{
return s_screen;
}

void ui_poll(void)
{
uint32_t now;

/* BUTTON1 cycles through the screens. */
if (bsp_button_pressed_event(0)) {
ui_set_screen((ui_screen_t)((s_screen + 1u) % (unsigned int)UI_SCREEN_COUNT));
}

if (!oled_present()) {
return;
}

now = timebase_ms();
if ((uint32_t)(now - s_last_refresh_ms) < UI_REFRESH_MS) {
return;
}
s_last_refresh_ms = now;
s_blink = !s_blink;

oled_clear();

switch (s_screen) {
case UI_SCREEN_STATUS:
ui_draw_status();
break;
case UI_SCREEN_MEASUREMENTS:
default:
ui_draw_measurements();
break;
}

oled_flush();
}

4.3.4、数据记录器

数据记录器在挂载了SD卡和FatFs文件系统后,将测量到的传感器数据以CSV格式写入文件中,每次开机或者文件记录到指定大小后自动进行文件轮转操作,以保证不会覆盖已有的文件。

static uint32_t logger_index_to_ms(uint32_t index)
{
#if (APP_SAMPLE_RATE_HZ % 1000u) == 0u
return index / (APP_SAMPLE_RATE_HZ / 1000u);
#else
return (uint32_t)(((uint64_t)index * 1000u) / APP_SAMPLE_RATE_HZ);
#endif
}

static void logger_store(uint32_t index, uint16_t soil1, uint16_t soil2)
{
uint32_t head = s_head;
log_record_t *rec;

if ((head - s_tail) >= APP_LOG_RING_RECORDS) {
s_status.records_dropped++;
return;
}

rec = &s_ring[head & LOG_RING_MASK];
rec->t_ms = logger_index_to_ms(index);
rec->soil1_raw = soil1;
rec->soil2_raw = soil2;
rec->temp1_c10 = s_temp[0];
rec->hum1_p10 = s_hum[0];
rec->temp2_c10 = s_temp[1];
rec->hum2_p10 = s_hum[1];

s_head = head + 1u;
}

static void logger_push_held(uint32_t count)
{
uint16_t soil1 = 0;
uint16_t soil2 = 0;

if (count == 0u) {
return;
}

/* Hold the most recent values across the gap. */
if ((s_head - s_tail) != 0u) {
const log_record_t *last = &s_ring[(s_head - 1u) & LOG_RING_MASK];
soil1 = last->soil1_raw;
soil2 = last->soil2_raw;
}

while (count-- != 0u) {
logger_store(s_index, soil1, soil2);
s_index++;
s_status.gaps_filled++;
}
}

/* ==========================================================================
* CSV conversion
*
* The ring buffer holds compact 16 byte records, but what lands on the card is
* CSV text: that way there is no export step at all, the card can be pulled and
* opened in a spreadsheet. One record costs roughly 40 bytes of space.
* ========================================================================== */

#define LOG_CSV_LINE_MAX 64u /**< worst case characters of one CSV line */

/** Records converted in one go. Keeps the conversion buffer small (2 kB) while
* still writing several sectors per call. */
#define LOG_CSV_CHUNK_RECORDS 32u

static char s_csv[LOG_CSV_CHUNK_RECORDS * LOG_CSV_LINE_MAX];

static const char s_csv_header[] = "t_ms,soil1_raw,soil1_pct,soil2_raw,soil2_pct,"
"temp1_c,hum1_pct,temp2_c,hum2_pct\r\n";

/** "25.3" for a tenths value, or an empty field when the reading is invalid. */
static void log_fmt_tenths(int32_t value10, int is_signed, char *buf, unsigned int len)
{
if (is_signed) {
if (value10 == (int32_t)LOG_TEMP_INVALID) {
buf[0] = '\0';
return;
}
(void)snprintf(buf, len, "%d.%d", (int)(value10 / 10), abs((int)(value10 % 10)));
} else {
if ((uint16_t)value10 == LOG_HUM_INVALID) {
buf[0] = '\0';
return;
}
(void)snprintf(buf, len, "%u.%u", (unsigned)(value10 / 10), (unsigned)(value10 % 10));
}
}

/** Format one record as a CSV line; returns the number of characters produced
* (without the terminating NUL). */
static unsigned int log_record_to_csv(const log_record_t *rec, char *buf, unsigned int len)
{
uint16_t p1 = soil_raw_to_permille(rec->soil1_raw);
uint16_t p2 = soil_raw_to_permille(rec->soil2_raw);
char t1[12];
char h1[12];
char t2[12];
char h2[12];
int n;

log_fmt_tenths((int32_t)rec->temp1_c10, 1, t1, sizeof(t1));
log_fmt_tenths((int32_t)rec->hum1_p10, 0, h1, sizeof(h1));
log_fmt_tenths((int32_t)rec->temp2_c10, 1, t2, sizeof(t2));
log_fmt_tenths((int32_t)rec->hum2_p10, 0, h2, sizeof(h2));

n = snprintf(buf, len, "%u,%u,%u.%u,%u,%u.%u,%s,%s,%s,%s\r\n", (unsigned)rec->t_ms,
(unsigned)rec->soil1_raw, (unsigned)(p1 / 10u), (unsigned)(p1 % 10u),
(unsigned)rec->soil2_raw, (unsigned)(p2 / 10u), (unsigned)(p2 % 10u), t1, h1, t2,
h2);

if (n < 0) {
return 0u;
}

return ((unsigned int)n < len) ? (unsigned int)n : (len - 1u);
}

/* ==========================================================================
* File handling
* ========================================================================== */

static int logger_open_next_file(void)
{
char name[32];
unsigned int attempt;
int err;

if (!storage_ready()) {
return E_BAD_STATE;
}

if (s_file_open) {
(void)storage_file_close(&s_file);
s_file_open = 0;
}

for (attempt = 0; attempt < 8u; attempt++) {
/* Keep a margin of free space, otherwise the file system cannot do its
* own bookkeeping any more. */
if (storage_is_full()) {
if (storage_delete_oldest_log() != E_NO_ERROR) {
printf("[log] card full and no old log to remove\n");
return E_FAIL;
}
continue;
}

s_file_index++;
if (s_file_index == 0u || s_file_index > APP_LOG_FILE_MAX) {
s_file_index = 1u;
}
storage_log_name(s_file_index, name, sizeof(name));

err = storage_file_open_append(&s_file, name);
if (err == E_NO_ERROR) {
s_file_open = 1;
s_file_bytes = storage_file_size(&s_file);
strncpy(s_file_name, name, sizeof(s_file_name) - 1u);
s_file_name[sizeof(s_file_name) - 1u] = '\0';
s_status.files_created++;

/* A brand new file gets the column headings; an existing one that
* is being appended to already has them. */
if (s_file_bytes == 0u) {
if (storage_file_write(&s_file, s_csv_header, sizeof(s_csv_header) - 1u) ==
E_NO_ERROR) {
s_file_bytes = sizeof(s_csv_header) - 1u;
}
}

printf("[log] writing to %s\n", s_file_name);
return E_NO_ERROR;
}

if (err == E_NONE_AVAIL) {
if (storage_delete_oldest_log() != E_NO_ERROR) {
printf("[log] no space left on the card\n");
return E_FAIL;
}
continue;
}

printf("[log] cannot open %s (%d)\n", name, err);
return E_FAIL;
}

return E_FAIL;
}

static void logger_close_file(void)
{
if (s_file_open) {
(void)storage_file_close(&s_file);
}
s_file_open = 0;
}

/** Move @p count records from the ring buffer into the current log file. */
static int logger_flush(uint32_t count)
{
uint32_t now = timebase_ms();

if (count == 0u || !storage_ready()) {
return E_NO_ERROR;
}

if (!s_file_open) {
if (logger_open_next_file() != E_NO_ERROR) {
return E_FAIL;
}
}

while (count != 0u) {
uint32_t slot = s_tail & LOG_RING_MASK;
uint32_t chunk = APP_LOG_RING_RECORDS - slot;
uint32_t text_len = 0;
uint32_t i;

if (chunk > count) {
chunk = count;
}
if (chunk > LOG_CSV_CHUNK_RECORDS) {
chunk = LOG_CSV_CHUNK_RECORDS;
}

for (i = 0; i < chunk; i++) {
text_len += log_record_to_csv(&s_ring[slot + i], &s_csv[text_len],
sizeof(s_csv) - text_len);
}

/* Rotate before the file grows beyond the configured size. */
if ((s_file_bytes > 0u) &&
((s_file_bytes + text_len) > (uint32_t)APP_LOG_FILE_ROTATE_BYTES)) {
if (logger_open_next_file() != E_NO_ERROR) {
return E_FAIL;
}
}

if (storage_file_write(&s_file, s_csv, text_len) != E_NO_ERROR) {
printf("[log] write failed\n");
return E_FAIL;
}

s_file_bytes += text_len;
s_status.records_written += chunk;
s_tail += chunk;
count -= chunk;
}

/* Bound the amount of data that a sudden reset can lose. */
if ((uint32_t)(now - s_last_sync_ms) >= LOG_SYNC_PERIOD_MS) {
(void)storage_file_sync(&s_file);
s_last_sync_ms = now;
}

return E_NO_ERROR;
}

/* ==========================================================================
* Public API
* ========================================================================== */

int logger_init(void)
{
memset(&s_status, 0, sizeof(s_status));

s_head = 0;
s_tail = 0;
s_index = 0;
s_recording = 0;
s_file_open = 0;
s_file_bytes = 0;
s_file_index = 0;
s_file_name[0] = '\0';

return E_NO_ERROR;
}

int logger_start(void)
{
if (!storage_ready()) {
printf("[log] no file system available\n");
return E_BAD_STATE;
}

if (s_recording) {
return E_NO_ERROR;
}

s_head = 0;
s_tail = 0;
s_index = 0;
s_file_open = 0;
s_file_bytes = 0;
s_status.records_written = 0;
s_status.records_dropped = 0;
s_status.gaps_filled = 0;
s_status.files_created = 0;
s_status.elapsed_ms = 0;

/* continue the numbering of the log files already on the volume */
s_file_index = storage_last_log_index();

if (logger_open_next_file() != E_NO_ERROR) {
return E_FAIL;
}

s_last_flush_ms = timebase_ms();
s_last_sync_ms = s_last_flush_ms;
s_recording = 1;
s_status.recording = 1;

bsp_led_red(1);
printf("[log] recording started\n");
return E_NO_ERROR;
}

int logger_stop(void)
{
if (!s_recording) {
return E_NO_ERROR;
}

s_recording = 0;
s_status.recording = 0;

/* Drain whatever is left in the ring buffer. */
(void)logger_flush(s_head - s_tail);
logger_close_file();

bsp_led_red(0);
printf("[log] recording stopped (%u records, %u dropped, %u held)\n",
(unsigned)s_status.records_written, (unsigned)s_status.records_dropped,
(unsigned)s_status.gaps_filled);

return E_NO_ERROR;
}

void logger_isr_on_sample(uint16_t soil1, uint16_t soil2)
{
if (!s_recording) {
return;
}

logger_store(s_index, soil1, soil2);
s_index++;
}

void logger_set_dht(dht11_id_t id, const dht11_sample_t *sample)
{
if (id >= DHT11_SENSOR_COUNT || sample == 0) {
return;
}

s_temp[id] = sample->temperature_c10;
s_hum[id] = sample->humidity_p10;
}

void logger_clear_dht(dht11_id_t id)
{
if (id >= DHT11_SENSOR_COUNT) {
return;
}

s_temp[id] = LOG_TEMP_INVALID;
s_hum[id] = LOG_HUM_INVALID;
}

void logger_gap_begin(void)
{
if (!s_recording) {
s_gap_active = 0;
return;
}

s_gap_ticks = timebase_ticks();
s_gap_index = s_index;
s_gap_active = 1;
}

void logger_gap_end(void)
{
uint32_t elapsed_us;
uint32_t expected;
uint32_t actual;

if (!s_gap_active) {
return;
}
s_gap_active = 0;

elapsed_us = timebase_elapsed_us(s_gap_ticks);
expected = (uint32_t)(((uint64_t)elapsed_us * APP_SAMPLE_RATE_HZ) / 1000000ull);
actual = s_index - s_gap_index;

if (expected > LOG_MAX_GAP_RECORDS) {
expected = LOG_MAX_GAP_RECORDS;
}

if (expected > actual) {
logger_push_held(expected - actual);
}
}

void logger_poll(void)
{
uint32_t now;
uint32_t pending;

if (!s_recording) {
return;
}

now = timebase_ms();
pending = s_head - s_tail;

if (pending == 0u) {
return;
}

if ((pending < APP_LOG_FLUSH_RECORDS) &&
((uint32_t)(now - s_last_flush_ms) < APP_LOG_FLUSH_TIMEOUT_MS)) {
return;
}

s_last_flush_ms = now;

if (logger_flush(pending) != E_NO_ERROR) {
printf("[log] storage error - recording stopped\n");
(void)logger_stop();
}
}

5、实物演示与说明

CodeFusion Studio™软件使用截图如下所示,CFS使用起来无论是编译还是下载都还是很方便的。



开发板实物运行图如下所示,可以看到图中所展示的界面是实时检测值界面,分别显示了两路土壤湿度和两路空气温湿度。另外本次项目没有制作转接底板,直接使用以前MAX78000FTHR项目活动的底板,都是同一类型开发板,接口是通用的。



运行一段时间后将SD卡拔出来用读卡器接电脑上,可以看到已经记录了几个文件了。



随机打开一个记录文件,可以看到其中的数据变化



将其中的数据直接使用excel的制图功能显示折线图,观察数据变化会更直观,可以看到其中有两个时间点的变化比较剧烈,那是因为我直接用手触摸了传感器,这样可以看到比较明显的数据变化


6、总结

6.1、遇到的难点与解决方法

本项目比较简单,我觉得唯一有难度的是CFS中图形化的配置页面使用,第一次使用的情况下,有许多按钮和选型不知道如何选择,不知道选择后会造成什么结果,而看SDK中的例子则只有代码,如何配置只能自己摸索,而且最后Generate Code生成的初始化代码soc_init都集合在PeripheralInit函数中了,而我们的代码中并没有显式的调用,实际上这个函数会在systemInit中调用,在进入main函数之前会先执行systemInit,所以只需要在图形化界面中配置好并生成代码,在进入main函数后我们的配置都是已经生效了。

6.2、心得体会

本次活动体验相对还是比较轻松的,CFS这个软件的使用体验还可以,在图形化配置界面这块我觉得加入一些引导会比较好,那种配置比较复杂的外设在选项框上加一些鼠标悬浮时的文本提示的话感觉会更好一些,可以详细说明下这个选项怎么用,在什么情况下可以保持默认,什么情况下必须该选之类的,毕竟如果直接把这些写在面板上会显得很杂乱,而加载鼠标悬浮提示上就可以让界面更清爽的同时也能提供更多信息。

附件下载
max32655.rar
完整可编译的CFS工程代码
团队介绍
个人团队
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号