1前言
项目需要使用MCU实现Modbus RTU协议与PLC通信,STM32作为从机需要将一些传感器信息上报给PLC,有时也需要STM32作为主机采用Modbus RTU协议获取伺服器或者其他设备的一些状态信息。这些信息不需要很多,很多场景下可能就几个地址的寄存器信息。文末给出使用工程的下载地址。
如下图所示,本文主要主要设计使用stm32cubeide stm32f103 freeRTOS 实现Modbus RTU协议对从机寄存器的读写。

2详细设计
采用STM32CubeIDE 1.19.0,其他版本类似,基本上一样,针对硬件配置如下
2.1时钟配置
采用8Mhz外部时钟经过倍频后放大

2.2调试串口1配置
如下图所示

该串口波特率设置为115200,用于调试信息打印,这里在main.c中添加如下代码,实现printf数据打印
cpp
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
while((USART1->SR&0x40)==0){};
USART1->DR = ch;
return ch;
}
2.3485串口的配置
采用uart2作为485串口,波特率选择9600,打开NVIC Settings,是能接受中断。


打开该中断后会在stm32f4xx_it.c有如下中断处理函数
cpp
/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
2.4freeRTOS的配置
使用freeRTOS可以简化编程,实现多任务编程。首先配置调试接口,可以根据需要配置,这里配置SW接口,这里需要配置Timebase Source为TIM1,避免和HAL库共用了SysTick时钟源。

添加485串口接受数据处理任务,以及对应的接受数据队列,这里也对默认任务的堆栈做了放大处理。

在Advanced settings中USE_NEWLIB_REENTRANT 需要Enabled,不然会又警告提示,项目实际上芯片资源充分,这里选择了使能。这里是否需要使能 USE_NEWLIB_REENTRANT 取决于应用需求。如果需要线程安全且可以接受额外的内存开销,建议启用此选项。如果不需要线程安全或内存资源有限,可以选择不启用,但需避免使用可能引发线程安全问题的函数。

3关键代码
3.1Modbus RTU 06功能码处理
针对06功能码,主要主机写入寄存器,将寄存器的值保存到本地。
cpp
// Modbus RTU 6号功能码函数
// Modbus RTU 主机写入寄存器值
void Modbus_RTU_Func6(u8 *src, u16 len)
{
u16 Regadd;
u16 val,j;
Regadd = src[2] * 256 + src[3]; //得到要修改的地址
val = src[4] * 256 + src[5]; //修改后的值
setVal2Regadd(Regadd, val);
// 开始返回Modbus RTU数据
Modbus_RTU_485_TX_Mode;
vTaskDelay(5);
for(j=0;j<len;j++)
{
Modbus_RTU_Send_Byte(src[j]);
}
vTaskDelay(5);
Modbus_RTU_485_RX_Mode;
}
3.2Modbus RTU 03功能码处理
03功能码主要实现主机读取寄存器数据的回复。将本地的数据返回给主机。
cpp
// Modbus RTU 3号功能码函数
// Modbus RTU 主机读取寄存器值
void Modbus_RTU_Func3(u8 *src, u16 len)
{
u16 Regadd,Reglen,crc,tmp,buf[8];
u8 i,j;
//要读取寄存器的首地址
Regadd = src[2] * 256 + src[3];
//要读取寄存器的数据长度
Reglen = src[4] * 256 + src[5];
//发送回应数据包
i = 0;
sendbuf[i++] = MY_MODBUS RTU_ADDR; //发送本设备地址
sendbuf[i++] = 0x03; //发送功能码
//Modbus_RTU.sendbuf[i++] = ((Reglen*2)/256); //返回字节个数
sendbuf[i++] = ((Reglen*2)%256); //返回字节个数
tmp = getValFromRegadd(Regadd, Reglen, buf);
if(tmp == 0)
{
printf("getValFromRegadd tmp = %d,error\r\n", tmp);
return;
}
for(j = 0;j < tmp; j++) //返回数据
{
sendbuf[i++] = buf[j]/256;//Reg[Regadd+j]/256;
sendbuf[i++] = buf[j]%256;//Reg[Regadd+j]%256;
}
crc = Modbus_RTU_CRC16(sendbuf,i); //计算要返回数据的CRC
sendbuf[i++] = crc%256;
sendbuf[i++] = crc/256;
// 开始返回Modbus_RTU数据
Modbus_RTU_485_TX_Mode;
vTaskDelay(5);
for(j=0;j<i;j++)
{
Modbus_RTU_Send_Byte(sendbuf[j]);
}
vTaskDelay(5);
Modbus_RTU_485_RX_Mode;
}
3.3Modbus RTU crc校验函数
针对Modbus RTU crc校验的函数如下,Modbus RTU可以选择的校验很多这里采用CRC16,有时候可能对不上对方校验码,这时可能要考虑更换校验算法。
cpp
u16 Modbus_RTU_CRC16(u8 *data, u16 length)//简化CRC校验提升通信速度
{
u16 j;
u16 reg_crc = 0xffff;
while(length--)
{
reg_crc ^= *data++;
for(j = 0; j < 8; j++)
{
if(reg_crc & 0x0001)
reg_crc = (reg_crc>>1) ^ 0xa001;
else
reg_crc = reg_crc>>1;
}
}
return reg_crc;
}
4测试记录
使用电脑模拟485主机进程测试,电脑端运行,sscom5.13.1,打开对485进行电压转换,这里需要借助一根线,连接到电脑。比如USB转485。
这里还需要补充实测记录
这里举例说明,发送指令
01 03 00 01 00 08 15 CC
其中
01 //从机地址
03 //功能码
00 01 //读取地址01
00 08 //读取长度08
15 CC //校验
关于CRC计算可以使用如下网址的在线计算,也可以使用其他软件:

返回指令
01 03 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 E4 59
01 //地址
03 //功能码
10 //数据长度
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 //数据字节
E4 59//校验
实际测试返回数据截图如下

5本文工程下载地址:
使用stm32cubeidestm32f103freeRTOS实现ModbusRTU协议寄存器读写过程详解,含说明文档、工程代码,测试记录,加速项目开发资源-CSDN下载