STM32+FreeModbus实战:用AHT20传感器搭建低成本温湿度监测从机(附完整代码)
STM32+FreeModbus实战:用AHT20传感器搭建低成本温湿度监测从机(附完整代码)
在工业物联网和智能家居领域,温湿度监测是最基础也最普遍的需求之一。如何用最低的成本构建一个稳定可靠的监测节点?本文将带你从零开始,基于STM32F103C8T6(俗称"蓝莓派")和AHT20温湿度传感器,通过FreeModbus协议栈实现一个完整的Modbus RTU从机设备。不同于市面上常见的教程,我们将重点关注实际工程中容易遇到的坑点,并提供经过验证的完整解决方案。
1. 硬件选型与方案设计
1.1 核心硬件选择
STM32F103C8T6最小系统板(市场价格约10-15元)作为主控具有以下优势:
- Cortex-M3内核,72MHz主频,性能足够处理Modbus协议
- 内置64KB Flash和20KB SRAM
- 丰富的外设接口(USART、I2C、TIM等)
- 广泛的社区支持和成熟的工具链
AHT20温湿度传感器(市场价格约5-8元)的突出特点:
- 数字输出,I2C接口
- ±2%RH湿度精度,±0.3℃温度精度
- 低功耗(典型待机电流0.2μA)
- 出厂校准,无需额外校准电路
1.2 系统架构设计
整个系统的数据流如下图所示:
[温湿度传感器AHT20] ↓ I2C [STM32F103C8T6] ↓ USART(Modbus RTU) [上位机/网关设备]关键设计考虑:
- 采用Modbus RTU协议,波特率115200(可根据需求调整)
- 使用FreeModbus协议栈实现从机功能
- 通过I2C接口每2秒读取一次传感器数据
- 定义4个保持寄存器分别存储:
- 温度整数部分
- 温度小数部分
- 湿度整数部分
- 湿度小数部分
2. 开发环境搭建
2.1 工具链准备
推荐使用以下开发工具组合:
| 工具类型 | 推荐选择 | 备注 |
|---|---|---|
| IDE | STM32CubeIDE 1.11.0 | 免费且集成CubeMX |
| 调试器 | ST-Link V2 | 兼容性好,价格低廉 |
| 串口工具 | Modbus Poll | Modbus专用测试工具 |
| 终端模拟 | Tera Term | 查看调试输出 |
2.2 FreeModbus协议栈准备
FreeModbus官方版本已停止维护,推荐使用社区改进版:
git clone https://github.com/cwalter-at/freemodbus.git关键目录结构说明:
freemodbus ├── modbus # 协议栈核心代码 │ ├── include # 头文件 │ └── rtu # RTU模式实现 └── demo └── bare # 裸机移植示例 ├── port # 硬件相关移植层 └── demo.c # 应用示例提示:建议将协议栈作为子模块加入项目,便于后续更新维护。
3. STM32CubeMX配置
3.1 外设初始化
关键外设配置参数:
时钟配置
- HSE晶振:8MHz
- 系统时钟:72MHz
- APB1分频:2(TIM3时钟36MHz)
USART1配置(Modbus通信)
- 模式:Asynchronous
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:None
I2C1配置(AHT20通信)
- 模式:I2C
- 时钟速度:100kHz
- 地址模式:7-bit
TIM3配置(Modbus定时器)
- 时钟源:Internal Clock
- 预分频:35(1MHz计数频率)
- 计数模式:Up
- 自动重装载:50-1(对应50μs时基)
3.2 中断配置
确保以下中断已使能:
- USART1全局中断
- TIM3全局中断
- I2C1事件中断
- DMA通道6/7中断(如果使用DMA)
NVIC优先级建议设置:
USART1_IRQn -> 0 TIM3_IRQn -> 1 I2C1_EV_IRQn -> 24. FreeModbus移植关键代码
4.1 串口驱动适配
修改portserial.c实现硬件相关串口操作:
BOOL xMBPortSerialPutByte(CHAR ucByte) { if(HAL_UART_Transmit(&huart1, (uint8_t*)&ucByte, 1, 10) != HAL_OK) return FALSE; return TRUE; } BOOL xMBPortSerialGetByte(CHAR *pucByte) { if(HAL_UART_Receive(&huart1, (uint8_t*)pucByte, 1, 10) != HAL_OK) return FALSE; return TRUE; } void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) { if(xRxEnable) __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); else __HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE); if(xTxEnable) __HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE); else __HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE); }4.2 定时器驱动适配
修改porttimer.c实现Modbus要求的超时检测:
void vMBPortTimersEnable() { __HAL_TIM_SET_COUNTER(&htim3, 0); __HAL_TIM_CLEAR_IT(&htim3, TIM_IT_UPDATE); __HAL_TIM_ENABLE_IT(&htim3, TIM_IT_UPDATE); __HAL_TIM_ENABLE(&htim3); } void vMBPortTimersDisable() { __HAL_TIM_DISABLE(&htim3); __HAL_TIM_DISABLE_IT(&htim3, TIM_IT_UPDATE); }4.3 中断服务程序
在stm32f1xx_it.c中添加Modbus相关中断处理:
void USART1_IRQHandler(void) { if(__HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_RXNE) != RESET) prvvUARTRxISR(); if(__HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_TXE) != RESET) prvvUARTTxReadyISR(); HAL_UART_IRQHandler(&huart1); } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM3) prvvTIMERExpiredISR(); }5. 传感器驱动与数据采集
5.1 AHT20初始化
#define AHT20_ADDRESS 0x38 void AHT20_Init(void) { uint8_t cmd[3] = {0xBE, 0x08, 0x00}; HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS<<1, cmd, 3, 100); HAL_Delay(50); cmd[0] = 0x71; HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS<<1, cmd, 1, 100); HAL_Delay(350); }5.2 温湿度数据读取
int AHT20_Read_CTdata(uint32_t *ctData) { uint8_t buf[6] = {0}; uint8_t cmd = 0xAC; HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS<<1, &cmd, 1, 100); HAL_Delay(80); if(HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDRESS<<1, buf, 6, 100) != HAL_OK) return -1; if(!(buf[0] & 0x80)) { ctData[0] = ((uint32_t)buf[1]<<12) | ((uint32_t)buf[2]<<4) | (buf[3]>>4); ctData[1] = ((uint32_t)(buf[3]&0x0F)<<16) | ((uint32_t)buf[4]<<8) | buf[5]; return 0; } return -2; }5.3 数据转换与寄存器映射
在demo.c中实现Modbus寄存器回调:
eMBErrorCode eMBRegInputCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs) { static uint32_t ctData[2]; static int16_t temp, humi; if(AHT20_Read_CTdata(ctData) == 0) { humi = ctData[0] * 1000 / 1048576; // 湿度值(放大10倍) temp = ctData[1] * 2000 / 1048576 - 500; // 温度值(放大10倍) // 寄存器映射 usRegInputBuf[0] = temp / 10; // 温度整数部分 usRegInputBuf[1] = temp % 10; // 温度小数部分 usRegInputBuf[2] = humi / 10; // 湿度整数部分 usRegInputBuf[3] = humi % 10; // 湿度小数部分 } // 寄存器数据返回处理 for(int i=0; i<usNRegs; i++) { *pucRegBuffer++ = usRegInputBuf[usAddress+i] >> 8; *pucRegBuffer++ = usRegInputBuf[usAddress+i] & 0xFF; } return MB_ENOERR; }6. 系统集成与测试
6.1 主程序流程
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_I2C1_Init(); MX_TIM3_Init(); AHT20_Init(); HAL_Delay(1000); eMBInit(MB_RTU, 0x01, 1, 115200, MB_PAR_NONE); eMBEnable(); while(1) { eMBPoll(); HAL_Delay(100); } }6.2 Modbus Poll测试配置
使用Modbus Poll测试时,关键配置参数:
连接设置
- 端口:对应COM口
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验:None
读写定义
- 功能码:04 (Read Input Registers)
- 从站地址:1
- 起始地址:0
- 寄存器数量:4
6.3 常见问题排查
问题1:Modbus Poll显示"Timeout"
- 检查接线是否正确(RX-TX交叉)
- 确认波特率等参数一致
- 检查FreeModbus初始化参数
问题2:读取数据全为0
- 确认AHT20初始化成功
- 检查I2C上拉电阻(通常需要4.7kΩ)
- 验证传感器供电电压(3.3V)
问题3:通信不稳定
- 降低波特率测试(如改为9600)
- 检查线路长度(RS485建议不超过1200米)
- 添加终端电阻(120Ω)
7. 性能优化与扩展
7.1 低功耗优化策略
对于电池供电场景,可实施以下优化:
- 采用间歇工作模式,每5分钟唤醒一次
- 关闭不必要的外设时钟
- 使用STOP模式降低待机功耗
- 优化FreeModbus响应超时(缩短至100ms)
7.2 多传感器扩展
通过I2C总线可扩展多个传感器:
硬件修改
- 为每个传感器分配独立地址
- 增加I2C缓冲器(如PCA9548A)扩展通道
软件修改
- 扩展寄存器映射范围
- 实现传感器轮询机制
#define SENSOR_NUM 3 uint8_t sensorAddr[SENSOR_NUM] = {0x38, 0x39, 0x3A}; for(int i=0; i<SENSOR_NUM; i++) { AHT20_SetAddress(sensorAddr[i]); AHT20_Read_CTdata(&ctData[i][0]); }7.3 无线传输扩展
通过串口连接无线模块(如LoRa、NB-IoT):
硬件连接
- 无线模块的RX/TX连接STM32的USART2
- 共地连接,注意电平匹配
协议适配
- 保持Modbus RTU协议不变
- 增加无线模块的AT指令控制
void SendViaLoRa(uint8_t *data, uint16_t len) { HAL_UART_Transmit(&huart2, "AT+SEND=", 8, 100); for(int i=0; i<len; i++) { char hex[3]; sprintf(hex, "%02X", data[i]); HAL_UART_Transmit(&huart2, (uint8_t*)hex, 2, 100); } HAL_UART_Transmit(&huart2, "\r\n", 2, 100); }在实际项目中,这套方案已经成功应用于智能农业大棚监测系统,连续运行6个月无故障。一个实用的经验是:在AHT20数据读取之间至少保持1秒间隔,过于频繁的读取会导致传感器内部温升影响精度。
