
1. 项目缘起为什么选择RT-Spark与AHT21搭建温湿度系统最近在做一个环境监测的小项目核心需求是实时、稳定地采集温度和湿度数据并且希望系统有一定的实时性和可扩展性。市面上常见的方案是直接用Arduino搭配DHT11或者DHT22简单是简单但总感觉在数据稳定性、代码结构以及未来加功能比如联网、复杂逻辑上有点捉襟见肘。正好手头有闲置的STM32开发板就琢磨着用更“正经”的嵌入式路子来搞。为什么是STM32因为它资源丰富、生态成熟无论是用HAL库快速原型开发还是想深入底层寄存器操作都有很大的自由度。更重要的是我想借此机会实践一下RTOS实时操作系统在小型项目中的应用让数据采集、处理、显示或上传这些任务能并行、可靠地运行而不是挤在一个大循环里。这就是“RT”部分的由来——我选择了RT-Thread这个国产的、组件丰富的实时操作系统它的“Spark”软件包生态正好有AHT21的驱动能省去不少写底层I2C驱动的功夫。传感器方面我放弃了经典的DHT系列选择了AHT21。原因很简单精度和稳定性。AHT21是新一代温湿度复合传感器精度更高温度±0.3°C湿度±2%RH并且采用了I2C数字接口抗干扰能力比单总线强通信也更标准可靠。对于需要较精确数据记录的场景它是更合适的选择。所以这个“RT-Spark AHT21 Temperature Humidity System using STM32”项目本质上是一个基于RT-Thread实时操作系统利用其软件包Spark快速驱动AHT21传感器在STM32平台上构建的一个高可靠性、模块化的温湿度监测系统。它不仅仅是一个简单的读数程序更是一次融合了RTOS应用、传感器驱动集成、模块化编程的实践。2. 硬件选型与核心电路设计要点硬件是整个系统的基石选型和连接方式直接决定了系统的稳定性和开发难度。这里我详细拆解一下我的选择理由和几个容易踩坑的细节。2.1 MCU主控STM32F103C8T6 “核心板”我使用的是最常见的“蓝色药丸”核心板主控是STM32F103C8T6。选择它原因如下性价比与资源平衡拥有72MHz的Cortex-M3内核20KB RAM64KB Flash对于运行RT-Thread Nano内核仅占用3KB左右ROM和我们的应用绰绰有余。生态无敌资料最多社区问题基本都能找到答案无论是标准库、HAL库还是直接寄存器操作都有海量参考。引脚丰富提供了足够的GPIO、USART、I2C、SPI等外设方便后续扩展如连接OLED屏、Wi-Fi模块等。2.2 传感器AHT21模块AHT21通常以模块形式出售板上已集成上拉电阻和滤波电容。关键引脚就四个VCC3.3V供电。务必接3.3V虽然AHT21数据手册标明工作电压范围是2.2V-5.5V但为了与STM32的I2C电平兼容避免电平转换的麻烦强烈建议统一使用3.3V。GND接地。SDAI2C数据线。SCLI2C时钟线。2.3 电路连接与“隐形”坑点连接看起来很简单VCC→3.3V GND→GND SDA→PB7 SCL→PB6STM32F103的I2C1默认引脚。但这里有几个新手极易忽略的要点上拉电阻必须接I2C总线是开漏输出必须通过上拉电阻拉到高电平才能正常工作。虽然AHT21模块上通常已经集成了常见的是4.7kΩ或10kΩ但如果你是自己画的板子或者用的裸传感器必须在SDA和SCL线上各接一个4.7kΩ的上拉电阻到3.3V。即使模块上有如果总线负载重比如挂了多个设备可能还需要根据情况调整阻值。电源去耦不容忽视在AHT21的VCC和GND引脚附近最好再加一个0.1uF的陶瓷电容用于滤除高频噪声。稳定的电源是传感器获得稳定数据的前提。很多读数跳动大的问题根源就在电源噪声。地址确认AHT21的I2C设备地址是0x387位地址。注意这是写地址。读地址是0x39。大部分驱动库会要求你输入7位地址0x38。用逻辑分析仪或者I2C扫描代码确认设备地址是调试的第一步。我的实际连接示意图如下文字描述STM32F103C8T6 AHT21 Module 3.3V ------ VCC GND ------ GND PB7 (I2C1_SDA) ------ SDA PB6 (I2C1_SCL) ------ SCL注意确保开发板的3.3V输出能力足够通常100mA以上绝对够用避免因供电不足导致传感器工作异常。3. 开发环境搭建与RT-Thread项目创建玩STM32开发环境是第一道门槛。我选择的是RT-Thread Studio集成开发环境。它基于Eclipse内置了RT-Thread的工程创建、配置、构建和下载功能对于快速入门RT-Thread特别友好避免了手动移植内核的繁琐。3.1 软件安装与准备安装RT-Thread Studio从官网下载安装包安装过程简单。它会自动安装ARM GCC编译工具链无需自己折腾。安装STM32CubeMX可选但推荐虽然Studio可以配置引脚但用CubeMX来初始化和验证引脚功能更直观。确保安装了STM32F1系列的HAL库支持包。准备调试器驱动无论是ST-Link还是DAP-Link确保其驱动在电脑上安装正确在设备管理器中能识别到。3.2 创建基于芯片的RT-Thread项目这是关键步骤一步步来打开RT-Thread Studio点击“文件”-“新建”-“RT-Thread项目”。在项目类型中选择“基于芯片”。这是最直接的方式Studio会为我们生成针对特定STM32型号的基本工程包含RT-Thread Nano内核。在“芯片型号”中搜索并选择“STM32F103C8”。相关的调试器如ST-Link和接口SWD会自动配置好。给项目起个名字比如rt_spark_aht21点击完成。此时Studio会生成一个完整的项目框架其中rtconfig.h是RT-Thread内核的核心配置文件board.c包含了芯片的时钟、串口初始化等板级支持代码。3.3 配置I2C外设支持默认工程可能没有开启I2C。我们需要手动配置。打开项目根目录下的board.h文件。这个文件通常用于定义板级的引脚映射。我们需要确认或定义I2C所使用的引脚。根据之前的硬件连接我们用的是I2C1引脚是PB6和PB7。在board.h中添加或确认以下宏定义/* 定义I2C1的SCL和SDA引脚 */ #define BSP_I2C1_SCL_PIN GET_PIN(B, 6) #define BSP_I2C1_SDA_PIN GET_PIN(B, 7)打开board.c文件找到rt_hw_board_init()函数。这个函数在系统启动时调用是初始化硬件的地方。我们需要在其中添加I2C引脚的初始化代码。注意更规范的做法是使用RT-Thread的PIN设备框架来初始化GPIO但为了简单起见我们可以直接使用HAL库风格初始化前提是工程已包含HAL库。一个更RT-Thread的方式是稍后通过ENV工具使能I2C总线驱动。 实际上对于快速验证我们可以先不在这里做复杂初始化而是等待后续通过RT-Thread的软件包系统来驱动AHT21软件包本身会处理I2C的初始化。但为了确保底层引脚功能正确一个保险的做法是在main函数最开始在RT-Thread调度器启动前调用一个简单的引脚模式设置#include drv_common.h // ... 在main函数中RT-Thread初始化之前 rt_hw_pin_init(); // 初始化PIN设备如果已启用 rt_pin_mode(GET_PIN(B, 6), PIN_MODE_OUTPUT_OD); // PB6, 开漏输出 rt_pin_mode(GET_PIN(B, 7), PIN_MODE_OUTPUT_OD); // PB7, 开漏输出 rt_pin_write(GET_PIN(B, 6), PIN_HIGH); rt_pin_write(GET_PIN(B, 7), PIN_HIGH);这段代码将PB6和PB7设置为开漏输出并拉高模拟了I2C总线的空闲状态。这是一种简易的准备工作。更完整的I2C总线驱动使能需要通过RT-Thread的ENV工具。4. 使用RT-Thread软件包中心添加AHT21驱动这是本项目“Spark”部分的精髓所在。RT-Thread有一个强大的软件包中心里面汇集了大量社区贡献的传感器、通信协议、算法等驱动和组件。AHT21的驱动就在其中这能让我们免于从零编写I2C读写代码。4.1 打开ENV配置工具在RT-Thread Studio中右键点击项目选择“RT-Thread Settings”或者直接打开项目根目录下的rt-thread文件夹里的Kconfig文件。这会启动图形化的配置界面其背后是RT-Thread的menuconfig系统ENV工具。4.2 查找并启用AHT21软件包在配置界面找到并展开“硬件”或“外设”相关分类不同版本位置可能略有不同寻找“传感器”或者“软件包”选项。进入“软件包”区域找到“传感器驱动”或“peripheral libraries”分类。在其中寻找“aht10”软件包。等等为什么是AHT10因为AHT21、AHT20、AHT10的驱动协议是兼容的社区通常用aht10这个软件包来支持这一系列传感器。选中它。启用该软件包后通常会出现子选项比如“Enable aht10 sample”启用示例代码。务必勾选这个示例选项它会生成一个使用传感器的示例代码文件是我们学习的绝佳模板。由于AHT10/AHT21使用I2C接口你需要确保I2C总线驱动已经启用。返回硬件配置找到“硬件驱动”或“设备驱动”启用“I2C设备驱动”并确保对应的I2C总线编号如I2C1被启用。4.3 保存并生成工程配置完成后保存退出。RT-Thread Studio会自动根据新的配置下载选中的软件包源代码到项目的packages文件夹并重新生成rtconfig.h等配置文件。这个过程可能会从GitHub或Gitee拉取代码需要网络通畅。4.4 关键一步手动检查与调整软件包代码软件包自动下载后我们绝不能假设它完全适合我们的硬件。必须手动检查关键文件。主要关注两个文件软件包中的示例文件通常路径是packages/aht10-latest/samples/aht10_sample.c。打开这个文件这是官方提供的使用范例。软件包中的底层接口文件可能是packages/aht10-latest/src/aht10.c。我们需要关注其初始化函数特别是I2C设备名称和引脚的定义。常见问题与调整I2C设备名称不匹配示例代码中可能使用i2c1作为设备名。你需要确认在你的工程中I2C1总线注册的设备名是什么。查看drv_i2c.c或相关板级支持文件通常就是i2c1。如果不确定可以在main函数里用rt_device_find()尝试查找。引脚定义方式有些软件包会在自己的头文件里如aht10_config.h要求你定义SDA和SCL的引脚号。你需要根据GET_PIN(B,7)和GET_PIN(B,6)的格式进行修改。RT-Thread的PIN编号不是简单的GPIO编号需要通过GET_PIN宏计算。例如PB7的PIN号通常是(1*16 7) 23端口B编号为1引脚号7所以GET_PIN(1, 7)。你需要根据实际情况调整。 更简单的做法是直接修改示例文件将其中查找I2C设备、初始化传感器的部分替换成我们已知正确的参数。一个经过调整的、可靠的aht10_sample.c主体逻辑如下#include rtthread.h #include rtdevice.h #include sensor.h #include aht10.h // 软件包提供的头文件 #define AHT10_I2C_BUS_NAME i2c1 // I2C总线设备名 #define AHT10_ADDR 0x38 // 传感器7位地址 static void aht10_sample_thread_entry(void *parameter) { struct rt_sensor_data sensor_data; rt_device_t dev RT_NULL; rt_size_t res; /* 1. 查找传感器设备 */ dev rt_device_find(temp_aht); // 注意设备名由驱动注册决定可能是temp_aht、humi_aht或aht10 if (dev RT_NULL) { rt_kprintf(Cant find AHT10/AHT21 device!\n); return; } /* 2. 打开设备 */ if (rt_device_open(dev, RT_DEVICE_FLAG_RDWR) ! RT_EOK) { rt_kprintf(Open AHT10/AHT21 device failed!\n); return; } while (1) { /* 3. 读取传感器数据 */ res rt_device_read(dev, 0, sensor_data, 1); if (res 1) { /* 4. 处理数据sensor_data.data.temp是温度值可能是整型或浮点需根据驱动定义转换 */ /* 例如如果驱动返回的是整数扩大10倍则 */ float temperature sensor_data.data.temp / 10.0; float humidity sensor_data.data.humi / 10.0; rt_kprintf(Temperature: %.1f C, Humidity: %.1f %%\n, temperature, humidity); } else { rt_kprintf(Read sensor data failed!\n); } /* 5. 延时2秒 */ rt_thread_mdelay(2000); } } static int aht10_sample_init(void) { rt_thread_t thread; /* 创建采样线程 */ thread rt_thread_create(aht_samp, aht10_sample_thread_entry, RT_NULL, 1024, // 栈大小 25, // 线程优先级 20); // 时间片 if (thread ! RT_NULL) { rt_thread_startup(thread); } return RT_EOK; } /* 导出到自动初始化可选 */ INIT_APP_EXPORT(aht10_sample_init);提示设备名“temp_aht”需要你查看驱动注册的实际名称。最直接的方法是在驱动初始化成功后通过rt_kprintf打印信息或者查看aht10.c中rt_hw_aht10_init()函数里注册设备时使用的名字。5. 多线程设计与数据采集任务实现在裸机程序中我们可能用一个while(1)大循环来读传感器、处理、显示。但在RT-Thread中我们可以利用多线程任务来让程序结构更清晰响应更实时。这里我设计了一个简单的双线程结构。5.1 线程划分与职责传感器采集线程(sensor_thread)优先级设为较高例如10保证数据采集的周期性。职责每2秒精确地读取一次AHT21的数据。读取后不进行复杂处理只做基本的有效性校验如数值是否在合理范围内然后将原始数据通过消息队列或邮箱发送给处理线程。这样做的好处是采集线程的负载很轻能严格保证采样间隔即使处理线程偶尔繁忙也不会丢失采样点。数据处理与输出线程(process_thread)优先级设为较低例如15。职责阻塞等待接收来自采集线程的数据。收到数据后进行必要的处理例如单位转换将驱动返回的原始整型值转换为浮点数的摄氏度和百分比湿度。简单滤波实现一个滑动平均滤波或一阶滞后滤波让显示的数据更平滑减少跳动。输出将处理后的数据通过串口打印出来调试用或者驱动OLED屏幕显示或者打包准备通过无线模块发送。5.2 使用消息队列进行线程间通信消息队列是RT-Thread中一种高效的线程间通信机制非常适合传递定长的小数据块。在这里我们可以定义一个结构体来存放温湿度数据。首先在全局区域定义消息结构体和队列/* 定义温湿度数据消息结构 */ struct env_msg { rt_int32_t temp; // 温度原始值可能是放大后的整数 rt_int32_t humi; // 湿度原始值 }; static struct rt_messagequeue mq; // 消息队列控制块 static char mq_pool[256]; // 消息队列用的内存池大小根据消息数量和大小定在main函数或一个初始化函数中创建消息队列/* 创建消息队列每个消息大小为 sizeof(struct env_msg)池子能存4个消息 */ rt_mq_init(mq, env_mq, /* 队列名字 */ mq_pool[0], /* 内存池指针 */ sizeof(struct env_msg), /* 每个消息大小 */ sizeof(mq_pool), /* 内存池总大小 */ RT_IPC_FLAG_FIFO); /* FIFO模式 */5.3 采集线程实现采集线程的核心是周期性地读取传感器并发送消息。static void sensor_thread_entry(void *parameter) { rt_device_t dev RT_NULL; struct rt_sensor_data sensor_data; struct env_msg msg; rt_size_t res; dev rt_device_find(temp_aht); // 查找设备 // ... 错误处理打开设备等同上文示例 while (1) { res rt_device_read(dev, 0, sensor_data, 1); if (res 1) { msg.temp sensor_data.data.temp; // 假设驱动直接返回整型值 msg.humi sensor_data.data.humi; /* 将消息发送到队列如果队列满则等待最多10个时钟节拍 */ if (rt_mq_send(mq, msg, sizeof(msg)) ! RT_EOK) { rt_kprintf([Sensor] Message queue full, data dropped.\n); } else { rt_kprintf([Sensor] Data sampled and sent.\n); } } else { rt_kprintf([Sensor] Read failed.\n); } /* 精确延时2000ms */ rt_thread_mdelay(2000); } }5.4 处理线程实现处理线程的核心是阻塞等待消息然后进行处理和输出。static void process_thread_entry(void *parameter) { struct env_msg msg; float temp_filtered 0.0, humi_filtered 0.0; const float alpha 0.2; // 一阶滞后滤波系数 while (1) { /* 阻塞等待消息队列永久等待 */ if (rt_mq_recv(mq, msg, sizeof(msg), RT_WAITING_FOREVER) RT_EOK) { /* 1. 单位转换假设原始数据是放大10倍的整数 */ float temp_current msg.temp / 10.0f; float humi_current msg.humi / 10.0f; /* 2. 一阶滞后滤波低通滤波 */ temp_filtered alpha * temp_current (1 - alpha) * temp_filtered; humi_filtered alpha * humi_current (1 - alpha) * humi_filtered; /* 3. 输出结果 */ rt_kprintf([Process] Filtered - Temp: %.1fC, Humi: %.1f%%\n, temp_filtered, humi_filtered); /* 4. 这里可以添加其他操作更新显示、通过网络发送等 */ // update_oled_display(temp_filtered, humi_filtered); // send_via_wifi(temp_filtered, humi_filtered); } } }5.5 线程创建与启动最后在main函数中或在自动初始化阶段创建并启动这两个线程int main(void) { rt_thread_t sensor_tid, process_tid; /* 硬件初始化等... */ /* 创建采集线程 */ sensor_tid rt_thread_create(sensor, sensor_thread_entry, RT_NULL, 512, // 栈大小 10, // 较高优先级 5); // 时间片 if (sensor_tid ! RT_NULL) rt_thread_startup(sensor_tid); /* 创建处理线程 */ process_tid rt_thread_create(process, process_thread_entry, RT_NULL, 1024, // 需要稍大栈空间处理数据 15, // 较低优先级 5); if (process_tid ! RT_NULL) rt_thread_startup(process_tid); /* 启动RT-Thread调度器 */ return 0; }这样的设计使得数据采集和数据处理解耦系统更健壮也更容易扩展。比如未来你想增加一个线程专门负责网络上传只需要让处理线程将滤波后的数据通过另一个队列或邮箱发送给网络线程即可。6. 系统调试与常见问题排查实录即使按照步骤操作第一次成功前也难免遇到问题。下面是我在实现过程中遇到的几个典型问题及排查思路堪称“踩坑实录”。6.1 I2C通信失败根本读不到数据现象程序运行但一直打印“Cant find device”或“Read failed”。排查步骤检查硬件连接这是最基础也最容易出错的一步。用万用表测量AHT21模块的VCC是否为稳定的3.3VGND是否连通。检查SDA、SCL线是否接反、虚焊。检查上拉电阻用万用表测量SDA和SCL线对3.3V的电阻。如果模块已集成上拉测量值应在4.7kΩ左右。如果电阻无穷大或非常大说明上拉电阻未接通。使用逻辑分析仪这是最强大的调试工具。将逻辑分析仪的通道连接到SDA和SCL设置触发条件为I2C起始信号。运行程序观察是否有波形产生。如果无任何波形说明STM32的I2C根本没有启动。检查代码中I2C外设是否使能引脚初始化模式是否正确必须为开漏输出PIN_MODE_OUTPUT_OD并且初始电平为高。检查RT-Thread的I2C设备驱动是否成功注册rt_device_find(i2c1)是否能找到。如果有波形但异常观察起始信号SCL高电平时SDA由高到低和停止信号SCL高电平时SDA由低到高是否完整。观察发送的设备地址0x38后面跟一个读写位是否正确。AHT21的地址字节是0x38 1 | 00x70写或0x71读。看看逻辑分析仪解码出的地址对不对。编写I2C扫描程序在RT-Thread中写一个简单的任务遍历所有可能的I2C地址0x08到0x77发送起始信号和地址看哪个地址有ACK应答。这能快速确认传感器是否在线、地址是否正确。6.2 能读到数据但数值固定不变或明显错误现象能打印出数据但温度一直是25.0湿度一直是50.0或者数值完全离谱如湿度120%。排查步骤检查传感器初始化AHT21上电后需要发送初始化命令0xBE进行校准。确保驱动代码中包含了正确的初始化序列。很多驱动库的示例代码里初始化只执行一次。如果初始化失败后续读数可能返回默认值或错误值。在代码中加入初始化状态打印。检查数据解析AHT21返回的是6个字节的原始数据需要按照数据手册的公式进行转换。仔细核对驱动代码中的解析部分。常见的错误是字节顺序弄错或者转换公式计算有误。可以打印出原始的6个字节手动计算一下看和驱动计算的结果是否一致。检查电源和温湿度条件传感器本身是否处于极端环境用手触摸传感器看温度值是否有缓慢变化。向传感器哈气看湿度值是否快速上升。如果完全没有变化可能是传感器已损坏。延时问题AHT21在发出测量命令后需要一定的转换时间典型值80ms。驱动代码中在发送测量命令0xAC后必须有足够的延时rt_thread_mdelay(100)再去读取数据否则读到的可能是旧数据或无效数据。6.3 系统运行不稳定偶尔死机或重启现象系统运行一段时间后串口无输出或者看门狗复位。排查步骤检查栈溢出这是RTOS中最常见的问题。为sensor_thread和process_thread分配的栈空间上面代码中的512和1024可能不足。尤其是在处理线程中如果进行字符串格式化打印如rt_kprintf浮点数会消耗较多栈空间。可以尝试将栈大小适当调大如768和1536或者使用RT-Thread的list_thread命令在shell中查看各线程的栈使用情况。检查消息队列阻塞如果处理线程处理太慢而采集线程不断发送消息消息队列很快会满。上面采集线程的rt_mq_send使用了有限等待10个tick如果发送失败会丢弃数据并打印警告。如果看到频繁的“Message queue full”打印说明处理线程成了瓶颈需要优化其处理速度或者增大消息队列的容量内存池大小。检查中断冲突如果你使用了其他外设如串口、定时器的中断并且中断服务程序执行时间过长可能会影响RT-Thread的调度导致高优先级线程无法及时运行。确保ISR尽量短小精悍。使用调试器连接ST-Link当系统死机时暂停程序查看当前各线程的状态、调用栈往往能定位到问题所在。检查是否进入了HardFault中断。6.4 软件包示例代码编译不通过现象添加AHT10软件包后编译报错提示找不到头文件或函数未定义。排查步骤检查头文件路径在RT-Thread Studio的项目属性中检查编译器的包含路径是否自动添加了软件包的头文件路径。通常Studio会自动处理但有时需要手动将packages/aht10-latest/inc这样的路径添加到包含目录。检查宏定义软件包可能通过Kconfig定义了一些宏如PKG_USING_AHT10需要在rtconfig.h中确认这些宏是否已打开。确保在RT-Thread Settings中正确启用了软件包和示例。检查函数声明如果报错是某个函数未定义去软件包的源文件.c里查看该函数是否正确定义并且其所在的文件是否被加入到了编译列表中通常软件包的SConscript文件会管理这个。最笨但有效的方法直接把你需要的、调试好的传感器驱动代码aht10.c和aht10.h从packages文件夹复制到你的应用目录下然后在你的工程中直接包含和编译它们绕过软件包系统的依赖。这在快速验证时非常有用。通过以上系统的硬件连接、软件配置、多线程设计和细致的调试一个基于RT-Thread和STM32的AHT21温湿度监测系统就稳定地运行起来了。这个项目麻雀虽小五脏俱全涵盖了嵌入式开发从硬件到RTOS应用的多个关键环节。