2026 ADI CodeFusion 竞赛 - 基于MAX32650实现空气质量监测仪
该项目使用了MAX32650,实现了空气质量监测仪的设计,它的主要功能为:用 AHT20 和 ENS160 测温度、湿度、eCO₂、TVOC 和 AQI,在 OLED 上显示数值、趋势曲线和空气质量表情,并按 30 秒一行存到 microSD 卡。
标签
ADI CodeFusion Studio
MAX32650
ENS160
AHT20
学嵌入式的Momo
更新2026-09-29
53

项目介绍

本次参加2026 ADI CodeFusion Studio™设计竞赛的赛道六:自命题。


本项目使用 ADI CodeFusion Studio,在 MAX32650FTHR 开发板做成一台独立的空气质量监测仪:支持采集温度、湿度、eCO₂、TVOC 和 AQI,自带显示屏、按键和 microSD 存储,不需要连接电脑即可运行、查看数据、留存历史记录。

image.png

固件以 1 Hz 的节奏采集环境数据。ENS160 每秒读取 eCO₂(ppm)、TVOC(ppb)与空气质量指数 AQI(1–5);AHT20每秒读取温度(0.1 °C 分辨率)与相对湿度(%RH)。ENS160 需要环境补偿,因此每秒先把 AHT20 的实测温湿度写入器件、再取气体数据。


本地显示是一块 128×64 单色 OLED,每 5 秒重绘一次,提供两个页面。数值页依次显示 eCO₂、TVOC、温湿度与 AQI,右侧画一张圆形表情,跟随 AQI 从 1 的大笑变到 5 的大皱眉;历史曲线页把最近 512 个 eCO₂ 采样,画成 128 列柱状图,曲线完全由 RAM 中的环形缓冲绘制、不依赖存储卡,顶部标出窗口内的最小与最大值。


存储方面,在 microSD 卡上使用 FAT 文件系统的 /SD:/data.csv,每 30 秒追加一行数据,每行写完立刻 fs_sync(),掉电最多丢失正在写的那一行;启动时若发现文件末尾缺少换行符会先补一个,避免新行粘在旧行末尾。


硬件介绍

主控与开发板

项目

参数

开发板

MAX32650FTHR(Feather 外形)

主控

MAX32650,Cortex-M4F,最高 120 MHz

系统时钟

IPO 120 MHz,sysclk-prescaler = 1

Flash

3 MB @ 0x10000000(代码分区 m4_flash 3008 KB,另有 64 KB coredump 分区)

SRAM

逻辑上 1 MB @ 0x20000000(由多块 SRAM bank 组成,链接使用该分区)

按键

SW2 = P1.19,SW3 = P1.21(低有效,内部上拉)

存储

板载 microSD 卡座(SDHC 4 位总线 + P1.8 卡座电源开关)

外设连接

外设均使用 I²C连接:

地址

器件

用途

0x3c

SSD1306

128×64 单色 OLED

0x38

AHT20

温度 / 相对湿度

0x53

ENS160

eCO₂ / TVOC / AQI 空气质量

软件介绍与软件架构

ADI CodeFusion Studio

本次项目主要使用 ADI CodeFusion Studio 进行开发,在 CFS 中完成外设配置和代码生成。

CFS 配置时钟:

配置使用的外设,包括按键(GPIO)、I2C、SDHC、UART。Zephyr 目前不支持 MAX32650 的 SDHC 配置,后面需要自己添加支持。

层次

选型

SDK / 构建

ADI CodeFusion Studio 2.2.1,Zephyr 4.3.0

配置

Kconfig(prj.conf + 应用级 m4/Kconfig)+ Devicetree overlay

驱动

Zephyr 传感器/显示/文件系统子系统 + 2 个工程内驱动副本

中间件

FATFS(ELM Chan,卷名 SD)、SDMMC 磁盘子系统

运行时

Zephyr 内核(抢占式多线程、mutex、atomic、看门狗)

工程结构

