2026 ADI CodeFusion 竞赛 - 基于max32650实现智能植物监护器
该项目使用了max32650,实现了智能植物监护器的设计,它的主要功能为:以 SHT45 采集植物所处环境的温湿度,同时读取土壤与光照两路开关量,在 OLED 上本地显示,并提供按键本地设置、蓝牙远程设置、越限报警与定时浇水,用于植物生长环境的实时监护与异常提示。。
标签
嵌入式系统
ADI
CodeFusion Studio
fly_lv
更新2026-09-29
11

基于max32650实现智能植物监护器

所选任务:赛道5创新应用的智能植物监护器
任务内容:采集土壤湿度、环境温湿度及光照等数据,实时显示植物生长环境,并根据设定阈值进行缺水提醒,可扩展自动浇水控制功能。

项目描述

本项目是一个基于 MAX32650 + Zephyr RTOS的智能植物监护器:以 SHT45 采集植物所处环境的温湿度,同时读取土壤与光照两路开关量,在 OLED 上显示对应的温湿度,光照,土壤湿度数据,并提供按键本地设置、蓝牙远程设置、越限报警与定时浇水,用于植物生长环境的实时监护与异常提示。

开发环境

采用vscode+CodeFusion Studio插件进行开发

image.png


下图是所用到的外设

image.png



硬件

硬件采用了max32650,0.96oled,sht45,光敏模块,土壤湿度传感器,mx-01ps蓝牙模块,面包板,杜邦线若干


接线关系


P2_17 、P2_18 ---------- OLED SHT45
P2_14 、P2_16 ----------- 蓝牙模块

P2_13 ------------- 土壤湿度模块

P2_15 -------------- 光敏模块



max32650

超低功耗Arm Cortex-M4 FPU微控制器,用于电池供电应用;DARWIN系列UP微控制器让小容量电池支持大型应用。MAX32650—MAX32652为超低功耗、存储器可扩展微控制器,专门设计用于高性能、电池供电应用。


image.png


0.96oled

0.96寸OLED显示屏是嵌入式开发和电子制作中非常经典的一款小尺寸、自发光、高对比度的显示模块。它以极低的功耗和简单的接线方式,成为Arduino、STM32、ESP32等单片机项目显示信息的首选。


image.png


sht45

SHT45 传感器属于第四代产品。在 20%到 70%的相对湿度范围内,SHT45 的精度高达±1.0%;在 5°C 到 60°C 的温度范围内,其精度则可达到±0.1°C。

image.png



实际硬件图

image.png



系统框图


image.png


设计思路

系统设计遵循"中断轻量化、线程解耦、总线串行化"原则:中断上下文仅做字节搬运与中断线开关,采样、解析、渲染等耗时逻辑全部下沉至独立线程;采集与显示经"最新样本优先"消息队列异步衔接,显示仅在文本变化时整帧刷屏以节省 I2C 带宽;初始化采用事件广播避免线程饿死,按键以"边沿中断—关闭中断—延时复检"实现规范化消抖,并辅以读取重试、栈分析与崩溃转储保障系统健壮性与可观测性。


下面是线程与优先级

image.png




软件流程图

系统总流程图

image.png


main.c — 启动与主循环

image.png


采样线程

image.png


显示线程

image.png


界面切换流程

image.png


命令接收与解析

image.png



代码

按键代码:

按键驱动模块(buttons.c)负责 SW0(P1.19)与 SW1(P1.21)的消抖与事件分发:按键中断仅关闭该引脚中断并调度 20 ms 延迟工作,消抖工作重新读取引脚电平,确认按下后分别触发"切换视图"与"阈值加一"事件,随后恢复双边沿中断。该"中断触发—延迟消抖—电平确认"机制可有效避免抖动误触发,使中断上下文保持极短,并通过统一的结构体与容器宏实现双按键的规范化驱动。

static void button_decode(struct k_work *w)
{
    struct k_work_delayable *dw = k_work_delayable_from_work(w);
    struct button *b = CONTAINER_OF(dw, struct button, dwork);


    if (gpio_pin_get_dt(&b->spec) == 1) {
        menu_event(b->ev);
    }


    gpio_pin_interrupt_configure_dt(&b->spec, GPIO_INT_EDGE_BOTH);
}


