STM32 Modbus RTU从机开发实战:核心实现与五大调试陷阱解析
知识点1【CRC校验】
CRC校验生成网址
CRC(循环冗余校验)在线计算_ip33.com
知识点2【代码演示】
代码书写思路
代码演示
main.c
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "rs485.h"int main(void)
{//优先级组的配置NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);RS485_GPIO_Init();USART1_Config(9600);USART2_Config(9600);printf("你好\\n");while(1){ Modbus_Init();}
}
rs485.h
#ifndef _RS485_H_
#define _RS485_H_
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "stdio.h"
#include "string.h"
//设备ID
#define SLAVE_ID 0xFE//串口相关宏
#define GPIO_USART1_2_TXRX GPIOA
#define PIN_USART1_TX GPIO_Pin_9
#define PIN_USART1_RX GPIO_Pin_10
#define PIN_USART2_TX GPIO_Pin_2
#define PIN_USART2_RX GPIO_Pin_3//RS485使能相关宏
#define GPIO_RS485_ENABLE GPIOD
#define PIN_RS485_ENABLE GPIO_Pin_7
#define RS485_DE() (GPIOD->ODR |= (0x01 << 7))
#define RS485_RE() (GPIOD->ODR &= ~(0x01 << 7))typedef struct{u8 recv_data[256];u8 send_data[256];u8 recv_size;u8 send_size;u8 flag; //数据接收完成标志位,1:接收完成,0:等待接收完成
}DATA_RS485;typedef struct{u16 recv_offset;u8 send_data[256];u16 recv_size;u8 send_size_byte;//用来计算总发送的字节数
}CMD_03;void USART1_Config(u16 baud);void USART2_Config(u16 baud);void RS485_GPIO_Init(void);void Modbus_Init(void);void CMD03_Fun(CMD_03 *data);void USART1_SendByte(u8 data);void USART2_SendByte(u8 data);void USART2_SendStr(u8 *data,u8 len);int fputc(int c,FILE *stream);uint16_t ModBus_CRC16(uint8_t *data, uint16_t length);
#endif
rs485.c
#include "rs485.h"DATA_RS485 data_rs485 = {0};
CMD_03 data_cmd03 = {0};//数据
u16 Server_data[] = {113,792,5564,56546,6546,5646,546,156,23,21};void USART1_Config(u16 baud)//PA9 TX PB10 RX
{GPIO_InitTypeDef GPIO_InitStruct;USART_InitTypeDef USART_InitStruct;//时钟配置RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//模式配置GPIO_StructInit(&GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin = PIN_USART1_TX;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin = PIN_USART1_RX;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA,&GPIO_InitStruct);//串口配置USART_StructInit(&USART_InitStruct);USART_InitStruct.USART_BaudRate = baud;USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;USART_InitStruct.USART_Parity = USART_Parity_No;USART_InitStruct.USART_StopBits = USART_StopBits_1;USART_InitStruct.USART_WordLength = USART_WordLength_8b;USART_Init(USART1,&USART_InitStruct);//串口使能USART_Cmd(USART1,ENABLE);printf("USART1 is ok!\\n");
}void USART2_Config(u16 baud)//PA2 TX PA3 RX
{GPIO_InitTypeDef GPIO_InitStruct;USART_InitTypeDef USART_InitStruct;NVIC_InitTypeDef NVIC_InitStruct;//时钟配置RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//模式配置GPIO_StructInit(&GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin = PIN_USART2_TX;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;GPIO_Init(GPIO_USART1_2_TXRX,&GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin = PIN_USART2_RX;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIO_USART1_2_TXRX,&GPIO_InitStruct);//串口配置USART_StructInit(&USART_InitStruct);USART_InitStruct.USART_BaudRate = baud;USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;USART_InitStruct.USART_Parity = USART_Parity_No;USART_InitStruct.USART_StopBits = USART_StopBits_1;USART_InitStruct.USART_WordLength = USART_WordLength_8b;USART_Init(USART2,&USART_InitStruct);//中断配置NVIC_InitStruct.NVIC_IRQChannel = USART2_IRQn;NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01;NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x01;NVIC_Init(&NVIC_InitStruct);//中断使能USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);USART_ITConfig(USART2,USART_IT_IDLE,ENABLE);//串口使能USART_Cmd(USART2,ENABLE);printf("USART2 is ok!\\n");
}void RS485_GPIO_Init(void)//PD7
{GPIO_InitTypeDef GPIO_InitStruct;//时钟配置RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,ENABLE);//模式配置GPIO_StructInit(&GPIO_InitStruct);GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;GPIO_InitStruct.GPIO_Pin = PIN_RS485_ENABLE;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_Init(GPIO_RS485_ENABLE,&GPIO_InitStruct);//接收使能RS485_RE();
}void Modbus_Init(void)
{int i = 0;u8 data_addr = 0;u8 data_fun = 0;u16 data_CRC = 0;u16 data_modbus_CRC = 0;//判断数据是否接收完成while(!data_rs485.flag);data_rs485.flag = 0;for(i = 0;i < data_rs485.recv_size;i++){printf("%02x ",data_rs485.recv_data[i]);}printf("\\r\\n");//接收到的数据解包//此处仅解析:校验位,地址码,功能码//FE 03 00 04 00 03 50 05(CRC)data_addr = data_rs485.recv_data[0];data_fun = data_rs485.recv_data[1];//data_rs485.recv_size - 2 ———— 低字节,data_rs485.recv_size - 2 ———— 高字节data_CRC = data_rs485.recv_data[data_rs485.recv_size - 2] | (data_rs485.recv_data[data_rs485.recv_size - 1] << 8);printf("data_CRC = %04x\\r\\n",data_CRC);data_modbus_CRC = ModBus_CRC16(data_rs485.recv_data,data_rs485.recv_size - 2);printf("data_modbus_CRC = %04x\\r\\n",data_modbus_CRC);//从机地址位if(data_addr != SLAVE_ID){printf("从机地址不符\\n");return;}//数据的正确性判断//校验位if(data_modbus_CRC != data_CRC){printf("校验位错误\\n");return;}//功能码判断switch(data_fun){case 0x01:break;case 0x02:break;case 0x03://FE 03 00 04 00 03 50 05(CRC)中的00 04 00 03 解包到 data_cmd03.recv_data//数据提取data_cmd03.recv_offset = (data_rs485.recv_data[2] << 8) | data_rs485.recv_data[3];data_cmd03.recv_size = (data_rs485.recv_data[4] << 8) | data_rs485.recv_data[5];CMD03_Fun(&data_cmd03);break;case 0x06:break;case 0x16:break;case 0x20:break;}//清空结构体,防止后续数据错误memset(&data_rs485,0,sizeof(data_rs485));memset(&data_cmd03,0,sizeof(data_cmd03));
}//功能码03的处理函数
void CMD03_Fun(CMD_03 *data)
{int i = 0;u16 modbus_CRC;//FE 03 00 04 00 03 50 05(CRC)//u16 Server_data[] = {113,792,5564,56546,6546,5646,546,156,23,21};//可知我们要提取的数据是:6546,5646,546if(data->recv_offset + data->recv_size > sizeof(data_cmd03)/sizeof(u16)){printf("请求大小错误\\n");return;}//发送数据组包//从机IDdata->send_data[data->send_size_byte++] = SLAVE_ID;//功能码data->send_data[data->send_size_byte++] = data_rs485.recv_data[1];//大小data->send_data[data->send_size_byte++] = data->recv_size * 2;//数据for(i = 0;i < data->recv_size;i++){data->send_data[data->send_size_byte++] = Server_data[i]/256;data->send_data[data->send_size_byte++] = Server_data[i]%256;}printf("%u\\n",data->send_size_byte);for(i = 0;i < data->send_size_byte;i++){printf("%02x ",data->send_data[i]);}printf("\\r\\n");//此时的data->send_size_byte刚好为要进行校验的总大小//校验位modbus_CRC = ModBus_CRC16(data->send_data,data->send_size_byte);printf("modbus_CRC = %x\\n",modbus_CRC);//校验位处理data->send_data[data->send_size_byte++] = modbus_CRC%256;data->send_data[data->send_size_byte++] = modbus_CRC/256;USART2_SendStr(data->send_data,data->send_size_byte);
}void USART1_SendByte(u8 data)
{USART1->DR = data;//while(!USART_GetFlagStatus(USART1,USART_FLAG_TC));while(!USART_GetFlagStatus(USART1,USART_FLAG_TXE));
}void USART2_SendByte(u8 data)
{USART2->DR = data;//while(!USART_GetFlagStatus(USART2,USART_FLAG_TC));while(!USART_GetFlagStatus(USART2,USART_FLAG_TXE));
}void USART2_SendStr(u8 *data,u8 len)
{int i = 0;RS485_DE();for(i = 0;i < len;i++){USART2_SendByte(data[i]);} while (!(USART2->SR & USART_FLAG_TC));RS485_RE();
}int fputc(int c,FILE *stream)
{USART1_SendByte((u8)c);return c;
}//CRC校验算法函数
uint16_t ModBus_CRC16(uint8_t *data, uint16_t length)
{uint16_t i;uint16_t crc_value = 0xffff;while (length--){crc_value ^= *data++;for(i = 0; i < 8; i++){if (crc_value & 0x0001 )crc_value = (crc_value >> 1) ^ 0xA001;elsecrc_value = crc_value >> 1;}}return crc_value;
}//USART2中断处理函数
void USART2_IRQHandler(void)
{//接收中断u8 data;if(USART_GetITStatus(USART2,USART_IT_RXNE)){USART_ClearITPendingBit(USART2,USART_IT_RXNE);//将Modbus数据帧写入 接收数组当中data = USART2->DR; //进行数据缓冲,防止数据覆盖data_rs485.recv_data[data_rs485.recv_size] = data;data_rs485.recv_size++;}//空闲中断if(USART_GetITStatus(USART2,USART_IT_IDLE)){USART2->SR;//接收的动作USART2->DR;//标志位置1data_rs485.flag = 1;}
}
代码运行结果展示
代码问题
1、TC和TXE的使用问题
现象:
如果使用注释内的TC,就会出现第一个字符打印实物的原因,这是我查找资料,找到的原因:
在 逐字节发送(fputc
、printf
)这种场景下,推荐用 TXE,它既不会阻塞得太久,也能保证每个字节都被正确推送到发送机里,不会弄断多字节编码的连续性。
当我们数据错误的时候可以把这个当作一个解决问题的方向
2、使能位的切换问题
3、逻辑错误,由于modbus发送的报文可能有0x00,因此不能这样
4、空闲中断的清除方法(重要)
下面是错误的
正确方法:
空闲中断的清除,需要一个接收数据的动作,与接收中断不一样。
读取状态寄存器只是走一个形式,最主要的是读取数据寄存器即DR
5、出现脏值
出现脏值的原因,即data->send_size_byte的值不对
因为每次 传输完成后都没有将
data_cmd03 以及 data_rs485结构体清0
现象:
修改方式:
Modbus_Init函数的结尾
结束
我最近在调整我的代码风格,各位如果有什么好的建议,可以私信或者评论,我会积极采纳,谢谢大家,希望我们能够一起进步!!!
希望大家能从我的代码中提取重点,错误中吸取经验!
代码重在练习!
代码重在练习!
代码重在练习!
今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏加关注,谢谢大家!!!
相关文章:

STM32 Modbus RTU从机开发实战:核心实现与五大调试陷阱解析
知识点1【CRC校验】 CRC校验生成网址 CRC(循环冗余校验)在线计算_ip33.com 知识点2【代码演示】 代码书写思路 代码演示 main.c #include "stm32f10x.h" #include "stm32f10x_conf.h" #include "rs485.h"int main(voi…...

Python----目标检测(《Fast R-CNN》和Fast R-CNN)
一、《Fast R-CNN》 1.1、基本信息 作者:Ross Girshick 机构:Microsoft Research 发表时间:2015年 论文链接:arXiv:1504.08083 代码开源:GitHub仓库(MIT License) 1.2、主要内容 Fast R…...

iEKF的二维应用实例
如果熟悉 EKF 与卡尔曼的推导的话,iEKF 就比较容易理解,关于卡尔曼滤波的推导以及EKF,可以参考以前的文章: 卡尔曼滤波原理:https://blog.csdn.net/a_xiaoning/article/details/130564473?spm1001.2014.3001.5502 E…...
机器学习中的线性回归:从理论到实践的深度解析
一、引言 线性回归(Linear Regression)是机器学习和统计学中最基础且应用广泛的模型之一,用于预测连续型目标变量。它通过建立输入特征与输出变量之间的线性关系,实现对未知数据的预测。无论是预测房价、股票走势,还是…...

