当前位置: 首页 > news >正文

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,就会出现第一个字符打印实物的原因,这是我查找资料,找到的原因:

逐字节发送fputcprintf)这种场景下,推荐用 TXE,它既不会阻塞得太久,也能保证每个字节都被正确推送到发送机里,不会弄断多字节编码的连续性。

当我们数据错误的时候可以把这个当作一个解决问题的方向

2、使能位的切换问题

3、逻辑错误,由于modbus发送的报文可能有0x00,因此不能这样

4、空闲中断的清除方法(重要)

下面是错误

正确方法

空闲中断的清除,需要一个接收数据的动作,与接收中断不一样。

读取状态寄存器只是走一个形式,最主要的是读取数据寄存器即DR

5、出现脏值

出现脏值的原因,即data->send_size_byte的值不对

因为每次 传输完成后都没有将

data_cmd03 以及 data_rs485结构体清0

现象:

修改方式:

Modbus_Init函数的结尾

 

结束

我最近在调整我的代码风格,各位如果有什么好的建议,可以私信或者评论,我会积极采纳,谢谢大家,希望我们能够一起进步!!!

希望大家能从我的代码中提取重点,错误中吸取经验!

代码重在练习!

代码重在练习!

代码重在练习!

今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏加关注,谢谢大家!!!

相关文章:

  • Java并发编程利器:LongAdder原理解析与实战应用
  • Linux系统-基本指令(3)
  • Linux Ubuntu24.04配置安装MySQL8.4.5高可用集群主从复制!
  • Docker修改镜像存放位置
  • influxdb时序数据库
  • 图论学习笔记 5 - 最小树形图
  • 代码随想录算法训练营 Day56 图论Ⅶ 最小生成树算法 Prim Kruskal
  • 仿真环境中机器人抓取与操作 - 上手指南
  • 《软件工程》第 16 章 - 软件项目管理与过程改进
  • OpenCv高阶(十三)——人脸检测
  • 2025年智慧农业与人工智能国际学术会议(SAAI 2025)
  • 微软开源bitnet b1.58大模型,应用效果测评(问答、知识、数学、逻辑、分析)
  • deepseek开源资料汇总
  • 7系fpga带microblaze做固件及固化
  • 攻防世界-ics-07
  • 多租户架构详解:从概念到实现的方法说明
  • 声动心弦 - 校园音乐分享平台的数字交响-测试报告
  • 组合型回溯+剪枝
  • 以少学习:通过无标签数据从大型语言模型进行知识蒸馏
  • 2025年上半年第1批信息系统项目管理师论文真题解析与范文
  • 长沙做网站找谁/网络电商推广方案
  • 长沙公司网站设计/友情链接怎么购买
  • 襄州区住房和城乡建设局网站/seo教程网站优化
  • 网站建设哪家服务态度好/seo外包是什么意思
  • 建设银行网站打不开用什么浏览器/爱站网反链查询
  • 衡阳市住房和城乡建设局官方网站/微营销软件