static void button_isr(const struct device *dev, struct gpio_callback *cb,
               uint32_t pins)
{
    struct button *b = CONTAINER_OF(cb, struct button, cb);


    ARG_UNUSED(dev);
    ARG_UNUSED(pins);


    /* mute the line until the debounced decode re-reads the level */
    gpio_pin_interrupt_configure_dt(&b->spec, GPIO_INT_DISABLE);
    k_work_schedule(&b->dwork, K_MSEC(BTN_DEBOUNCE_MS));
}


static int button_init(struct button *b)
{
    int ret;


    if (!gpio_is_ready_dt(&b->spec)) {
        return -ENODEV;
    }
    ret = gpio_pin_configure_dt(&b->spec, GPIO_INPUT);
    if (ret < 0) {
        return ret;
    }
    k_work_init_delayable(&b->dwork, button_decode);
    gpio_init_callback(&b->cb, button_isr, BIT(b->spec.pin));
    ret = gpio_add_callback_dt(&b->spec, &b->cb);
    if (ret < 0) {
        return ret;
    }
    return gpio_pin_interrupt_configure_dt(&b->spec, GPIO_INT_EDGE_BOTH);
}


static struct button btn_sw0 = {
    .spec = GPIO_DT_SPEC_GET(SW0_NODE, gpios),
    .ev = UI_EV_SW0_SHORT,
};


static struct button btn_sw1 = {
    .spec = GPIO_DT_SPEC_GET(SW1_NODE, gpios),
    .ev = UI_EV_SW1_SHORT,
};


界面代码 :
OLED 菜单模块(menu.c)实现视图状态机、阈值管理与 10 Hz 显示线程:SW0 循环切换主界面(实时温湿度与光照/土壤数字电平)、温度阈值、湿度阈值与 LED1 点亮时间四个视图,SW1 对当前设置视图的数值加一(钳位 0~99 / 1~99);显示线程从传感器消息队列中仅取最新样本渲染,并仅在文本内容变化时才刷新屏幕,未变化帧不产生任何 I2C 开销,配合互斥锁保障视图、阈值与总线访问的并发安全,兼顾显示实时性与总线效率。


void menu_event(enum ui_event ev)
{
    k_mutex_lock(&ui_lock, K_FOREVER);


    switch (ev) {
    case UI_EV_SW0_SHORT:
        switch (cur_view) {
        case VIEW_MAIN:
            cur_view = VIEW_SET_TTH;
            printf("UI: set temp view\n");
            break;
        case VIEW_SET_TTH:
            cur_view = VIEW_SET_HTH;
            printf("UI: set hum view\n");
            break;
        case VIEW_SET_HTH:
            cur_view = VIEW_SET_TIME;
            printf("UI: set time view\n");
            break;
        case VIEW_SET_TIME:
            cur_view = VIEW_MAIN;
            printf("UI: main view\n");
            break;
        }
        break;


    case UI_EV_SW1_SHORT:
        /* Increment the value shown by the current view only; same
         * clamps as menu_set_*(): 0..99 degC / %RH, 1..99 s */
        switch (cur_view) {
        case VIEW_MAIN:
            break;
        case VIEW_SET_TTH:
            if (t_th < 99) {
                t_th++;
            }
            printf("UI: T threshold -> %d C\n", t_th);
            break;
        case VIEW_SET_HTH:
            if (h_th < 99) {
                h_th++;
            }
            printf("UI: H threshold -> %d %%\n", h_th);
            break;
        case VIEW_SET_TIME:
            if (t_time < 99) {
                t_time++;
            }
            printf("UI: LED1 on-time -> %d s\n", t_time);
            break;
        }
        break;
    }


    k_mutex_unlock(&ui_lock);
}