max32650-air-quality/
├── m4/
│   ├── CMakeLists.txt          # 叠加 overlay、编译工程内驱动副本
│   ├── Kconfig                 # SDHC_MAX32_SDSC_FIX / DHT20_SKIP_RESET
│   ├── prj.conf                # CFS 生成,保持不变(追加应用开关)
│   ├── boards/
│   │   └── max32650fthr.overlay# CFS 生成:内存分区、引脚、时钟,保持原样
│   ├── overlays/
│   │   ├── ssd1306.overlay     # OLED + 使能 I2C1
│   │   ├── microsd.overlay     # SDHC 节点 + 卡座电源
│   │   ├── sensors.overlay     # ENS160 + AHT20
│   │   └── watchdog.overlay    # 使能 WDT0
│   ├── drivers/
│   │   ├── sdhc_max32.c        # 上游 SDHC 驱动副本(SDSC 寻址修复)
│   │   └── dht20.c             # 上游 DHT20 驱动副本(去掉复位序列)
│   └── src/main.c              # 应用:5 个线程,约 1260 行
├── tools/
│   ├── fw.ps1                  # 一键 build / flash / reset
│   └── verify.ps1              # 一次 OpenOCD + 一次 GDB 读取运行态
└── testdata/data.csv           # 固件 CSV 格式的样例日志(500 行),用于验证格式与上位机解析

线程架构(软件架构核心)

应用不使用中断处理按键,也不在主循环里串行做事,而是把职责切成 5 个线程,用「小结构 + 互斥锁 + 原子标志」共享数据。

线程

优先级

周期

主要工作

sensors

5

1 s

读 AHT20 → 写 ENS160 补偿 → 读 ENS160 → 入环形缓冲 → 发布 latest → 打印一行遥测

display

6

200 ms 轮询 / 5 s 重绘

处理熄屏与换页请求,绘制数值页或曲线页

sdcard

7

500 ms 轮询 / 30 s 记录

挂载卡、追加 CSV 行、响应清空日志请求

buttons

8

20 ms

轮询两个按键并消抖,产生原子请求标志

watchdog

9

1 s

监督心跳,判定卡死则停止喂狗,由硬件复位恢复

共享数据与同步方式:

数据

保护方式

写入者 → 读取者

latest(最新一次读数结构体)

reading_lock(mutex)

sensors → display / sdcard

chart_samples[]、chart_count、chart_head(512 项环形缓冲)

chart_lock(mutex)

sensors → display

三个请求标志

atomic(置位/清零)

buttons → display / sdcard

thread_progress[](心跳时间戳)

atomic

各线程 → watchdog

I²C 总线由 Zephyr 驱动内部的信号量串行化,三个器件(OLED / AHT20 / ENS160)不会并发访问同一条总线。


工程组织:overlay 叠加与驱动替换

CFS 只会进行基本的 Zephy device tree 配置,例如可以配置 I2C,但是 I2C 上的设备我们需要另外的 overlay 文件配置。这样可以避免 CFS 重新生成代码的适合把 device tree 配置覆盖。

boards/max32650fthr.overlay (CFS 生成,禁止手改)
        │  被 EXTRA_DTC_OVERLAY_FILE 叠加
        ▼
overlays/ssd1306.overlay ─► microsd.overlay ─► sensors.overlay ─► watchdog.overlay
        │
        ▼
   最终 zephyr.dts

m4/CMakeLists.txt 在 find_package(Zephyr) 之前设置 EXTRA_DTC_OVERLAY_FILE,因此这些 overlay 一定在 CFS 生成的 overlay 之后应用,配置优先级由我们自己掌握,同时 CFS 任何时候重新生成它的文件都不会影响我们。

overlay 叠加:让 CFS 生成的文件保持原样

m4/CMakeLists.txt —— 必须在 find_package(Zephyr) 之前设置,才能保证这 4 个 overlay 应用在 CFS 生成的 boards/<board>.overlay 之后:

# Applied after the CFS-generated boards/<board>.overlay, so they can override
# it. Must be set before find_package(Zephyr), which runs the devicetree
# processing.
set(EXTRA_DTC_OVERLAY_FILE
    ${CMAKE_CURRENT_SOURCE_DIR}/overlays/ssd1306.overlay
    ${CMAKE_CURRENT_SOURCE_DIR}/overlays/microsd.overlay
    ${CMAKE_CURRENT_SOURCE_DIR}/overlays/sensors.overlay
    ${CMAKE_CURRENT_SOURCE_DIR}/overlays/watchdog.overlay)


find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

顺带把 HAL 缺的 SDHC 源文件补进来(该 HAL 未为 MAX32650 构建 SDHC):

# The ADI HAL does not build its SDHC support for MAX32650 (Source/MAX32650/
# CMakeLists.txt omits Source/SDHC), but the Zephyr sdhc_max32 driver needs it.
set(SDHC_HAL_DIR ${ZEPHYR_BASE}/../modules/hal/adi/MAX/Libraries/PeriphDrivers/Source/SDHC)
zephyr_include_directories(${SDHC_HAL_DIR})
zephyr_library_sources(
    ${SDHC_HAL_DIR}/sdhc_me10.c
    ${SDHC_HAL_DIR}/sdhc_reva.c
)

