简介面向STM32与嵌入式开发者的四合一土壤检测工程资源包源自智慧温室项目基于STM32F103C8T6设计主要解决土壤PH值及氮磷钾含量的实时采集与显示问题。系统通过RS485接口向综合土壤传感器发送问询帧接收后解析应答帧再经数据换算得到对应数值RS485总线传输距离远、支持多点通信适合农业环境分布式部署。压缩包共8.69MB包含232个文件核心代码以30个C源文件和36个H头文件为主并配有启动文件、Keil工程配置、链接脚本以及axf/hex烧录文件便于重新编译和直接下载编译生成的中间文件与依赖分析文件较为齐全有助于深入理解工程构建与排错。已有2143人浏览学习。借助该资源可系统掌握RS485总线通信、土壤传感器协议解析、OLED显示驱动及标准外设库中定时器、ADC、USART、I2C、CAN等模块的工程用法目录结构清晰适合嵌入式初级和中级开发者作为实战参考、课程设计或二次开发基础。1. 先算一笔账成品土壤检测仪三千块这套RS485方案一百五差在哪某农业基地要给五个大棚配土壤速测仪问了一圈成品设备单台带探头的PH、氮磷钾检测仪报价三四千数据还导不出来。基于STM32F103C8T6单片机使用RS485传感器检测土壤PH、氮磷钾并显示在OLED显示屏这套方案单点硬件成本能压到一百五十块上下数据格式、显示内容、后续联网全都自己说了算。链路并不复杂土壤传感器把PH、氮、磷、钾转成Modbus RTU寄存器值通过RS485差分总线送到STM32F103C8T6的串口单片机解析后驱动OLED显示。一根双绞线最远能跑一千米一条总线最多挂三十二个设备以后想加土壤湿度、温度探头只是改改从机地址的事。适合两类人一类是给农业物联网做数据底座的开发者这套通信代码可以原样搬进自己的采集网关另一类是手里有STM32最小系统板、想认真弄懂Modbus RTU收发时序的入门者。读懂这篇你会知道从硬件接线到协议解析的全部细节还有五个最常见的翻车点在哪、怎么绕开。2. 系统架构与硬件选型RS485、探头、供电这条链路怎么搭2.1 RS485比模拟量强在哪差分传输与Modbus RTU土壤探头的输出方式常见三种模拟量4-20mA或0-5V、RS485、少数低成本款直接出I2C。模拟量的坑在于一根信号线只能带一个参数测PH、氮、磷、钾四个参数就要四对信号线加四路ADC大棚里水泵、卷帘机、补光灯一开市电干扰顺着线窜进单片机数值当场乱跳。RS485是差分传输两根线A和B互为参考干扰同时作用于两根线上形成共模噪声接收端一相减就抵消了所以百米级布线依旧稳定。Modbus RTU是RS485线上最常见的应用层协议。主机向某个地址发一帧命令从机回一帧数据帧格式固定拆成地址、功能码、数据、CRC校验四段。土壤传感器厂家通常把PH、氮、磷、钾分别映射到保持寄存器里我们用0x03这个读保持寄存器功能码去取。协议完全公开排查问题时逻辑很清楚命令不对就回看帧字节地址不对就回看地址位设备不响应就回看波特率和接线没有多少玄学成分。选型上有一条建议买探头时选产品页明确写着Modbus RTU协议的最好附寄存器地址表。我遇到过只标RS485输出但协议含糊的探头买回来只能自己一帧一帧逆向浪费一下午。价格上多花二十块能省三倍调试时间这笔账值得。2.2 器件清单与引脚接线七个器件一张表整套方案需要七样东西先列清单器件参考型号数量作用主控STM32F103C8T6最小系统板1解析协议、驱动显示RS485收发器SP3485或MAX34851TTL电平与差分信号互转土壤探头7合1 RS485型1检测PH、氮磷钾、温湿度电源适配器24V 2A1给探头和降压模块供电降压模块MP1584124V降到5V再经板载LDO到3.3VOLEDSSD1306 0.96寸 I2C1显示检测数值终端电阻120Ω 1/4W2长线布线时匹配阻抗STM32F103C8T6的USART2接RS485I2C1接OLED方向控制随便挑一个空闲GPIO。收发器选3.3V供电的SP3485和单片机同电压域省掉电平转换。接线关系如下STM32引脚器件引脚说明PA2SP3485 DI发送数据PA3SP3485 RO接收数据PA1SP3485 DE/RE并接高电平发送低电平接收PB6OLED SCLI2C时钟PB7OLED SDAI2C数据总线ASP3485 A、探头A差分正总线BSP3485 B、探头B-差分负公共地全部GND必须共地DE和RE在SP3485上是两个独立使能脚一个管发送一个管接收。常规做法是把它们短接后共用一个GPIO切换输出高电平进入发送态低电平进入接收态比软件里分别控制两个脚省事得多。A和B注意探头标注有的厂家写A、B-有的写485A、485B按同名牌接同一根线处理。接反的典型症状是主机发命令后完全无响应后面避坑章节还会细说。2.3 供电与共地24V探头和3.3V单片机怎么和平相处探头标称工作电压12V或24V功耗名义上只有几十毫安但上电瞬间的浪涌电流可能冲到几百毫安。如果跟单片机共用一个3.3V稳压器这一下浪涌直接让OLED闪屏严重的整板复位。我一般的接法是24V适配器出来先到探头同时并一路进MP1584降压到5V再进最小系统板上的AMS1117降到3.3V给STM32、SP3485、OLED供电。关键动作是所有GND连到一起。RS485是共地通信探头地和板子地电位不一致时轻则乱码重则烧坏收发器。上电前用万用表电阻档量探头GND和板子GND阻值接近0Ω才是真共地。防浪涌的做法是探头电源入口并联一个1000uF电解电容SP3485的VCC引脚放一颗100nF去耦电容。总线布线用双绞屏蔽线屏蔽层单端接地大棚电机启动时数据照常跑。这套供电结构沿用到现在没再出现过上电复位的问题。3. 打通Modbus RTU链路串口初始化、命令帧与超时解析3.1 Modbus RTU帧结构请求8字节、响应N字节主机读取探头数据的命令帧固定8字节例如读地址0x01从寄存器0x0000开始取4个寄存器01 03 00 00 00 04 44 09拆开看01是从机地址03是功能码读保持寄存器00 00是起始寄存器地址00 04是要读的寄存器数量44 09是CRC16校验值低字节在前。响应帧则是地址、功能码、数据字节数、数据区、CRC数据区每个寄存器占2字节且高字节在前所以读4个寄存器响应是13字节01 03 08 8字节数据 2字节CRC。CRC16在Modbus里是绕不开的。它初始值0xFFFF多项式0xA001按位处理。写一个通用函数命令帧和响应帧都能用它校验// Modbus RTU CRC16 计算输入数据缓冲区和长度返回16位校验值 uint16_t modbus_crc16(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ buf[i]; // 先和当前字节异或 for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; // Modbus 多项式 else crc 1; } } return crc; // 字节序低字节在前 }逻辑说明每个字节进来先与CRC低字节异或然后右移8次每次判断最低位是否为1决定是否异或多项式。返回的crc在拼装命令帧时先放低8位再放高8位这是Modbus RTU的固定字节序放反了探头会直接丢弃命令帧。参数上只要注意buf长度别传错命令帧长度6、响应帧长度是3 数据字节数。3.2 串口和RS485方向切换USART2初始化的四个要点USART2挂在APB1总线上时钟36MHz配4800波特率时分频结果正好是整数误差为0。多数土壤探头出厂默认4800少数默认9600以手册为准。初始化代码void rs485_usart_init(void) { GPIO_InitTypeDef g; USART_InitTypeDef u; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // PA2 USART2_TX复用推挽输出 g.GPIO_Pin GPIO_Pin_2; g.GPIO_Mode GPIO_Mode_AF_PP; g.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, g); // PA3 USART2_RX浮空输入 g.GPIO_Pin GPIO_Pin_3; g.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, g); // PA1 控制 SP3485 的 DE/RE短接后的公共端推挽输出 g.GPIO_Pin GPIO_Pin_1; g.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, g); GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET); // 默认接收态 u.USART_BaudRate 4800; // 多数探头默认4800 u.USART_WordLength USART_WordLength_8b; // 8数据位 u.USART_StopBits USART_StopBits_1; // 1停止位 u.USART_Parity USART_Parity_No; // 无校验 u.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART2, u); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); // 接收中断 USART_Cmd(USART2, ENABLE); }参数说明8数据位、1停止位、无校验是Modbus RTU默认参数组合千万别开成9位带校验位探头不会理你。波特率注意和探头手册对上4800和9600混用是超时重灾区。方向切换函数是这个方案里最见功力的地方一句话总结发送前拉高DE等所有字节从移位寄存器里真正走完再切回低电平接收。只查发送数据寄存器空TXE是不够的字节可能还在移位寄存器里这时候切方向会把最后一个字节截断void rs485_set_dir(uint8_t tx) { if (tx) { GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET); // DE1 进入发送态 delay_us(100); // 给收发器稳定时间 } else { // 必须等最后一个字节完全发完查TC标志而不是TXE while (USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET); delay_us(100); // 总线释放让从机接管 GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET); // RE0 回到接收态 } }这个100us延时的含义是给收发器和总线一点缓冲时间太快切回接收态探头回帧的前几个字节就可能被漏掉。RS485是半双工同一时刻总线上只能有一个设备在发方向切换的时机决定了通信稳定性的上限。3.3 读取PH、氮磷钾的完整时序从发命令到超时校验有了CRC和方向切换剩下的就是拼命令、发出去、等响应、校验四条路。这里用一个状态简单的串口中断接收配合超时轮询代码如下uint8_t rx_buf[32]; volatile uint16_t rx_cnt 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { uint8_t b USART_ReceiveData(USART2); if (rx_cnt sizeof(rx_buf)) rx_buf[rx_cnt] b; // 中断里只收数据不做解析 } } // 读取从机 addr 从 reg 开始的 count 个保持寄存器结果存 data uint16_t rs485_read(uint8_t addr, uint16_t reg, uint16_t count, uint8_t *data, uint16_t timeout_ms) { uint8_t cmd[8]; uint16_t crc, expect_len; uint32_t tick 0; expect_len 3 count * 2 2; // 地址功能码字节数数据CRC cmd[0] addr; // 从机地址默认0x01 cmd[1] 0x03; // 读保持寄存器功能码 cmd[2] reg 8; // 起始寄存器高字节 cmd[3] reg 0xFF; cmd[4] count 8; // 寄存器数量高字节 cmd[5] count 0xFF; crc modbus_crc16(cmd, 6); cmd[6] crc 0xFF; // CRC 低字节在前 cmd[7] crc 8; rx_cnt 0; // 清零接收计数 rs485_set_dir(1); // 切发送态 for (uint8_t i 0; i 8; i) { USART_SendData(USART2, cmd[i]); while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); } rs485_set_dir(0); // 切回接收态 while (tick timeout_ms) { if (rx_cnt expect_len) break; // 帧收齐 delay_ms(1); tick; } if (rx_cnt expect_len) return 0; // 超时返回失败 // 响应帧CRC校验防止总线干扰导致数据错位 crc modbus_crc16(rx_buf, expect_len - 2); if ((rx_buf[expect_len - 2] ! (crc 0xFF)) || (rx_buf[expect_len - 1] ! (crc 8))) return 0; memcpy(data, rx_buf[3], count * 2); // 跳过地址、功能码、字节数 return count * 2; }逻辑说明函数先按帧格式拼出8字节命令计算CRC后发送然后切回接收态等待。等待用了简单轮询加1ms步进超时时间由调用方传入。响应一收齐马上算CRC这一步很多人省略但农田环境总线上干扰多偶尔错一个字节没有CRC校验就会把垃圾数据当真值显示在OLED上。数据区从rx_buf偏移3开始拷贝因为前3字节是地址、功能码、字节数。4. 数据换算与OLED显示从寄存器值到屏幕上的PH、氮磷钾4.1 寄存器地址与换算公式以典型7合1探头为例不同厂家对寄存器地址的分配差异很大这一点先有心理准备。我手上这支7合1探头的典型映射如下寄存器地址参数换算方式量程示例0x0000土壤PH原始值×0.10.0~14.00x0001氮含量原始值×0.10~199.9 mg/kg0x0002磷含量原始值×0.10~299.9 mg/kg0x0003钾含量原始值×0.10~299.9 mg/kg0x0004土壤湿度原始值×0.10~100%0x0005土壤温度原始值×0.1-40~80℃如果手上探头的手册遗失了有一个逆向办法把探头分别插进酸性液和清水里用串口助手连续读0x0000到0x000F这16个寄存器看哪个寄存器的值跟着变化变化值乘以0.1正好等于试纸读数那就是PH。氮磷钾同样操作用不同浓度的营养液去验证。这个方法十分钟能摸清一张寄存器表比问卖家客服可靠。解析代码直接把响应帧里的数据区拆成四个有符号短整数高字节在前typedef struct { int16_t ph_raw; // PH 寄存器原始值 int16_t n_raw; // 氮寄存器原始值 int16_t p_raw; // 磷寄存器原始值 int16_t k_raw; // 钾寄存器原始值 } SoilData; void soil_parse(SoilData *d, uint8_t *buf) { // buf 是响应帧的数据区第0~1字节约PH2~3节约氮4~5节约磷6~7节约钾 d-ph_raw (buf[0] 8) | buf[1]; d-n_raw (buf[2] 8) | buf[3]; d-p_raw (buf[4] 8) | buf[5]; d-k_raw (buf[6] 8) | buf[7]; }注意这里的换算统一是原值除以10所以显示时拆整数和小数即可不需要浮点。如果探头手册写明某个参数是1倍率那直接显示原值如果是0.01倍率显示时除以100。这个倍率信息必须看手册或者验证出来是数据准确性的第一道门槛。4.2 OLED显示用整数拆分代替浮点sprintfSTM32F103C8T6没有硬件浮点单元C标准库里sprintf带%f会把浮点格式化代码整个拉进固件占用好几KB Flash不说运行时还慢OLED刷新肉眼可见地卡。养成习惯数值拆分后用%d打印。void oled_show_soil(SoilData *d) { char line[24]; // PH原始值55显示为5.5拆整数和小数避开%f sprintf(line, PH: %d.%d, d-ph_raw / 10, d-ph_raw % 10); oled_show_string(0, 0, line); // 氮、磷、钾同样拆开 sprintf(line, N: %d.%d mg/kg, d-n_raw / 10, d-n_raw % 10); oled_show_string(0, 2, line); sprintf(line, P: %d.%d mg/kg, d-p_raw / 10, d-p_raw % 10); oled_show_string(0, 4, line); sprintf(line, K: %d.%d mg/kg, d-k_raw / 10, d-k_raw % 10); oled_show_string(0, 6, line); }oled_show_string是SSD1306驱动里的字符串显示函数用u8g2库或者自己写的点阵驱动都行重点不在驱动来源而在刷新策略。OLED是128x64像素四行数据每行16像素高度刷新时只更新变化行对应的页不要每次整屏清空再重画否则屏幕一直闪大棚里光线强的时候根本看不清。刷屏节奏上还有一个细节SSD1306内部带显存操作是先写显存再一次性刷新到屏幕。如果驱动是写一个字节刷一个字节这种低级写法必须改成先写显存缓冲再统一更新否则四行数据刷完要几百毫秒。4.3 主循环状态机2秒一轮的轮询与刷新节奏主循环不需要实时操作系统一个简单状态机就能把流程管好。读数据、转换、显示三步串行中间插延时让探头从容响应int main(void) { uint8_t data[16]; SoilData soil; sys_clock_init(); // 72MHz主频 rs485_usart_init(); // 串口RS485方向脚初始化 oled_init(); // SSD1306初始化 while (1) { // 读地址0x01从寄存器0x0000开始的4个寄存器超时300ms if (rs485_read(0x01, 0x0000, 4, data, 300) 8) { soil_parse(soil, data); // 原始值解析 oled_show_soil(soil); // 拆分整数后显示 } else { oled_show_string(0, 0, Sensor No Resp); // 超时提示 } delay_ms(2000); // 2秒一轮土壤参数变化慢无需更快 } }参数说明读4个寄存器对应PH、氮、磷、钾返回8字节数据。超时300ms是因为7合1探头响应时间通常50到200ms留足余量。轮询周期2秒是折中值——OLED显示需要肉眼可读的刷新率而土壤参数本身不会秒级突变2秒一轮既省功耗又不闪屏。想接湿度温度把count改成6再扩展显示即可状态机框架不用动。5. RS485土壤检测避坑五个翻车现场与对应的解决手段这套方案做出来容易稳定跑起来难。下面按实际调试顺序整理五个高频问题每条都是现象、原因、解决三层。5.1 请求超时串口助手有响应STM32却收不到现象用USB转485工具在电脑串口助手里发01 03 00 00 00 04 44 09探头正常回13个字节换成STM32发同样的命令却一直超时。原因DE/RE方向控制没拉起来。最常见是PA1没初始化成推挽输出或者DE和RE没有短接导致收发器永远停在静默态其次是A、B两根线在板子上接反了。解决先用示波器或万用表量SP3485的RO脚空闲时应为高电平说明处于接收态再量DI脚在发送命令期间是否有波形。如果RO一直低电平检查DE/RE是否被错误拉高。A、B接反对调即可。我调试时习惯在rs485_read开头加一句翻转LED肉眼确认程序确实执行到了发送动作。5.2 数据跳变同一个寄存器读出两种值现象连续读十次PH大部分是55pH 5.5偶尔蹦出571这种离谱值或者读出来的氮磷钾顺序错位。原因方向切换时机太早响应帧被截掉前几个字节后续所有字节整体前移解析时数据全错位。总线上没接终端电阻或者接了三个以上阻抗不匹配引起反射数据在长线上来回反弹。解决确认rs485_set_dir(0)分支里一定等了USART_FLAG_TC而不是TXETC才是移位寄存器彻底发完。收发切换之间保留至少100us缓冲。总线短于2米时可以不接终端电阻超过10米必须两端各接一个120Ω千万别接三个反射会更乱。5.3 显示No Resp手动复位又能成功几次现象上电后OLED显示No Resp按一下复位键第一次读通常成功第二次又失败循环往复。原因探头响应慢300ms超时不够或者上电瞬间探头还在初始化主循环立即发命令探头根本没准备好。还有一个隐蔽原因上一次轮询超时后响应帧尾部残留在接收缓冲里下一次读命令把残余字节当成响应开头长度永远对不上。解决上电后先延时3到5秒再发第一条命令把超时放宽到500ms每次发送命令前强制rx_cnt清零并清掉USART接收缓冲里的残留数据。轮询周期不要短于1秒Modbus RTU协议要求帧与帧之间有至少3.5个字符时间的空闲间隔连着发命令会让从机来不及处理。5.4 PH读数不准稳定但比试纸低0.5现象读数非常稳定重复性也好但和试纸、实验室结果系统性偏差永远低0.3到0.6。原因探头电极出厂后放置太久校准点漂移插入深度不够探针没有完全接触土壤土壤太干离子活度低测出来的是悬浮液浓度而不是田间真实值。解决按手册做两点校准多数探头支持把探针插入4.00和6.86标准缓冲液通过专用命令写入校准值测试前把土壤浇透探头全埋入土等待60秒以上让读数稳定干土测出来的数值只作趋势参考别当真值记录。校准是这类传感器的日常保养上电后连着稳定读数半小时再采数也是降低偏差的好习惯。5.5 传感器一上电整板复位OLED闪一下然后黑屏现象程序里只要执行到RS485发送OLED就闪、单片机复位或者探头一接上24V板子立刻重启。原因典型是供电问题。探头瞬态浪涌电流把共用的3.3V拉低到单片机复位阈值以下另一个可能是探头地和板子地没接共模电压灌进收发器把IO打坏。解决按第2.3节的方案分路供电——探头吃24V单片机走MP1584降压所有GND焊接在一起探头电源入口并联1000uF电解电容。如果复位只发生在RS485发送瞬间重点查SP3485的VCC对地有没有100nF去耦电容以及接地线是否足够粗。用示波器看3.3V波形发送瞬间跌落超过0.3V就要加电容。6. 进阶玩法一条总线上挂多个探头做轮询与数据校准6.1 多探头轮询改地址与帧间时序单点测完大棚里五块地要五个探头RS485总线此时才真正体现价值。做法把每支探头单独接上总线用串口助手发写寄存器命令把从机地址改成0x01、0x02、0x03改完断电重启生效再统一挂到总线上。代码里循环访问地址表uint8_t addr_list[] {0x01, 0x02, 0x03}; // 三支探头地址 for (uint8_t i 0; i 3; i) { if (rs485_read(addr_list[i], 0x0000, 4, data, 300) 8) { soil_parse(soil, data); oled_show_page(i, soil); // 按点位切换显示 } delay_ms(100); // 帧间空闲让出总线 }轮询的关键参数是间隔Modbus RTU要求帧间至少3.5个字符时间的静默4800波特率下约7ms这里留100ms既满足协议又给探头处理时间。探头改地址的操作比较敏感规则是一次只接一支改完验证再换下一支千万不要多支并联时广播改地址否则所有探头改到同一个地址后面全乱。6.2 数据可信度滤波、交叉验证与校准习惯传感器读回来的原始值不能直接信尤其氮磷钾这类易受土壤含水量影响的参数。我一般做两级处理连续采样5次去掉最大值和最小值剩余3次取平均再和实验室标准方法做交叉验证——同一块土样这边采一次数那边用试纸或标准液测一次偏差在允许范围内才把这个探头设为可信数据源。校准不是一劳永逸我给自己定的频率是每两个月校准一次PH氮磷钾探头每季度用标准液抽检一次。这套方案做到这里已经从一个单片机小项目变成了真正可用的农业监测节点。我做这类项目的第一习惯永远是先把探头接串口助手把寄存器表和倍率摸清楚再写单片机代码让MCU去猜一个从没见过的协议是最浪费时间的弯路。方向切换的时序、供电的隔离、响应的超时这三件事踩一遍就能记住一辈子。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?