【通关文件操作(下)】--文件的顺序读写(续),sprintf和sscanf函数,文件的随机读写,文件缓冲区,更新文件
目录 四.文件的顺序读写(续) 4.8--fwrite函数 4.9--fread函数 五.sprintf函数和sscanf函数 5.1--函数对比 5.2--sprintf函数 5.3--sscanf函数 六.文件的随机读写 6.1--fseek函数 6.2--ftell函数 6.3--rewind函数 七.文件缓冲区 7.1--fflush函数 八.更新文件 &…...

mysql的Memory引擎的深入了解
目录 1、Memory引擎介绍 2、Memory内存结构 3、内存表的锁 4、持久化 5、优缺点 6、应用 前言 Memory 存储引擎 是 MySQL 中一种高性能但非持久化的存储方案,适合临时数据存储和缓存场景。其核心优势在于极快的读写速度,需注意数据丢失风险和内存占…...
尚硅谷-尚庭公寓部署文档
文章目录 整合版部署文档部署架构图1. 项目目录结构增加注释的 Dockerfile 配置(1) 后端服务1 Dockerfile (backend/service1/Dockerfile)(2) 后端服务2 Dockerfile (backend/service2/Dockerfile) Dockerfile 配置说明重要注意事项3. Nginx 配置(1) 主配置文件 (nginx/nginx.c…...
使用函数证明给定的三个数是否能构成三角形
问题描述 给定三条边,请你判断一下能不能组成一个三角形。 输入数据第一行包含一个数M,接下有M行,每行一个实例,包含三个正数A,B,C。其中A,B,C <1000; 对于每个测试实例,如果三条边长A,B,C能组成三角形的话&#x…...

【数据结构】——二叉树堆(下)
一、堆中两个重要的算法 我们前面学习了树的概念和结构,还要树的一种特殊树--二叉树,然后我们学习了堆,知道了堆分为大堆和小堆,接下来我们就使用堆来进行一个排序。 在学习我们的堆排序前,我们先详细学习一下我们堆…...

t009-线上代驾管理系统
项目演示地址 摘 要 使用旧方法对线上代驾管理系统的信息进行系统化管理已经不再让人们信赖了,把现在的网络信息技术运用在线上代驾管理系统的管理上面可以解决许多信息管理上面的难题,比如处理数据时间很长,数据存在错误不能及时纠正等问题…...
目标检测预测框置信度(Confidence Score)计算方式
预测框的置信度(Confidence Score)是目标检测模型输出的一个关键部分,它衡量了模型对一个预测框中包含特定类别对象的确定程度。 不同的目标检测模型(如Faster R-CNN、SSD、YOLO、DETR等)在计算置信度时有其特有的机制…...

【题解-洛谷】B4295 [蓝桥杯青少年组国赛 2022] 报数游戏
题目:B4295 [蓝桥杯青少年组国赛 2022] 报数游戏 题目描述 某班级男生人数为 X X X 人,女生人数为 Y Y Y 人,现全班同学围成一个圆圈,并按照顺时针方向为每名同学编号(从 1 1 1 到 X Y XY XY)。现给…...

Bootstrap项目 - 个人作品与成就展示网站
文章目录 前言一、项目整体概述1. 项目功能介绍1.1 导航栏1.2 首页模块1.3 关于我模块1.4 技能模块1.5 作品模块1.6 成就模块1.7 博客模块1.8 联系我模块 2. 技术选型说明 二、项目成果展示1. PC端展示1.1 首页1.2 关于我1.3 技能1.4 作品1.5 成就1.6 博客1.7 联系我 2. 移动端…...

新能源汽车霍尔线束介绍
新能源汽车作为传统燃油车的重要替代方案,其核心驱动系统依赖于高效、精准的电子控制技术。在这一体系中,霍尔线束作为关键传感器组件,承担着电机转速、位置信号的实时采集与传输任务,其性能直接影响整车动力输出的稳定性和能量利…...
2023网络应用专业-Python程序设计复习题目
2023技校网络应用专业-Python程序设计复习题目 须知: 个人信息要填写正确,在线答题时间不限,可以反复作答,次数不限,最后取最高分。 第一部分:单选题 1. 在Python交互模式下,输入下面代码: >>> “{0:.2f}”.format(12345.6789) 回车后显示的结果为: [单选…...
Termux可用中间人网络测试工具Xerosploit
Termux可用中间人网络测试工具Xerosploit。 Xerosploit 是一款基于 MITM 的本地网络渗透测试工具包。 食用方法: git clone https://github.com/LionSec/xerosploit cd xerosploit sudo python3 install.py 运行: sudo xerosploit 使用备注࿱…...

气镇阀是什么?
01、阀门介绍: 油封机械真空泵的压缩室上开一小孔,并装上调节阀,当打开阀并调节入气量,转子转到某一位置,空气就通过此孔掺入压缩室以降低压缩比,从而使大部分蒸汽不致凝结而和掺入的气体一起被排除泵外起此…...

SmolVLM2: The Smollest Video Model Ever(七)
编写测试代码与评价指标 现在的数据集里面只涉及tool的分类和手术phase的分类,所以编写的评价指标还是那些通用的,但是: predicted_labels:[The current surgical phase is CalotTriangleDissection, Grasper, Hook tool exists., The curre…...

RFID综合项目实训 | 基于C#的一卡通管理系统
目录 基于C#的一卡通管理系统 【实验目的】 【实验设备】 【实验内容】 【实验步骤】 实验准备 第一部分 界面布局设计 第二部分 添加串口通讯函数及高频标签操作功能函数(部分代码) 第五部分 实验运行效果 基于C#的一卡通管理系统 【实验目的】 熟悉 …...

mysql如何设置update时间字段自动更新?
之前在给网站做表设计的时候时间字段都是用的datetime类型,初始值都是设置的CURRENT_TIMESTAMP。 由于给文章表设计的有创建时间和更新时间两个字段,但是更新时间字段需要在更新文章后再次更新,当时由于不了解mysql还可以设置自动更新时间戳…...
数据库备份与恢复专业指南
数据库备份与恢复专业指南 一、备份与恢复核心概念 关键结论:数据库备份是数据安全的最后防线,恢复能力才是真正的备份有效性检验标准。 AI大模型专栏:https://duoke360.com/tutorial/path/ai-lm 1.1 备份类型 物理备份:直接复制数据库文件(如MySQL的.ibd文件、Oracle的.d…...

【第4章 图像与视频】4.5 操作图像的像素
文章目录 前言示例-获取和修改图像数据图像数据的遍历方式图像滤镜负片滤镜黑白滤镜浮雕滤镜filter滤镜属性 前言 getImageData() 与 putImageData() 这两个方法分别用来获取图像的像素信息,以及向图像中插入像素。与此同时,如果有需要,也可…...

Science Advances 上海理工大学与美国杜克大学(Duke University)共同开发了一种仿生复眼相机
编辑丨%科学家开发了一种 AI 辅助的仿生复眼相机。炎炎夏日,相信各位读者都有被蚊子骚扰过的恼火记忆。但往往想要清剿蚊子的时候,却被它灵巧地躲开,再难找到。诸如蚊子这种节肢动物的视觉系统已经进化了 5 亿多年,从寒武纪一直到…...

正点原子Z20 ZYNQ 开发板发布!板载FMC LPC、LVDS LCD和WIFI蓝牙等接口,资料丰富!
正点原子Z20 ZYNQ 开发板发布!板载FMC LPC、LVDS LCD和WIFI&蓝牙等接口,资料丰富! 正点原子新品Z20 ZYNQ开发板来啦!核心板全工业级设计,主控芯片型号是XC7Z020CLG484-2I。开发板由核心板底板组成&…...

软件测评中心如何确保软件品质?需求分析与测试计划很关键
软件测评中心承担着对软件进行评估、测试和审查的任务,它有一套规范的流程来确保软件的品质,并且能够向客户和开发者提供详实的软件状况分析报告。 需求分析环节 这一环节至关重要,必须与客户和开发团队保持密切交流。我们需要从他们那里精…...
004 flutter基础 初始文件讲解(3)
之前,我们正向的学习了一些flutter的基础,如MaterialApp,Scaffold之类的东西,那么接下来,我们将正式接触原代码: import package:flutter/material.dart;void main() {runApp(const MyApp()); }class MyAp…...

2025LitCTF 复现
easy_file 登录界面 尝试admin 和密码1 没成功 结果尝试了弱口令 admin 用户和 password 密码就登录成功了 文件上传 新建一个空的 新建木马.txt 文件发现上传成功 然后写入一句话木马 <?php eval($_POST[a]); ?> 上传失败 说明可能是有字符被过滤了,猜…...
英语中最难学的部分是时态
英语中最难学的部分是时态。英语的时态体系包含16种时态,每种时态都有其独特的用法和规则,这使得时态成为英语学习中的一个难点 。 时态的定义和分类 时态可以分为时间和体态两个维度: 时间:现在时、过去时、将来时、过…...
Python 如何让自动驾驶的“眼睛”和“大脑”真正融合?——传感器数据融合的关键技术解析
Python 如何让自动驾驶的“眼睛”和“大脑”真正融合?——传感器数据融合的关键技术解析 自动驾驶技术从来都不是“单兵作战”。如果你细看一辆自动驾驶汽车,它其实是一个传感器的集合体:摄像头、激光雷达(LiDAR)、毫米波雷达、超声波传感器、GPS……这些传感器各自发挥作…...

使用C# 快速删除Excel表格中的重复行数据-详解
目录 简介 使用工具 C# 删除Excel工作表中的重复行 C# 删除指定Excel单元格区域中的重复行 C# 基于特定列删除重复行 RemoveDuplicates 方法快速比较 简介 重复行是指在Excel表格中完全相同的多行数据。这些冗余行的存在可能源于多种原因,例如: …...