m4/overlays/ssd1306.overlay —— 冷启动修复与面板参数:

&i2c1 {
    /* The CFS pin canvas assigns I2C1 but leaves it disabled. */
    status = "okay";


    ssd1306: ssd1306@3c {
        compatible = "solomon,ssd1306fb";
        reg = <0x3c>;
        width = <128>;
        height = <64>;
        multiplex-ratio = <63>;
        segment-remap;
        com-invdir;
        prechargep = <0x22>;
        ready-time-ms = <300>;
    };
};

m4/overlays/microsd.overlay —— 从寄存器地址开始定义 SDHC 节点,并把 6 根信号线设为 3.3 V 电平域:

soc {
    sdhc0: sdhc@400b6000 {
        compatible = "adi,max32-sdhc";
        reg = <0x400b6000 0x1000>;
        interrupts = <82 0>;
        clocks = <&gcr ADI_MAX32_CLOCK_BUS1 10>;
        power-delay-ms = <1500>;
        status = "okay";
        pinctrl-0 = <&sdhc_cmd_p1_0 &sdhc_dat2_p1_1 &sdhc_dat3_p1_3
                 &sdhc_dat0_p1_4 &sdhc_clk_p1_5 &sdhc_dat1_p1_6>;
        pinctrl-names = "default";


        mmc {
            compatible = "zephyr,sdmmc-disk";
            disk-name = "SD";
            status = "okay";
        };
    };
};


&sdhc_cmd_p1_0 {
    power-source = <MAX32_VSEL_VDDIOH>;
    drive-strength = <1>;
};
/* dat2 / dat3 / dat0 / clk / dat1 五组引脚同样是 MAX32_VSEL_VDDIOH */

启动

上电/复位后,Zephyr 内核先建好内核对象、中断和时钟,然后按 SYS_INIT 优先级依次执行驱动与钩子。其中加了一个钩子,等等设备初始化:

SYS_INIT(wait_for_sensors_ready, POST_KERNEL, 80)
​
static int wait_for_sensors_ready(void)
{
const struct device *bus = DEVICE_DT_GET(DT_BUS(ENS160_NODE));
const struct device *aht20_dev = DEVICE_DT_GET_OR_NULL(AHT20_NODE);
struct sensor_health aht20 = {
.bus = bus, .addr = AHT20_ADDR, .status_reg = AHT20_STATUS_REGISTER,
.needs_calibration = true, .name = "AHT20",
};

if (aht20_dev == NULL) {
return 0;
}

if (wait_for_device(&aht20, SENSOR_READY_TIMEOUT_MS)) {
k_msleep(SENSOR_SETTLE_MS);
} else {
printf("AHT20 did not come up within %d ms\n", SENSOR_READY_TIMEOUT_MS);
}

return 0;
}


应用一共 5 个线程,全部由 main() 创建后即返回(main 线程退化为空闲线程)。优先级数字越小越高:sensors 5 → display 6 → sdcard 7 → buttons 8 → watchdog 9。

int main(void)
{
(void)display_bring_up();

k_thread_create(&sensor_tid, sensor_stack, K_THREAD_STACK_SIZEOF(sensor_stack),
(k_thread_entry_t)sensor_thread, NULL, NULL, NULL,
K_PRIO_PREEMPT(5), 0, K_NO_WAIT);
k_thread_name_set(&sensor_tid, "sensors");

k_thread_create(&display_tid, display_stack, K_THREAD_STACK_SIZEOF(display_stack),
(k_thread_entry_t)display_thread, NULL, NULL, NULL,
K_PRIO_PREEMPT(6), 0, K_NO_WAIT);
k_thread_name_set(&display_tid, "display");

k_thread_create(&sd_tid, sd_stack, K_THREAD_STACK_SIZEOF(sd_stack),
(k_thread_entry_t)sd_thread, NULL, NULL, NULL,
K_PRIO_PREEMPT(7), 0, K_NO_WAIT);
k_thread_name_set(&sd_tid, "sdcard");

k_thread_create(&button_tid, button_stack, K_THREAD_STACK_SIZEOF(button_stack),
(k_thread_entry_t)button_thread, NULL, NULL, NULL,
K_PRIO_PREEMPT(8), 0, K_NO_WAIT);
k_thread_name_set(&button_tid, "buttons");

k_thread_create(&watchdog_tid, watchdog_stack, K_THREAD_STACK_SIZEOF(watchdog_stack),
(k_thread_entry_t)watchdog_thread, NULL, NULL, NULL,
K_PRIO_PREEMPT(9), 0, K_NO_WAIT);
k_thread_name_set(&watchdog_tid, "watchdog");

return 0;
}

