STM32实现Modbus RTU协议详解
使用stm32f407封装modbus RTU协议的数据帧格式
1、 modbus协议
Modbus协议是一种用于工业控制的网络通讯协议。同一条通信线上只有一个主设备,多个从设备,最多可以有247个从机设备。
Modbus协议 定义了四种寄存器:
线圈(Coil):1bit,可读可写
离散输入(Discrete Input):1bit,只读
保持寄存器(Holding Register):1byte,可读可写
输入寄存器(Input Register):1byte,只读
modbus的帧格式:
2、代码实现
使用stm32f407封装modbus RTU协议的数据帧格式,stm32(从机)跟modbus poll(主机)进行通信(这里直接使用串口助手是一样的效果)。
2.1 串口配置
串口就正常配置,1停止位、8数据位、无校验位、1停止位。开启接收中断、空闲中断。但一般来说,它应保持在至少3.5个字符时间的范围内,以确保数据帧的正确区分和接收。这里直接使用空闲中断来代替这个时间间隔。
#define USART1_RX_MAXLEN 256
uint8_t usart1_RxData[USART1_RX_MAXLEN]; //串口接收数据缓冲区
uint16_t usart1_Rxlen = 0; //串口接收到数据的长度
uint8_t usart1_RxFlag = 0; //进入空闲中断后置为1// 串口1中断
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
//通过对 USART_DR 寄存器执行读入操作将该位RXNE清零。
usart1_RxData[usart1_Rxlen] = USART_ReceiveData(USART1);
usart1_Rxlen += 1;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
if(USART_GetITStatus(USART1, USART_IT_IDLE) == SET)
{
//该位由软件序列清零(读入 USART_SR 寄存器,然后读入 USART_DR 寄存器)
USART1->SR;
USART1->DR;
usart1_RxFlag = 1;
}
}
2.2 modbus相关定义
typedef enum {
READ_COILS = 0x01, // 读线圈
READ_DISCRETE_INPUTS = 0x02, // 读离散输入
READ_HOLDING_REGISTERS = 0x03, // 读保持寄存器
READ_INPUT_REGISTERS = 0x04, // 读输入寄存器
WRITE_SINGLE_COIL = 0x05, // 写单个线圈
WRITE_SINGLE_REGISTER = 0x06, // 写单个保持寄存器
WRITE_MULTIPLE_COILS = 0x0F, // 写多个线圈
WRITE_MULTIPLE_REGISTERS = 0x10, // 写多个保持寄存器
// 其他功能码
DIAGNOSTIC = 0x08,
}FUNCTION_CODE; // 枚举定义的功能码typedef struct {
uint8_t send_buf[256]; // 从机要发送的数据
uint8_t recv_buf[256]; // 从机接收到的数据
}SLAVE_DATA; // 从机相关的数据SLAVE_DATA slave_data;
#define SLAVE_ADDRESS 0x01 // 当前从机的地址
以下是寄存器的定义(数据可自己修改验证)
// 线圈
#define COILS_MAXNUM 32
static uint8_t Coils[COILS_MAXNUM] = {1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0};
// 离散量输入
#define DISCRETE_INPUTS_MAXNUM 32
static uint8_t Discrete_Inputs[DISCRETE_INPUTS_MAXNUM] = {1, 1, 0, 0, 1, 1, 1, 0, 0, 1};
// 保持寄存器
#define HOLDING_REG_MAXNUM 32
static uint16_t Holding_REG[HOLDING_REG_MAXNUM] = {0xA0A0, 0xB0B0, 0xC0C0, 0xD0D0, 0xE0E0, 0xF0F0, 0};
// 输入寄存器
#define INPUT_REG_MAXNUM 32
static uint16_t Input_REG[INPUT_REG_MAXNUM] = {0x1A1A, 0x1B1B, 0x1C1C, 0x1D1D, 0x1E1E, 0x1F1F, 0};
2.3 常用功能码实现
2.3.1 准备
/**
* @brief 清除从机数据的数据
* @param 无
* @retval 无
*/
void Modbus_clear_SlaveData(void)
{
memset(slave_data.send_buf, 0, sizeof(slave_data.send_buf));
memset(slave_data.recv_buf, 0, sizeof(slave_data.recv_buf));
}/**
* @brief 判断接受的CRC数据是否正确
* @param 无
* @retval 1,正确 0,不正确
*/
uint8_t Modbus_Judge_CRC(unsigned char *data, unsigned int len)
{
// 由于ModBus_CRC16()这个函数
uint16_t recv_crc = (uint16_t)((data[len – 2] << 8) | (data[len – 1]));
uint16_t crc = ModBus_CRC16(data, len – 2);
if(recv_crc == crc)
return 1;
return 0;
}/**
* @brief 判断接受的从机地址 是不是 当前从机地址
* @param 无
* @retval 1,是 0,不是
*/
uint8_t Modbus_Judge_SlaveAddress(void)
{
if(slave_data.recv_buf[0] == SLAVE_ADDRESS)
{
return 1;
}
return 0;
}/**
* @brief 识别接收到的功能码
* @param 无
* @retval 功能码
*/
uint8_t Modbus_Judge_FuncCode(void)
{
return slave_data.recv_buf[1];
}
这里补充个CRC校验
/*
* @name CRC_Check
* @brief CRC校验
* @param data-> len->长度
* @retval CRC校验值
*/
uint16_t ModBus_CRC16(unsigned char *data, unsigned int len)
{
unsigned int i, j, tmp, CRC16;CRC16 = 0xFFFF; //CRC寄存器初始值
for (i = 0; i < len; i++)
{
CRC16 ^= data[i];
for (j = 0; j < 8; j++)
{
tmp = (unsigned int)(CRC16 & 0x0001);
CRC16 >>= 1;
if (tmp == 1)
{
CRC16 ^= 0xA001; //异或多项式
}
}
}
return (CRC16 << 8) | (CRC16 >> 8);
}
需要进行CRC校验,可以点这里 ——> CRC校验
2.3.2 功能码:01H-读线圈状态
描述:读从机线圈寄存器,位操作,可读单个或者多个;
发送指令: 假设从机地址位0x01,寄存器开始地址0x0023,寄存器结束抵制0x0038,总共读取21个线圈。协议图如下:(这里的寄存器数量低8位应该是0x15才对)
响应: 返回数据的每一位对应线圈状态,1-ON,0-OFF,如下图;
上表中data1表示0x0023-0x002a的线圈状态,data1的最低位代表最低地址的线圈状态,可以理解为小端模式;
data2表示地址0x002b-0x0033的线圈状态,如下表:
data3表示地址0x0034-0x0038的线圈状态,不够8位,字节高位填充为0,如下表:
/**
* @brief 读线圈
* @param 无
* @retval 功能码
*/
void Modbus_Function_01(void)
{
uint16_t Coils_start_Address = (uint16_t)((slave_data.recv_buf[2]<<8))|(slave_data.recv_buf[3]); // 线圈起始地址
uint16_t Read_Coils_cnt = (uint16_t) ((slave_data.recv_buf[4]<<8 )| (slave_data.recv_buf[5])); // 线圈数量
slave_data.send_buf[0] = SLAVE_ADDRESS; // 从机地址
slave_data.send_buf[1] = READ_COILS; // 功能码
if((Read_Coils_cnt%8) == 0) //整除
slave_data.send_buf[2] = (uint8_t)(Read_Coils_cnt/8);
else
slave_data.send_buf[2] = (uint8_t)(Read_Coils_cnt/8) + 1;//返回字节数uint16_t k = 3; // 跳过前面从机地址、 功能码 、 返回字节数
for (uint16_t i = Coils_start_Address; i < Coils_start_Address + Read_Coils_cnt; i += 8)
{
slave_data.send_buf[k] = 0x00;
for (uint8_t j = 0; j < 8 && i + j < Coils_start_Address + Read_Coils_cnt; j++)
{
if (Coils[i + j] == 0x01)
{
slave_data.send_buf[k] |= (1 << j);
}
}
k++;
}
uint16_t crc = ModBus_CRC16(slave_data.send_buf, 3 + slave_data.send_buf[2]);
slave_data.send_buf[3 + slave_data.send_buf[2]] = (uint8_t)(crc >> 8);
slave_data.send_buf[3 + slave_data.send_buf[2] + 1] = (uint8_t)(crc);
MyUsart1_SendArray(slave_data.send_buf, 3 + slave_data.send_buf[2] + 1+ 1);
Modbus_clear_SlaveData();
}
效果:寄存器的定义和数据初始化在 2.2(绿色为主机,白色从机)
2.3.3 功能码:02H-读离散输入状态
读离散输入跟读线圈格式一样,这里不过多介绍
/**
* @brief 读离散输入
* @param 无
* @retval 无
*/
void Modbus_Function_02(void)
{
uint16_t Discrete_Inputs_start_Address = (uint16_t)((slave_data.recv_buf[2]<<8))|(slave_data.recv_buf[3]);
uint16_t Discrete_Inputs_cnt = (uint16_t) ((slave_data.recv_buf[4]<<8 )| (slave_data.recv_buf[5]));
slave_data.send_buf[0] = SLAVE_ADDRESS;
slave_data.send_buf[1] = READ_DISCRETE_INPUTS;
if((Discrete_Inputs_cnt % 8) == 0) //整除
{
slave_data.send_buf[2] = (uint8_t)(Discrete_Inputs_cnt/8);
}
else
{
slave_data.send_buf[2] = (uint8_t)(Discrete_Inputs_cnt/8) + 1;//返回数据的字节数
}uint16_t k = 3;
for (uint16_t i = Discrete_Inputs_start_Address; i < Discrete_Inputs_start_Address + Discrete_Inputs_cnt; i += 8)
{
slave_data.send_buf[k] = 0x00;
for (uint8_t j = 0; j < 8 && (i + j < Discrete_Inputs_start_Address + Discrete_Inputs_cnt); j++)
{
if (Discrete_Inputs[i + j] == 0x01)
{
slave_data.send_buf[k] |= (1 << j);
}
}
k++;
}
uint16_t crc = ModBus_CRC16(slave_data.send_buf, 3 + slave_data.send_buf[2]);
slave_data.send_buf[3 + slave_data.send_buf[2]] = (uint8_t)(crc >> 8);
slave_data.send_buf[3 + slave_data.send_buf[2] + 1] = (uint8_t)(crc);
MyUsart1_SendArray(slave_data.send_buf, 3 + slave_data.send_buf[2] + 1+ 1);
Modbus_clear_SlaveData();
}
效果:寄存器的定义和数据初始化在 2.2(绿色为主机,白色从机)
2.3.4 功能码:03H-读保持寄存器
描述:读保持寄存器,字节指令操作,可读单个或者多个;
发送指令: 从机地址0x01,保持寄存器起始地址0x0032,读2个保持寄存器
响应:
/**
* @brief 读保持寄存器
* @param 无
* @retval 无
*/
void Modbus_Function_03(void)
{
uint16_t Holding_REG_start_Address = (uint16_t)((slave_data.recv_buf[2]<<8))|(slave_data.recv_buf[3]);
uint16_t Read_Holding_REG_cnt = (uint16_t) ((slave_data.recv_buf[4]<<8 )| (slave_data.recv_buf[5]));
slave_data.send_buf[0] = SLAVE_ADDRESS;
slave_data.send_buf[1] = READ_HOLDING_REGISTERS;
slave_data.send_buf[2] = Read_Holding_REG_cnt * 2; //返回数据的字节数
for(uint16_t k = 3, i = Holding_REG_start_Address; i <=Holding_REG_start_Address+Read_Holding_REG_cnt; i += 1, k += 2)
{
slave_data.send_buf[k] = (uint8_t)(Holding_REG[i] >> 8);
slave_data.send_buf[k+1] = Holding_REG[i];
}
uint16_t crc = ModBus_CRC16(slave_data.send_buf, 3 + Read_Holding_REG_cnt * 2);
slave_data.send_buf[3 + (Read_Holding_REG_cnt * 2)] = (uint8_t)(crc >> 8);
slave_data.send_buf[3 + (Read_Holding_REG_cnt * 2) + 1] = (uint8_t)(crc);
MyUsart1_SendArray(slave_data.send_buf, 3 + (Read_Holding_REG_cnt * 2) + 1 + 1);
Modbus_clear_SlaveData();
}
效果:寄存器的定义和数据初始化在 2.2(绿色为主机,白色从机)
2.3.5 功能码:04H-读输入寄存器
同读保持寄存器格式一致,这里就不作介绍了
/**
* @brief 读输入寄存器
* @param 无
* @retval 无
*/
void Modbus_Function_04(void)
{
uint16_t Input_REG_start_Address = (uint16_t)((slave_data.recv_buf[2]<<8))|(slave_data.recv_buf[3]);
uint16_t Input_REG_cnt = (uint16_t) ((slave_data.recv_buf[4]<<8 )| (slave_data.recv_buf[5]));
slave_data.send_buf[0] = SLAVE_ADDRESS;
slave_data.send_buf[1] = READ_INPUT_REGISTERS;
slave_data.send_buf[2] = Input_REG_cnt * 2; //返回数据的字节数
for(uint16_t k = 3, i = Input_REG_start_Address; i <=Input_REG_start_Address+Input_REG_cnt; i += 1, k += 2)
{
slave_data.send_buf[k] = (uint8_t)(Input_REG[i] >> 8);
slave_data.send_buf[k+1] = Input_REG[i];
}
uint16_t crc = ModBus_CRC16(slave_data.send_buf, 3 + Input_REG_cnt * 2);
slave_data.send_buf[3 + (Input_REG_cnt * 2)] = (uint8_t)(crc >> 8);
slave_data.send_buf[3 + (Input_REG_cnt * 2) + 1] = (uint8_t)(crc);
MyUsart1_SendArray(slave_data.send_buf, 3 + (Input_REG_cnt * 2) + 1 + 1);
Modbus_clear_SlaveData();
}
效果:寄存器的定义和数据初始化在 2.2(绿色为主机,白色从机)
2.3.6 功能码:05H-写单个线圈
描述:写单个线圈,位操作,只能写一个,写0xff00表示设置线圈状态为ON,写0x0000表示设置线圈状态为OFF
发送指令: 设置0x0032线圈为ON;
响应: 同发送指令
void Modbus_Function_05(void)
{
uint16_t Holding_REG_start_Address = (uint16_t)((slave_data.recv_buf[2]<<8))|(slave_data.recv_buf[3]);
slave_data.send_buf[0] = SLAVE_ADDRESS;
slave_data.send_buf[1] = WRITE_SINGLE_COIL;
// 高 低8位地址
slave_data.send_buf[2] = slave_data.recv_buf[2];
slave_data.send_buf[3] = slave_data.recv_buf[3];
// 高 低8位数据
uint16_t coils_value = (uint16_t)((slave_data.recv_buf[4] << 8)|(slave_data.recv_buf[5]));
if(coils_value == 0xff00)
{
Coils[Holding_REG_start_Address] = 0x01;
slave_data.send_buf[4] = (uint8_t)0xff;
slave_data.send_buf[5] = (uint8_t)0x00;
}
else if(coils_value == 0x0000)
{
Coils[Holding_REG_start_Address] = 0x00;
slave_data.send_buf[4] = (uint8_t)0x00;
slave_data.send_buf[5] = (uint8_t)0x00;
}
uint16_t crc = ModBus_CRC16(slave_data.send_buf, 6);
slave_data.send_buf[6] = (uint8_t)(crc >> 8);
slave_data.send_buf[7] = (uint8_t)(crc);
MyUsart1_SendArray(slave_data.send_buf, 7 + 1);
Modbus_clear_SlaveData();
}
效果:寄存器的定义和数据初始化在 2.2(绿色为主机,白色从机)
2.3.7 功能码:06H-写单个保持寄存器
描述:写单个保持寄存器,字节指令操作,只能写一个;
发送指令: 写0x0032保持寄存器为0x1232;
响应:同发送指令;
/**
* @brief 写单个保持寄存器
* @param 无
* @retval 无
*/
void Modbus_Function_06(void)
{
uint16_t Holding_REG_start_Address = (uint16_t)((slave_data.recv_buf[2]<<8))|(slave_data.recv_buf[3]);
slave_data.send_buf[0] = SLAVE_ADDRESS;
slave_data.send_buf[1] = WRITE_SINGLE_REGISTER;// 高 低8位地址
slave_data.send_buf[2] = slave_data.recv_buf[2];
slave_data.send_buf[3] = slave_data.recv_buf[3];
// 高 低8位数据
Holding_REG[Holding_REG_start_Address] = (uint16_t)((slave_data.recv_buf[4] << 8)|(slave_data.recv_buf[5]));
slave_data.send_buf[4] = (uint8_t)(Holding_REG[Holding_REG_start_Address]>>8 );
slave_data.send_buf[5] = (uint8_t)(Holding_REG[Holding_REG_start_Address]);
uint16_t crc = ModBus_CRC16(slave_data.send_buf, 6);
slave_data.send_buf[6] = (uint8_t)(crc >> 8);
slave_data.send_buf[7] = (uint8_t)(crc);
MyUsart1_SendArray(slave_data.send_buf, 7 + 1);
Modbus_clear_SlaveData();
}
效果:寄存器的定义和数据初始化在 2.2(绿色为主机,白色从机)
2.3.8 功能码:0FH-写多个线圈
描述:写多个线圈寄存器。若数据区的某位值为“1”表示被请求的相应线圈状态为ON,若某位值为“0”,则为状态为OFF。
发送指令: 线圈地址为0x04a5,写12个线圈,
上图中DATA1为0x0c,表示:
DATA2为0x02,不够8位,字节高位填充0:
响应:
/**
* @brief 写多个线圈
* @param 无
* @retval 无 uint16_t Input_REG_cnt = (uint16_t) ((slave_data.recv_buf[4]<<8 )| (slave_data.recv_buf[5]));
*/
void Modbus_Function_15(void)
{
uint16_t Coils_start_Address = (uint16_t)((slave_data.recv_buf[2]<<8))|(slave_data.recv_buf[3]);
uint16_t Coils_cnt = (uint16_t) ((slave_data.recv_buf[4]<<8 )| (slave_data.recv_buf[5])); // 写线圈的数量
uint8_t Byte_cnt = (uint8_t)(slave_data.recv_buf[6]<<8); // 后面数据 有多少个字节
uint8_t k = 7; // 跳过地址和数量字段
for (uint16_t i = 0; i < Coils_cnt; i++)
{
uint8_t byte_index = i / 8; // 字节偏移
uint8_t bit_index = i % 8; //位偏移
if ((slave_data.recv_buf[k + byte_index] & (0x01 << bit_index)) != 0)
{
Coils[Coils_start_Address + i] = 1;
}
else
{
Coils[Coils_start_Address + i] = 0;
}
}
slave_data.send_buf[0] = SLAVE_ADDRESS;
slave_data.send_buf[1] = WRITE_MULTIPLE_COILS;
// 高 低8位地址
slave_data.send_buf[2] = slave_data.recv_buf[2];
slave_data.send_buf[3] = slave_data.recv_buf[3];
// 高 低8位寄存器数量
slave_data.send_buf[4] = slave_data.recv_buf[4];
slave_data.send_buf[5] = slave_data.recv_buf[5];
// 字节数量
slave_data.send_buf[6] = slave_data.recv_buf[6];
uint16_t crc = ModBus_CRC16(slave_data.send_buf, 6);
slave_data.send_buf[7] = (uint8_t)(crc >> 8);
slave_data.send_buf[8] = (uint8_t)(crc);
MyUsart1_SendArray(slave_data.send_buf, 7 + 2);
Modbus_clear_SlaveData();
}
效果:寄存器的定义和数据初始化在 2.2(绿色为主机,白色从机)
2.3.9 功能码:10H-写多个保持寄存器
描述:写多个保持寄存器,字节指令操作,可写多个;
发送指令: 保持寄存器起始地址为0x0034,写2个寄存器4个字节的数据;
响应:
void Modbus_Function_16(void)
{
uint16_t Holding_REG_start_Address = (uint16_t)((slave_data.recv_buf[2]<<8))|(slave_data.recv_buf[3]);
uint16_t Holding_REG_cnt = (uint16_t)((slave_data.recv_buf[4]<<8))|(slave_data.recv_buf[5]);
uint8_t Byte_cnt = (uint8_t)(slave_data.recv_buf[6]<<8); // 后面数据 有多少个字节
slave_data.send_buf[0] = SLAVE_ADDRESS;
slave_data.send_buf[1] = WRITE_MULTIPLE_REGISTERS;// 高 低8位地址
slave_data.send_buf[2] = slave_data.recv_buf[2];
slave_data.send_buf[3] = slave_data.recv_buf[3];
uint16_t k = 7; // 跳过 1地址+1功能码+2寄存器起始地址+2寄存器数量+1数据数量
for(uint16_t i = 0; i < Holding_REG_cnt; i++)
{
Holding_REG[Holding_REG_start_Address + i] =(uint16_t)((slave_data.recv_buf[k] << 8)| slave_data.recv_buf[k+1]);
k+=2;
}
// 高 低8位寄存器数量
slave_data.send_buf[4] = slave_data.recv_buf[4];
slave_data.send_buf[5] = slave_data.recv_buf[5];
uint16_t crc = ModBus_CRC16(slave_data.send_buf, 6);
slave_data.send_buf[6] = (uint8_t)(crc >> 8);
slave_data.send_buf[7] = (uint8_t)(crc);
MyUsart1_SendArray(slave_data.send_buf, 6 + 2);
Modbus_clear_SlaveData();
}
效果:寄存器的定义和数据初始化在 2.2(绿色为主机,白色从机)
2.3.9 集成起来
当串口接收完数据,就进行下面的逻辑判断。
int main(void)
{
while(1)
{
if(usart1_RxFlag == 1) //接收到数据
{
Modbus_Event(); // 这个函数在下面
usart1_Rxlen = 0;
usart1_RxFlag = 0;// 清标志位
}
}
}
/**
* @brief Modbus总体逻辑
* @param 无
* @retval 功能码
*/
int8_t Modbus_Event(void)
{
uint8_t func_code;
// 将串口接收到的数据,放入 slave_data.recv_buf中
memcpy(slave_data.recv_buf , usart1_RxData, sizeof(usart1_RxData[0])*usart1_Rxlen);
// 验证从机地址是否正确
if(Modbus_Judge_SlaveAddress() == 0) return 1;
// 验证CRC校验是否正确
if(Modbus_Judge_CRC(usart1_RxData, usart1_Rxlen) == 0) return 2;
// 得到数据帧的功能码
func_code = Modbus_Judge_FuncCode();
switch(func_code)
{
case READ_COILS:
Modbus_Function_01();
break;
case READ_DISCRETE_INPUTS:
Modbus_Function_02();
break;
case READ_HOLDING_REGISTERS:
Modbus_Function_03();
break;
case READ_INPUT_REGISTERS:
Modbus_Function_04();
break;
case WRITE_SINGLE_COIL:
Modbus_Function_05();
break;
case WRITE_SINGLE_REGISTER:
Modbus_Function_06();
break;
case WRITE_MULTIPLE_COILS:
Modbus_Function_15();
break;
case WRITE_MULTIPLE_REGISTERS:
Modbus_Function_16();
break;
default :
break;
}
return 0;
}
作者:Rocket279