static void render_view(char lines[4][24])
{
    switch (cur_view) {
    case VIEW_MAIN: {
        const struct telem *s = &last_telem;


        if (s->valid) {
            uint32_t ta = (s->t_centi < 0)
                          ? -(uint32_t)s->t_centi
                          : (uint32_t)s->t_centi;
            uint32_t ra = (s->rh_centi < 0)
                          ? 0U : (uint32_t)s->rh_centi;


            snprintf(lines[0], 24, "T: %s%u.%02u C",
                 (s->t_centi < 0) ? "-" : "",
                 ta / 100U, ta % 100U);
            snprintf(lines[1], 24, "RH: %u.%02u %%",
                 ra / 100U, ra % 100U);
        } else {
            strcpy(lines[0], "T: --.-- C");
            strcpy(lines[1], "RH: --.-- %");
        }
        /* Digital level spec: pin low -> "hight", pin high -> "low" */
        strcpy(lines[2], s->light_lvl ? "L:low" : "L:hight");
        strcpy(lines[3], s->soil_lvl ? "S:low" : "S:hight");
        break;
    }


    case VIEW_SET_TTH:
        strcpy(lines[0], "Set Temp Th");
        snprintf(lines[1], 24, "Th: %d C", t_th);
        lines[2][0] = '\0';
        strcpy(lines[3], "SW0:Next");
        break;


    case VIEW_SET_HTH:
        strcpy(lines[0], "Set Hum Th");
        snprintf(lines[1], 24, "Th: %d %%", h_th);
        lines[2][0] = '\0';
        strcpy(lines[3], "SW0:Next");
        break;


    case VIEW_SET_TIME:
        strcpy(lines[0], "Set Time");
        snprintf(lines[1], 24, "Time: %d s", t_time);
        lines[2][0] = '\0';
        strcpy(lines[3], "SW0:Done");
        break;
    }
}


串口蓝牙代码:

UART1 命令行控制模块实现串口在线调参,无需重新烧录即可修改告警阈值与 LED 点亮时间。模块采用"中断—消息队列—解析线程"三级架构,将耗时解析移出中断上下文;支持`T:xx` 、`RH:xx` 设置温湿度阈值,`time:xx` 设置水泵开启时间(用led1点亮代替),`time_on` 立即开启水泵(点亮 LED1),并具备空闲超时断帧、参数钳位与互斥保护机制。该模块仅占用 UART1 接收通道,与传感器线程的数据上报互不干扰,兼具实时性与健壮性。

static void process_line(const char *s)
{
    const char *p;
    bool any = false;
    int v;


    p = strstr(s, "time_on");
    if (p == NULL) {
        p = strstr(s, "TIME_ON");
    }
    if (p != NULL) {
        printf("UART1 cmd: time_on token -> LED1 on\n");
        sensor_led1_on();
        any = true;
    }


    p = strstr(s, "time:");
    if (p == NULL) {
        p = strstr(s, "TIME:");
    }
    if (p != NULL) {
        v = atoi(p + 5);
        printf("UART1 cmd: time token -> %d s\n", v);
        menu_set_time(v);
        any = true;
    }


    p = strstr(s, "RH:");
    if (p != NULL) {
        v = atoi(p + 3);
        printf("UART1 cmd: RH token -> %d %%\n", v);
        menu_set_hth(v);
        any = true;
    }


    p = strstr(s, "T:");
    if (p != NULL) {
        v = atoi(p + 2);
        printf("UART1 cmd: T token -> %d C\n", v);
        menu_set_tth(v);
        any = true;
    }


    if (!any) {
        printf("UART1 cmd: no time_on/time:/T:/RH: token in line\n");
    }
}


传感器代码:

传感器采样线程(sensor.c)以 1 Hz 周期经互斥保护的 I2C1 总线读取 SHT45 温湿度数据,内置瞬态错误重试与定点数转换机制,并将结果发布至遥测消息队列、经 UART1 上报,同时完成超阈值告警评估;线程还负责采样土壤/光照数字输入(P2.13/P2.15),并作为 LED1(P1.18,低电平点亮)的唯一驱动者,在响应`time_on` 命令点亮后按设定时间自动熄灭,且当湿度低于阈值、土壤为高时自动触发一次照明脉冲,实现了数据采集、上报、告警与执行器控制的统一调度。