线程健康由心跳时间戳体现,每个线程在自己的每轮循环开头打一次点:

#define REPORT_PROGRESS(id) atomic_set(&thread_progress[id], (atomic_val_t)k_uptime_get())

看门狗消费这些心跳来判断系统是否阻塞。

线程间数据共享:一把锁发布、一把锁读取

写侧(sensor_thread):采样结果入环形缓冲后整体发布:

        if (r.air_ok) {
            /* The chart is drawn from RAM, so record every measurement. */
            chart_add(r.eco2);
        }


        k_mutex_lock(&reading_lock, K_FOREVER);
        latest = r;
        k_mutex_unlock(&reading_lock);

读侧(display_thread):只在自己要重绘时才取一份快照,不长时间占锁:

        if (on && k_uptime_get() >= next_render) {
            k_mutex_lock(&reading_lock, K_FOREVER);
            r = latest;
            k_mutex_unlock(&reading_lock);


            if (mode == MODE_CHART) {
                display_chart();
            } else {
                display_sensors(&r);
            }


            next_render = k_uptime_get() + DISPLAY_RENDER_MS;
        }

display 线程

职责:消费按钮线程发出的请求(熄屏、换页)、按 5 秒节流重绘 OLED;数值页画四行数据 + 空气质量表情,曲线页画 128 柱历史曲线。它只读 latest 与环形缓冲,不产生数据。两种请求都用「原子的置位并清零」来消费(atomic_set(&flag, 0) != 0 表示有人请求过),任何请求都会把 next_render 置 0,从而立即重绘一次:

static void display_thread(void)
{
    enum { MODE_SENSORS, MODE_CHART } mode = MODE_SENSORS;
    bool on = true;
    int64_t next_render = 0;


    for (;;) {
        struct reading r;


        REPORT_PROGRESS(THREAD_DISPLAY);


        if (atomic_set(&request_blank_toggle, 0) != 0) {
            on = !on;
            if (on) {
                display_blanking_off(display_dev);
            } else {
                display_blanking_on(display_dev);
            }
            printf("Display %s\n", on ? "on" : "off");
            next_render = 0;
        }


        if (atomic_set(&request_mode_toggle, 0) != 0) {
            mode = (mode == MODE_SENSORS) ? MODE_CHART : MODE_SENSORS;
            /* ... 打印当前页面名 ... */
            next_render = 0;
        }


        if (on && k_uptime_get() >= next_render) {
            k_mutex_lock(&reading_lock, K_FOREVER);
            r = latest;
            k_mutex_unlock(&reading_lock);


            if (mode == MODE_CHART) {
                display_chart();
            } else {
                display_sensors(&r);
            }


            next_render = k_uptime_get() + DISPLAY_RENDER_MS;
        }


        k_msleep(DISPLAY_PERIOD_MS);
    }
}

数值页的四行文本同样是 snprintf() 到行缓冲后再逐行 cfb_print(),最后一次性 cfb_framebuffer_finalize() 写屏(缺数据时用 "air warming up" / "no T/RH" 之类占位文本)。

sensors 线程

职责:每秒读一次两个传感器、维护 ENS160 的工作模式与补偿、把结果写入环形缓冲并发布给其他线程。它是唯一的数据生产者,也是唯一直接访问两个传感器器件的线程。

主循环是「先救器件 → 再读数据 → 保证 ENS160 在工作模式 → 判定有效性 → 入环 → 发布 → 打印」:

