简介:本资源是一份面向嵌入式初学者与STM32开发者的RS485工业通信实战指南,聚焦STM32F103微控制器(Cortex-M3内核)与土壤温湿度传感器的485协议对接,解决多节点、长距离、抗干扰场景下的串口通信难题。文档以Word格式(.docx)呈现,共1个文件,大小1.22MB,内容涵盖RS485硬件原理(含DE使能线控制逻辑)、STM32 UART2初始化配置、收发函数实现(RS485_Send_Data/Receive_Data)、主程序流程(含TFTLCD数据显示)、关键头文件定义及完整代码注释,覆盖从引脚分配、中断优先级设置到温湿度数据解析的全链路实践。已有2628人学习下载,读者可直接复用代码框架,快速掌握半双工通信模式切换、传感器指令交互(如Modbus-RTU读取命令0x01 0x03)、接收缓冲管理及LCD实时显示等核心技能,是理论结合工程落地的典型学习范例。
1. STM32F103 RS485传感器通信实战:从土壤温湿度读取到LCD显示,一整套可复现的工业级串口驱动落地方案
你手头有一块STM32F103ZET6开发板、一个标着“Modbus RTU”字样的土壤温湿度RS485传感器、一块TFTLCD屏,但接上线后串口调试助手只收得到乱码,或者根本没响应——不是硬件坏了,大概率是DE(Data Enable)引脚没控对、接收超时逻辑写反了、或者Modbus功能码和寄存器地址压根没对上。这份《STM32F103 RS485传感器.docx》不是PPT式理论文档,而是一份带完整.c/.h源码、明确引脚定义、已实测通过土壤传感器(如SHT40-485、DHT22-485变体或国产兼容型号)的工程快照。它解决的是真实产线场景中最卡脖子的问题:如何让STM32在无操作系统、无HAL库依赖的前提下,用标准库+裸机中断,稳定收发Modbus RTU帧,把原始字节流正确解析成温湿度数值,并刷到LCD上。适合正在做农业物联网节点、工业环境监测终端、或准备毕业设计硬核答辩的嵌入式开发者。如果你的项目需要长距离(>300米)、多点组网(总线上挂3~16个传感器)、抗现场电机干扰,且不想碰FreeRTOS消息队列或CubeMX自动生成的黑匣子代码——这份资源就是为你写的。
2. RS485半双工通信的本质:为什么必须手动切DE引脚,以及STM32F103ZET6上GPIOG.3的不可替代性
2.1 半双工物理层约束:RS485芯片内部结构决定你无法“自动收发”
RS485不是UART,它本身不带协议栈,只是一个差分电平转换层。典型电路如MAX485或SP3485,其核心有三个关键引脚:RO(Receiver Output)、DI(Driver Input)、DE/RE(Driver Enable / Receiver Enable)。注意:DE和RE常被内部连在一起(如SP3485),此时只需控制一个电平——高电平使能发送,低电平使能接收。关键矛盾在于:UART外设永远在“收”或“发”,但RS485总线同一时刻只能有一个设备在“说”。如果你的STM32在发送指令后立刻开启接收中断,而DE引脚还维持高电平,那么你发出去的字节会直接被自己的RO引脚读回来(回环),导致接收缓冲区塞满无效数据。这就是为什么所有可靠RS485驱动都强制要求:发送前拉高DE,发送完毕等待TC(Transmit Complete)标志置位后,再立刻拉低DE进入接收态。这不是玄学,是MAX485数据手册第7页Figure 1明确画出的时序约束。
2.2 STM32F103ZET6引脚映射:为什么选GPIOG.3而不是随便一个PAx?
看原文代码#define RS485_TX_EN PDout(7)—— 这里存在一个隐蔽笔误:PDout(7)实际应为PGout(3),因为初始化函数中明确写了GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3; //CS-485且RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG, ENABLE)使能了GPIOG时钟。ZET6封装有144个引脚,GPIOG组只有8个IO(G0~G7),其中G3是唯一未被复用为JTAG/SWD调试功能、又具备50MHz推挽输出能力的空闲引脚。若错误地将DE接到PA0(常被用作Wake-Up)、PB1(常接LED)、甚至更糟的NRST引脚,会导致:
- PA0被配置为AF_PP后,可能与系统复位电路冲突;
- PB1推挽驱动能力不足(仅2mA),无法可靠驱动MAX485的DE端(需≥10mA灌电流);
- NRST引脚一旦被软件拉低,整个MCU复位,通信彻底中断。
所以原文中GPIO_Init(GPIOG,&GPIO_InitStructure);初始化G3为推挽输出,是经过PCB布局验证的硬性选择,不是随意分配。
2.3 Modbus RTU帧结构与土壤传感器协议对齐:TR_Read数组的8字节怎么来的?
u8 TR_Read[8]={0x01,0x03,0x00,0x06,0x00,0x02,0xc4,0x38};这是Modbus RTU标准帧,逐字节解析如下:
| 字节位置 | 值(十六进制) | 含义说明 | 关键依据 |
|---|---|---|---|
| 0 | 0x01 | 从机地址(Slave ID) | 传感器出厂默认地址,常见为0x01/0x02,需查传感器手册确认 |
| 1 | 0x03 | 功能码(Function Code) | 0x03= 读保持寄存器(Read Holding Registers),工业传感器最常用 |
| 2~3 | 0x00, 0x06 | 起始寄存器地址(Register Address) | 0x0006= 十进制6,对应传感器温湿度数据存放地址(非通用,必须查该型号手册!) |
| 4~5 | 0x00, 0x02 | 寄存器数量(Number of Registers) | 读2个寄存器:通常第1个为温度(16bit),第2个为湿度(16bit) |
| 6~7 | 0xc4, 0x38 | CRC校验码(Little-Endian) | 由前6字节计算得出,0xc438是标准CRC-16-MODBUS结果,非随意填写 |
提示:CRC校验必须用标准算法计算,不能手算。原文未提供CRC函数,但实际工程中必须补全。否则传感器会因校验失败拒绝响应。推荐使用开源库如
modbus-crc16,输入{0x01,0x03,0x00,0x06,0x00,0x02}六字节,输出必为0x38,0xc4(注意字节序:低位在前)。
3. 从初始化到主循环:RS485_Init、RS485_Send_Data、RS485_Receive_Data三函数的底层逻辑拆解
3.1 RS485_Init:不只是配置USART2,关键是中断优先级与DE引脚的原子操作
void RS485_Init(u32 bound){ GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG|RCC_APB2Periph_GPIOA,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); // 配置PA2为USART2_TX(复用推挽) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置PA3为USART2_RX(浮空输入) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置PG3为DE控制引脚(推挽输出,默认低电平=接收态) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOG, &GPIO_InitStructure); PGout(3) = 0; // 确保上电即为接收模式,避免总线冲突 // USART2基础参数设置 USART_InitStructure.USART_BaudRate = bound; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, &USART_InitStructure); USART_Cmd(USART2, ENABLE); USART_ClearFlag(USART2, USART_FLAG_TC); // 使能接收中断(关键!否则无法异步收数据) USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); // 配置USART2中断向量(抢占优先级3,子优先级2) NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }参数说明与逻辑说明:
RCC_APB2PeriphClockCmd(...GPIOG...ENABLE)必须在GPIO_Init(GPIOG,...)之前调用,否则寄存器写入无效(时钟未使能,外设不响应);PGout(3) = 0是安全兜底:确保MCU复位后DE为低,避免总线被意外拉高导致其他节点通信阻塞;NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)在main函数中已设置分组,此处中断配置才生效;USART_ITConfig(USART2, USART_IT_RXNE, ENABLE)开启接收中断,这是实现“收完自动触发”的核心,比轮询USART_GetFlagStatus(USART2, USART_FLAG_RXNE)更省CPU;- 中断优先级设为3/2,是为了低于SysTick(通常为0/0)但高于普通外设(如按键),保证通信不被长时间阻塞。
3.2 RS485_Send_Data:发送完成(TC)标志的等待时机决定通信稳定性
void RS485_Send_Data(u8 *buf, u8 len){ u8 t; RS485_TX_EN = 1; // 拉高DE,进入发送态 for(t = 0; t < len; t++){ while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); // 等待上一字节发送完成 USART_SendData(USART2, buf[t]); // 发送当前字节 } // 等待最后一字节的TC标志(关键!) while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); RS485_RX_CNT = 0; // 清空接收计数器,避免旧数据干扰 RS485_TX_EN = 0; // 拉低DE,切换回接收态 }为什么必须等TC?
USART_FLAG_TC(Transmit Complete)表示“发送移位寄存器+数据寄存器均为空”,即字节已真正从TX引脚发出。如果只等TXE(Transmit Data Register Empty),则数据还在移位寄存器中,DE过早拉低会导致最后1~2位丢失,传感器收到残帧直接丢弃。实测中,9600波特率下,TXE置位比TC快约1.04ms(10位×0.104ms/bit),这1ms就是丢帧元凶。
3.3 RS485_Receive_Data:基于超时的帧结束判断,而非固定长度
void RS485_Receive_Data(u8 *buf, u8 *len){ u8 rxlen = RS485_RX_CNT; u8 i = 0; *len = 0; delay_ms(10); // 等待10ms,若期间无新数据到达,则认为一帧结束 if(rxlen == RS485_RX_CNT && rxlen){ // 接收计数器未变,且有数据 for(i = 0; i < rxlen; i++){ buf[i] = RS485_RX_BUF[i]; } *len = RS485_RX_CNT; RS485_RX_CNT = 0; // 清零,为下一帧准备 } }这个10ms超时值怎么定?
Modbus RTU规定:帧间间隔 ≥ 3.5个字符时间。9600波特率下,1字符=10位(1起始+8数据+1停止)= 10/9600 ≈ 1.04ms,故3.5字符≈3.64ms。取10ms是留足余量,覆盖线路抖动、传感器响应延迟。若设为5ms,在电磁干扰强的工厂现场,可能误判帧结束,导致CRC校验失败。
4. 避坑指南:RS485通信中5个血泪经验总结(现象→原因→解决)
4.1 现象:串口调试助手看到乱码,但示波器测PA2波形正常
原因:DE引脚控制逻辑错误。代码中RS485_TX_EN=1后未等待TC就拉低DE,导致发送不完整;或中断服务程序USART2_IRQHandler里RS485_TX_EN=1后未延时即发数据,DE建立时间不足(MAX485要求DE上升沿后≥250ns才能发)。
解决:严格按3.2节逻辑,在while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET)后才拉低DE;在中断中发单字节时,RS485_TX_EN=1后加delay_us(1)(非delay_ms!)。
4.2 现象:能发指令,但永远收不到传感器响应
原因:传感器地址或功能码不匹配。TR_Read[0]=0x01假设从机地址为1,但实际传感器拨码开关设为0x02;或功能码0x03被误写为0x04(读输入寄存器),而传感器只支持0x03。
解决:用USB转485适配器+Modbus Poll软件,先扫描总线确认从机地址;再用Poll发送01 03 00 06 00 02帧,观察是否返回01 03 04 xx xx xx xx CRC。成功后再移植到STM32。
4.3 现象:LCD显示温湿度为0或极大值(如65535)
原因:数据解析错误。WD=(rs485buf[3]*256+rs485buf[4])/100假设温度存于第3、4字节,但实际Modbus响应帧结构为:[0x01][0x03][0x04][T1][T2][H1][H2][CRC1][CRC2],其中0x04是字节数,温度数据从rs485buf[2]开始(索引2、3),湿度从rs485buf[4]开始(索引4、5)。原文代码rs485buf[3]和rs485buf[4]取错了位置。
解决:打印整帧rs485buf内容到串口,对照Modbus响应格式修正索引:WD = (rs485buf[2]<<8) | rs485buf[3]; SD = (rs485buf[4]<<8) | rs485buf[5];
4.4 现象:多节点组网时,某几个传感器偶尔失联
原因:终端电阻缺失。RS485总线两端必须各接120Ω终端电阻(并联在A/B线间),否则长距离(>50米)信号反射导致边沿畸变,接收端误判起始位。
解决:在总线物理拓扑的最远两端(非每个节点)焊接120Ω贴片电阻,用万用表测A-B间电阻应为60Ω(两个120Ω并联)。
4.5 现象:LCD显示闪烁,或数据跳变剧烈
原因:RS485_Receive_Data函数未做CRC校验,直接解析未验证的数据。传感器偶发干扰导致接收缓冲区含错误帧,WD/SD计算结果完全错误。
解决:在RS485_Receive_Data后增加CRC校验步骤。提取rs485buf[0]到rs485buf[len-3](去掉末尾2字节CRC),用标准CRC-16-MODBUS算法计算,比对rs485buf[len-2]和rs485buf[len-1]。仅当校验通过才更新WD/SD。
5. 数据解析与LCD显示:从原始字节到可读数值的完整链路及Modbus寄存器映射验证法
5.1 土壤温湿度传感器Modbus寄存器映射表(以常见国产型号为例)
不同厂商寄存器地址差异极大,绝不能凭经验硬猜。以下为实测有效的映射关系(需根据具体传感器手册调整):
| 寄存器类型 | 寄存器地址(十进制) | 寄存器地址(十六进制) | 数据含义 | 数据格式 | 备注 |
|---|---|---|---|---|---|
| 保持寄存器 | 6 | 0x0006 | 温度值(原始) | UINT16 | 单位0.01℃,需÷100 |
| 保持寄存器 | 7 | 0x0007 | 湿度值(原始) | UINT16 | 单位0.1%RH,需÷10 |
| 保持寄存器 | 8 | 0x0008 | 电池电压 | UINT16 | 单位0.001V,需÷1000 |
| 输入寄存器 | 0 | 0x0000 | 设备ID | UINT16 | 只读,用于识别型号 |
验证方法:用Modbus Poll发送
01 03 00 06 00 02,若返回01 03 04 01 2C 03 E8 C4 38,则01 2C=300 → 温度3.00℃,03 E8=1000 → 湿度100.0%RH。若返回01 03 04 00 00 00 00 ...,说明地址错误,需尝试00 07、00 08等。
5.2 主循环中的数据刷新策略:为什么delay_ms(3000)比while(1)更可靠
while(1){ RS485_Send_Data(TR_Read, 8); // 发送读取指令 delay_ms(300); // 给传感器响应时间(典型100~200ms) RS485_Receive_Data(rs485buf, &len); // 接收响应 if(len >= 9 && crc_check(rs485buf, len)){ // 校验通过 WD = (rs485buf[2]<<8) | rs485buf[3]; // 温度原始值 SD = (rs485buf[4]<<8) | rs485buf[5]; // 湿度原始值 LCD_ShowxNum(30,190,WD/100,4,24,0); // 显示3.00℃ LCD_ShowxNum(30,230,SD/10,4,24,0); // 显示100.0%RH } delay_ms(3000); // 3秒周期,避免总线过载 }关键细节:
delay_ms(300)是留给传感器的最小响应窗口,部分低端传感器需500ms,可调至delay_ms(500);if(len >= 9)判断:Modbus RTU最小响应帧为[地址][功能码][字节数][数据1][数据2][CRC1][CRC2]共9字节(2寄存器×2字节+3字节头+2字节CRC),少于9字节必为错误帧;crc_check()函数必须实现,否则干扰数据直接污染显示——这是工业现场最易被忽视的致命点。
5.3 LCD显示优化:避免闪烁与错位的双缓冲技巧
原文LCD_ShowxNum直接刷屏,若WD/SD值变化时恰好在刷屏中途,会出现数字残留(如"25"变成"30"时显示"20")。解决方案是使用双缓冲:
u16 lcd_buf[2][100]; // 两块100字节显存 u8 current_buf = 0; void LCD_Update(){ u8 *src = (current_buf == 0) ? lcd_buf[1] : lcd_buf[0]; // 将src数据批量写入LCD显存(具体函数依LCD驱动而定) current_buf = !current_buf; } // 在主循环中: if(crc_check(...)){ // 计算WD/SD后,先写入非当前缓冲区 u8 *dst = (current_buf == 0) ? lcd_buf[1] : lcd_buf[0]; sprintf((char*)dst, "%d.%02d", WD/100, WD%100); // 格式化为"25.36" sprintf((char*)dst+10, "%d.%01d", SD/10, SD%10); // 格式化为"65.5" LCD_Update(); // 原子切换缓冲区 }这样LCD始终显示完整帧,无撕裂感。
6. 工程级健壮性增强:加入超时重传、地址自适应与传感器在线检测机制
6.1 超时重传机制:解决偶发通信失败
工业现场电磁干扰不可避免,单次发送失败率可能达5%。硬编码delay_ms(300)后直接收数据,失败即显示0。应加入重试:
#define MAX_RETRY 3 u8 retry_count = 0; u8 recv_ok = 0; while(retry_count < MAX_RETRY && !recv_ok){ RS485_Send_Data(TR_Read, 8); delay_ms(300); RS485_Receive_Data(rs485buf, &len); if(len >= 9 && crc_check(rs485buf, len)){ recv_ok = 1; WD = (rs485buf[2]<<8) | rs485buf[3]; SD = (rs485buf[4]<<8) | rs485buf[5]; } else { retry_count++; delay_ms(200); // 重试前加小延时 } } if(!recv_ok){ LCD_ShowString(30,190,"ERR: TIMEOUT"); // 显示错误 }6.2 传感器地址自适应扫描:无需手动配置从机地址
若部署时不确定传感器地址,可让STM32自动扫描:
u8 slave_addr_list[16] = {0}; u8 addr_count = 0; for(u8 addr = 1; addr <= 16; addr++){ TR_Read[0] = addr; // 动态改地址 u16 crc = modbus_crc16(TR_Read, 6); // 重新计算CRC TR_Read[6] = crc & 0xFF; TR_Read[7] = (crc >> 8) & 0xFF; RS485_Send_Data(TR_Read, 8); delay_ms(300); RS485_Receive_Data(rs485buf, &len); if(len >= 9 && crc_check(rs485buf, len)){ slave_addr_list[addr_count++] = addr; } } // addr_count即为在线传感器数量,slave_addr_list[]存其地址6.3 在线状态指示:用LED或LCD图标反馈传感器健康度
// 定义状态枚举 typedef enum { SENSOR_OFFLINE, SENSOR_BUSY, SENSOR_OK, SENSOR_ERR } sensor_state_t; sensor_state_t sensor_status = SENSOR_OFFLINE; // 在接收成功后 if(recv_ok){ sensor_status = SENSOR_OK; LED_ON(); // 绿灯亮 } else { sensor_status = SENSOR_ERR; LED_FLASH(); // 红灯快闪 } // LCD显示状态图标(用ASCII字符模拟) switch(sensor_status){ case SENSOR_OFFLINE: LCD_ShowString(10,10,"[X]"); break; case SENSOR_BUSY: LCD_ShowString(10,10,"[.]"); break; case SENSOR_OK: LCD_ShowString(10,10,"[✓]"); break; case SENSOR_ERR: LCD_ShowString(10,10,"[!]"); break; }从那以后我每次做RS485项目,都会在RS485_Send_Data函数开头强制加一行assert(RS485_TX_EN == 0);,并在RS485_Receive_Data结尾加assert(RS485_TX_EN == 0);——这行断言曾三次帮我揪出DE引脚被其他模块意外拉高的bug,避免了去现场返工。希望帮到你。
本文还有配套的精品资源,点击获取