static void sensor_thread(void *p1, void *p2, void *p3)
{
    ARG_UNUSED(p1);
    ARG_UNUSED(p2);
    ARG_UNUSED(p3);


    uint32_t led1_sec = 0;  /* remaining LED1 on-time, 0 = off */
    bool auto_trig_prev = false;    /* last auto-trigger condition state */


    app_wait_ready();


    while (1) {
        int32_t t_centi = 0, rh_centi = 0;
        int ret;


        /* Digital inputs: raw physical levels (low = 0, high = 1) */
        int light_lvl = gpio_pin_get_raw(light_in.port, light_in.pin);
        int soil_lvl = gpio_pin_get_raw(soil_in.port, soil_in.pin);


        /* I2C bus transaction group, protected by the bus mutex */
        k_mutex_lock(&i2c_lock, K_FOREVER);
        ret = sht45_read(&t_centi, &rh_centi);
        k_mutex_unlock(&i2c_lock);


        struct telem t = {
            .valid = (ret == 0),
            .t_centi = t_centi,
            .rh_centi = rh_centi,
            .light_lvl = (light_lvl > 0),
            .soil_lvl = (soil_lvl > 0),
            .err = ret,
        };


        if (k_msgq_put(&telem_q, &t, K_NO_WAIT) != 0) {
            k_msgq_purge(&telem_q);
            k_msgq_put(&telem_q, &t, K_NO_WAIT);
        }


        /* Auto-trigger: when the humidity drops below its threshold while
         * soil reads high, fire ONE LED1 pulse (rising edge only, so a
         * lasting condition does not keep re-arming the timer). */
        if (ret == 0) {
            bool cond = (rh_centi < (int32_t)menu_hth() * 100) &&
                    (soil_lvl > 0);


            if (cond && !auto_trig_prev) {
                printf("LED1 auto-trigger: RH below %d %%, soil high\n",
                       menu_hth());
                atomic_set(&led1_req, 1L);
            }
            auto_trig_prev = cond;
        }


        /* One-shot LED1 (exclusive owner of the pin): a "time_on"
         * request lights it (physical low) and (re)loads the on-time;
         * the 1 Hz loop tick then counts it down and turns it off
         * (physical high). */
        if (atomic_cas(&led1_req, 1L, 0L)) {
            led1_sec = (uint32_t)menu_time();
            gpio_pin_set_raw(led1_out.port, led1_out.pin, 0);
            printf("LED1 ON for %u s\n", led1_sec);
        } else if (led1_sec > 0 && --led1_sec == 0) {
            gpio_pin_set_raw(led1_out.port, led1_out.pin, 1);
            printf("LED1 OFF\n");
        }


        if (ret == 0) {
            uint32_t ta = (t_centi < 0) ? -(uint32_t)t_centi
                            : (uint32_t)t_centi;
            uint32_t ra = (rh_centi < 0) ? 0U : (uint32_t)rh_centi;
            char uline[48];


            app_alarm_set(t_centi > (int32_t)menu_tth() * 100 ||
                      rh_centi > (int32_t)menu_hth() * 100);


            printf("Temp: %s%u.%02u C, RH: %u.%02u %%\n",
                   (t_centi < 0) ? "-" : "", ta / 100U, ta % 100U,
                   ra / 100U, ra % 100U);
            snprintf(uline, sizeof(uline),
                 "Temp: %s%u.%02u C, RH: %u.%02u %%\r\n",
                 (t_centi < 0) ? "-" : "", ta / 100U,
                 ta % 100U, ra / 100U, ra % 100U);
            uart1_puts(uline);
        } else {
            char uline[40];


            printf("SHT45 read failed: %d\n", ret);
            snprintf(uline, sizeof(uline),
                 "SHT45 read failed: %d\r\n", ret);
            uart1_puts(uline);
        }


        k_msleep(SAMPLE_INTERVAL_MS);
    }
}




实物演示

通过sw0切换界面,sw1设置阈值

显示数据界面

image.png


温湿度设置界面,屏幕第二行显示温湿度的阈值,在对应的界面能通过sw1设置对应的阈值

image.png

image.png


定时浇水设置界面,

image.png


蓝牙接收界面,使用了微信小程序


image.png


codefusion studio界面

image.png

image.png


遇到的难点及解决方法

难点:一开始配置的i2c1识别不到0.96oled屏幕,但是基于官方的例程能点亮oled
解决方法:把点不亮的oled项目询问了ai,发现是引脚的电压不匹配,要配置成VDDIOH才能点亮oled

其他的外设配置都要设置成这个电压域才能进行操作
image.png


心得体会

感谢电子森林和adi举办的ADI CodeFusion Studio™设计竞赛,我从中学习到了许多zephyr知识,例如线程、消息队列、互斥量、设备树的配置等,体会到了zephyr的便捷性,不需要重复造轮子,直接用现成的,还有如果要替换芯片,更改对应的设备树,上层的代码不需要更改就能成功跑通。





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