static void sensor_thread(void)
{
    const struct device *bus = DEVICE_DT_GET(DT_BUS(ENS160_NODE));
    struct sensor_health aht20 = { .dev = DEVICE_DT_GET_OR_NULL(AHT20_NODE), .bus = bus,
                       .addr = AHT20_ADDR, .status_reg = AHT20_STATUS_REGISTER,
                       .needs_calibration = true, .name = "AHT20" };
    struct sensor_health ens160 = { .dev = DEVICE_DT_GET(ENS160_NODE), .bus = bus,
                    .addr = ENS160_ADDR, .status_reg = ENS160_STATUS_REGISTER,
                    .needs_calibration = false, .name = "ENS160" };
    bool started = false;
    int64_t invalid_since = 0;
    int64_t last_restart = 0;


    for (;;) {
        struct reading r;


        REPORT_PROGRESS(THREAD_SENSORS);


        /*
         * Sensors that failed their boot-time init are retried here, so a
         * slow power-up does not leave the board without readings.
         */
        sensor_ensure(&aht20);
        sensor_ensure(&ens160);


        read_sensors(&r);


        /* ... 第一次拿到数据后进入 ENS160 工作模式,见 4.5.2 ... */
        if (!started && device_is_ready(ens160.dev)) {
            ens160_start(&ens160, &r);
            started = true;
            invalid_since = 0;
        }


        if (r.air_ok) {
            invalid_since = 0;
        } else if (invalid_since == 0) {
            invalid_since = k_uptime_get();
        }


        /* ... 长时间无有效数据则重跑启动时序,见 4.5.2 末段 ... */


        if (r.air_ok) {
            /* The chart is drawn from RAM, so record every measurement. */
            chart_add(r.eco2);
        }


        /* 发布给 display / sdcard 线程(见 4.3) */
        k_mutex_lock(&reading_lock, K_FOREVER);
        latest = r;
        k_mutex_unlock(&reading_lock);


        printf("eCO2 %d ppm, TVOC %d ppb, AQI %d", r.eco2, r.tvoc, r.aqi);
        /* ... 或 "eCO2/TVOC/AQI n/a",随后再打印温湿度或 ", T/RH n/a" ... */


        k_msleep(SENSOR_PERIOD_MS);
    }
}

buttons 线程


职责:每 20 ms 轮询两个按键(不使用中断),消抖后把「换页」「熄屏 / 点亮」「清空日志」三种意图转成原子请求标志,交给 display 与 sdcard 线程执行。按键动作都在松开时判定,长按阈值 2 s。

static void button_update(struct button_state *btn, bool is_power_button)
{
    int raw = gpio_pin_get_dt(btn->spec);


    if (raw < 0) {
        return;
    }


    if (raw != btn->candidate) {
        btn->candidate = raw;
        btn->samples = 1;
    } else if (btn->samples < BUTTON_DEBOUNCE) {
        btn->samples++;
    }


    if (btn->samples < BUTTON_DEBOUNCE || btn->stable == btn->candidate) {
        return;
    }


    btn->stable = btn->candidate;


    if (btn->stable != 0) {              /* 按下,记录时刻 */
        btn->pressed_at = k_uptime_get();
        return;
    }


    if (!is_power_button) {              /* P1.21:换页 */
        atomic_or(&request_mode_toggle, 1);
        return;
    }


    if (btn->pressed_at != 0 && k_uptime_get() - btn->pressed_at >= LONG_PRESS_MS) {
        atomic_or(&request_clear_log, 1);    /* 长按 ≥ 2 s:清日志 */
    } else {
        atomic_or(&request_blank_toggle, 1); /* 短按:熄屏/点亮 */
    }
}

sdcard 线程

职责:启动时复位 SDHC 控制器并挂载卡;每 30 s 取一次最新读数写一行 CSV;响应「清空日志」请求。它同时承担两个约束:写卡(而不是只留在 RAM)与卡不可用也不牵连其他功能——挂载失败时打印一行说明后结束线程(日志功能停用,传感器与显示照常运行),既不做重试挂载,也不会让整机跟着停摆。

MicroSD 卡上记录的数据

static void sd_thread(void)
{
    char line[64];
    int64_t next_log = 0;


    sd_mounted = (mount_card() == 0);
    if (!sd_mounted) {
        printf("SD: logging disabled, sensors and display keep running\n");
        return;
    }


    dump_csv();
    terminate_last_line();


    for (;;) {
        if (atomic_set(&request_clear_log, 0) != 0) {
            clear_log();
        }


        if (k_uptime_get() >= next_log) {
            struct reading r;


            k_mutex_lock(&reading_lock, K_FOREVER);
            r = latest;
            k_mutex_unlock(&reading_lock);


            /*
             * Log whenever the air quality is valid, even if the climate
             * sensor is missing, so the record survives AHT20 dropouts.
             */
            if (r.air_ok) {
                if (r.climate_ok) {
                    snprintf(line, sizeof(line),
                         "%u,%d,%d,%d,%d.%d,%d\n",
                         (unsigned int)k_uptime_get(), r.eco2, r.tvoc,
                         r.aqi, r.temp_tenths / 10,
                         abs(r.temp_tenths % 10), r.rh);
                } else {
                    snprintf(line, sizeof(line), "%u,%d,%d,%d,,\n",
                         (unsigned int)k_uptime_get(), r.eco2, r.tvoc,
                         r.aqi);
                }
                append_to_csv(line);
            }


            next_log = k_uptime_get() + LOG_PERIOD_MS;
        }


        k_msleep(SD_PERIOD_MS);
    }
}

watchdog 线程

职责:判断被监督的线程是否还在推进——健康就喂狗,不健康就不喂,让硬件看门狗复位整机。它是「软件跑飞也能自恢复」的最后一道保险。sdcard 线程被有意排除在监督之外。

#define DHCSR_ADDR  0xE000EDF0UL
#define DHCSR_C_DEBUGEN (1UL << 0)


static void watchdog_thread(void)
{
    const struct device *wdt = DEVICE_DT_GET(DT_NODELABEL(wdt0));
    struct wdt_timeout_cfg cfg = {
        .window = { .min = 0U, .max = WATCHDOG_TIMEOUT_MS },
        .callback = NULL,
        .flags = WDT_FLAG_RESET_SOC,
    };
    int channel;


    if (*(volatile uint32_t *)DHCSR_ADDR & DHCSR_C_DEBUGEN) {
        printf("Watchdog not armed: debugger attached\n");
        return;                       /* 挂调试器时不启用,避免打断烧写 */
    }


    channel = wdt_install_timeout(wdt, &cfg);


    if (!device_is_ready(wdt) || channel < 0) {
        printf("Watchdog unavailable\n");
        return;
    }


    if (wdt_setup(wdt, 0) < 0) {
        printf("Watchdog setup failed\n");
        return;
    }


    for (;;) {
        int64_t now = k_uptime_get();
        bool stuck = false;


        for (int i = 0; i < THREAD_COUNT; i++) {
            int64_t last = (int64_t)atomic_get(&thread_progress[i]);


            if (last != 0 && now - last > THREAD_STALE_MS) {   /* 8000 ms */
                printf("Thread '%s' stalled %d ms, letting the watchdog reset the board\n",
                       thread_name[i], (int)(now - last));
                stuck = true;
            }
        }


        if (!stuck) {
            wdt_feed(wdt, channel);
        }


        k_msleep(WATCHDOG_CHECK_MS);
    }
}

遇到的问题和解决方法

1. CFS 不能生成 MAX32650 的 Zephyr SDHC 设备树:在 CFS 里开启 SDHC,但是生成的文件里只有几句警告:

#warning P1.0 (B2) is not mapped to SDHC_CMD as the peripheral or signal is not currently supported.
#warning P1.1 (D3) is not mapped to SDHC_DAT2 as the peripheral or signal is not currently supported.
#warning P1.2 (C3) is not mapped to SDHC_WP as the peripheral or signal is not currently supported.
#warning P1.3 (E4) is not mapped to SDHC_DAT3 as the peripheral or signal is not currently supported.
#warning P1.4 (C4) is not mapped to SDHC_DAT0 as the peripheral or signal is not currently supported.
#warning P1.5 (D4) is not mapped to SDHC_CLK as the peripheral or signal is not currently supported.
#warning P1.6 (B4) is not mapped to SDHC_DAT1 as the peripheral or signal is not currently supported.

最后只能自己写设备树和修改 SDHC 驱动来支持 MicroSD。
2. SD 卡只能读到块 0: 上游驱动在发送命令前用数据阶段的块地址覆盖了 cmd->arg,这对按块寻址的 SDHC 卡无害,但对按字节寻址的标准容量(SDSC)卡会导致除首块以外全部读错,表现为「能识别卡、能挂载、文件内容全 0」。修好寻址后 FAT 读写恢复正常。


心得体会

这次从点亮一块 OLED 做到一台能独立运行、自己记录数据的监测仪,最大的感受是:真正花时间的从来不是写代码,而是搞清楚"现象背后到底是什么"。AHT20 "时好时坏"查了很久,经过很多次调试才发现问题。

CodeFusion Studio 的基本功能还是很好用的,项目搭建、编译调试都很方便,但是 SDK 支持似乎不完善(开启SDHC只会生成警告),希望能继续完善 MSDK 和 Zephyr 支持,做到真正开箱即用。

附件下载
zephyr.elf
Firmware
max32650-air-quality-v2.zip
Code
团队介绍
DDL Fighter
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号