本实验使用Stm32c8t6实现Modbus_rtu从机
主机为串口调试助手,通过串口调试助手来与单片机实现数据交互.
Modbus协议
Modbus协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其它设备之间可以通信。它已经成为一通用工业标准.
Modbus协议格式
modbus信息帧所允许的最大长度为256个字节。所以数据的最大长度是252个字节
一个数据帧可以分为:设备码、功能码、数据码、校验码
常用的指令码(功能码)
主设备发送读取0x03指令
0x00 0x03 0x0000 0x0000 0x0000
(设备地址) (指令码) (从设备起始地址) (读取个数) (校验)
从设备回复读取0x03指令
0x00 0x03 0x00 0x0000 0x0000
(设备地址) (指令码) (返回的字节个数) (返回的数据) (校验)
主设备发送单个写入0x06指令
0x00 0x06 0x0000 0x0000 0x0000
(设备地址) (指令码) (从设备起始地址) (写入数据) (校验)
从设备回复写入0x06指令
0x00 0x06 0x0000 0x0000 0x0000
(设备地址) (指令码) (从设备起始地址) (修改数量) (校验)
主设备发送多个写入0x10指令
0x00 0x10 0x0000 0x0000 0x00 0x0000 0x0000
(设备地址) (指令码) (从设备起始地址) (从哪里写) (写入个数) (写入数据) (校验)
从设备回复写入0x10指令
0x00 0x10 0x0000 0x0000 0x0000
(设备地址) (指令码) (从设备起始地址) (修改数量) (校验)
代码部分
本次实验使用Modbus_RTU协议,单片机做从,串口调试助手做主
首先使能串口,定时器等外设
串口USART1:
/**
*@brief 串口初始化函数
*@retval 无
*/
void Serial_UsartInit(void)
{
//开启GPIO和串口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
//GPIO配置
GPIO_InitTypeDef serialgpio;
serialgpio.GPIO_Mode = GPIO_Mode_AF_PP;
serialgpio.GPIO_Pin = GPIO_Pin_9;//串口发送
serialgpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&serialgpio);
serialgpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
serialgpio.GPIO_Pin = GPIO_Pin_10;//串口接受
GPIO_Init(GPIOA,&serialgpio);
//串口配置
USART_InitTypeDef serialconfig;
serialconfig.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
serialconfig.USART_BaudRate = 9600;
serialconfig.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
serialconfig.USART_Parity = USART_Parity_No;
serialconfig.USART_StopBits = USART_StopBits_1;
serialconfig.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&serialconfig);
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
//串口中断配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitTypeDef serialnvic;
serialnvic.NVIC_IRQChannelPreemptionPriority = 1;
serialnvic.NVIC_IRQChannelSubPriority = 1;
serialnvic.NVIC_IRQChannelCmd = ENABLE;
serialnvic.NVIC_IRQChannel = USART1_IRQn;
NVIC_Init(&serialnvic);
USART_Cmd(USART1,ENABLE);
}
/**
*@brief 串口发送数据函数
*@retval 无
*/
void Serial_SendByte(uint8_t byte)
{
USART_SendData(USART1,byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}
定时器TIM3:
/**
*@brief 定时器初始化函数
*@retval 无
*/
void Modbus_TimeIRQInit(void)
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
//gpio配置
GPIO_InitTypeDef timgpio;
timgpio.GPIO_Mode = GPIO_Mode_AF_PP;
timgpio.GPIO_Pin = GPIO_Pin_0;
timgpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&timgpio);
TIM_InternalClockConfig(TIM3);
//定时器配置
TIM_TimeBaseInitTypeDef timconfig;
timconfig.TIM_Prescaler = 9;
timconfig.TIM_CounterMode = TIM_CounterMode_Up;
timconfig.TIM_Period = 7199;
timconfig.TIM_ClockDivision = TIM_CKD_DIV1;
timconfig.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&timconfig);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
//中断配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef timnvic;
timnvic.NVIC_IRQChannelPreemptionPriority = 1;
timnvic.NVIC_IRQChannelSubPriority = 1;
timnvic.NVIC_IRQChannelCmd = ENABLE;
timnvic.NVIC_IRQChannel = TIM3_IRQn;
NVIC_Init(&timnvic);
TIM_Cmd(TIM3,ENABLE);
}
MODBUS相关代码
//定义一个结构体用来储存ModBus的数据
typedef struct
{
uint8_t dev_addr; //本设备从机地址
uint8_t rece_buf[64]; //接受缓冲区
uint8_t rece_cnt; //接受计数器
uint8_t send_buf[64]; //发送缓冲区
uint8_t rece_flag; //接受标志位
uint8_t timout; //数据持续时间
uint8_t timrun; //定时器开始计数标志
}Modbus_t;
//定义一个数组用于Modbus读写
uint16_t Reg[]={0x0001,0x0002,0x0003,0x0004,0x0005,0x0006,0x0007,0x0008,
0x0009,0x0010,0x0011,0x0012,0x0013,0x0014,0x0015,0x0016};
/**
*@brief Modbus初始化函数
*@retval 无
*/
void Modbus_Init(void)
{
modbusdata.dev_addr = 0x02;
modbusdata.timrun = 0;
}
/**
*@brief CRC校验函数
*@retval CRC校验数
*/
uint16_t Modbus_CRC16(uint8_t *ch,uint8_t len)
{
uint16_t crc = 0xffff,code = 0xa001;
char i,j;
for(i = 0;i< len;i++){
crc ^= ch[i];
for(j = 0;j < 8;j++){
if(crc&1){
crc>>=1;
crc^=code;
}else{
crc>>=1;
}
}
}
return crc;
}
/**
*@brief Modbus事件函数
*@retval 错误码
*/
uint8_t Modbus_Event(void)
{
uint16_t crc,rccrc;
if(modbusdata.rece_flag == 0) //判断数据接受是否完成
{
return ERR_FlAG; //宏定义ERR_FLAG = 0x01
}
//获取CRC校验
crc = Modbus_CRC16(&modbusdata.rece_buf[0],modbusdata.rece_cnt-2);
//获取数据帧的CRC校验
rccrc = modbusdata.rece_buf[modbusdata.rece_cnt-1];
rccrc <<=8;
rccrc |= modbusdata.rece_buf[modbusdata.rece_cnt-2];
//对比CRC校验
if(crc == rccrc)
{
//判断头码
if(modbusdata.rece_buf[0] == modbusdata.dev_addr)
{
//索引功能码
switch(modbusdata.rece_buf[1])
{
case 0x03:Modbus_ReadCMD();break;
case 0x06:Modbus_WriteBitCMD();break;
case 0x10:Modbus_WriteDataCMD();break;
default:Serial_SendByte(0x44);break;
}
}
else
{
modbusdata.rece_cnt = 0;
modbusdata.rece_flag = 0;
return ERR_CRC; //宏定义ERR_CRC = 0x03
}
}
else
{
modbusdata.rece_cnt = 0;
modbusdata.rece_flag = 0;
return ERR_ADDR;//宏定义ERR_ADDR = 0x04
}
modbusdata.rece_cnt = 0;
modbusdata.rece_flag = 0;
return 0x05;
}
/********************************************************
************各个功能码的指令格式在上面有*******************
********************************************************/
/**
*@brief Modbus功能0x03函数
*@retval 无
*/
void Modbus_ReadCMD(void)
{
uint8_t i,j;
uint16_t regaddr,reglen,r_crc;
//获取 要读取寄存器的首地址(16位)
regaddr = modbusdata.rece_buf[2];
regaddr <<=8;
regaddr |= modbusdata.rece_buf[3];
//获取 要读取寄存器的数量(16位)
reglen = modbusdata.rece_buf[4];
reglen <<=8;
reglen |= modbusdata.rece_buf[5];
//从机回复数据
i=0;
modbusdata.send_buf[i++] = modbusdata.dev_addr;
modbusdata.send_buf[i++] = 0x03;
modbusdata.send_buf[i++] = (reglen*2)%256;
for(j=0;j<reglen;j++)
{
modbusdata.send_buf[i++] = Reg[j]>>8;
modbusdata.send_buf[i++] = Reg[j];
}
r_crc = Modbus_CRC16(modbusdata.send_buf,i);
modbusdata.send_buf[i++] = r_crc>>8;
modbusdata.send_buf[i++] = r_crc;
//从机发送回复数据
for(j=0;j<i;j++)
{
Serial_SendByte(modbusdata.send_buf[j]);
}
}
/**
*@brief Modbus功能0x06函数
*@retval 无
*/
void Modbus_WriteBitCMD(void)
{
uint16_t regaddr,w_data,w_crc;
uint16_t i,j;
//获取 要写入寄存器的起始地址(16位)
regaddr = modbusdata.rece_buf[2];
regaddr <<=8;
regaddr |= modbusdata.rece_buf[3];
//获取 要写入寄存器的数据(16位)
w_data = modbusdata.rece_buf[4];
w_data <<=8;
w_data |= modbusdata.rece_buf[5];
Reg[regaddr] = w_data;
//从机回复数据
i=0;
modbusdata.send_buf[i++] = modbusdata.dev_addr;
modbusdata.send_buf[i++] = 0x06;
modbusdata.send_buf[i++] = regaddr>>8;
modbusdata.send_buf[i++] = regaddr;
modbusdata.send_buf[i++] = w_data>>8;
modbusdata.send_buf[i++] = w_data;
w_crc = Modbus_CRC16(modbusdata.send_buf,i);
modbusdata.send_buf[i++] = w_crc>>8;
modbusdata.send_buf[i++] = w_crc;
//从机发送回复数据
for(j=0;j<i;j++)
{
Serial_SendByte(modbusdata.send_buf[j]);
}
}
/**
*@brief Modbus功能0x10函数
*@retval 无
*/
void Modbus_WriteDataCMD(void)
{
uint16_t regaddr,reglen,w_crc;
uint16_t i,j;
//获取 要写入寄存器的起始地址(16位)
regaddr = modbusdata.rece_buf[2];
regaddr <<=8;
regaddr |= modbusdata.rece_buf[3];
//获取 从那个寄存器开始写入(16位)
reglen = modbusdata.rece_buf[4];
reglen <<=8;
reglen |= modbusdata.rece_buf[5];
//获取 要写入寄存器的数据
for(i=0;i<reglen;i++)
{
Reg[reglen+i] = modbusdata.rece_buf[7+i*2];
Reg[reglen+i] <<=8;
Reg[reglen+i] |= modbusdata.rece_buf[8+i*2];
}
//从机回复数据
modbusdata.send_buf[0] = modbusdata.dev_addr;
modbusdata.send_buf[1] = 0x10;
modbusdata.send_buf[2] = regaddr>>8;
modbusdata.send_buf[3] = regaddr;
modbusdata.send_buf[4] = reglen>>8;
modbusdata.send_buf[5] = reglen;
w_crc = Modbus_CRC16(modbusdata.send_buf,6);
modbusdata.send_buf[6] = w_crc>>8;
modbusdata.send_buf[7] = w_crc;
//从机发送回复数据
for(j=0;j<8;j++)
{
Serial_SendByte(modbusdata.send_buf[j]);
}
}
中断配置:
/**********************************************
//中断配置时,串口的中断优先级必须大于定时器的优先级,
//否则会出问题,目前我暂且没弄明白是哪里的问题,
//知道的朋友可以评论区告诉我
**********************************************/
void USART1_IRQHandler(void)
{
uint8_t data;
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
//接受下发数据
data = USART_ReceiveData(USART1);
if(modbusdata.rece_flag == 1)
{
return;
}
//将下发数据储存至接受缓冲区
modbusdata.rece_buf[modbusdata.rece_cnt++] = data;
modbusdata.timout = 0;
//接受一个数据后打开定时器 因为modbus rtu标准协议规定:每帧数据间隔至少为3.5个字符时间
if(modbusdata.rece_cnt == 1)
{
modbusdata.timrun = 1;
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET)
{
//判断是否打开定时器
if(modbusdata.timrun != 0)
{
modbusdata.timout++;
if(modbusdata.timout >= 8)
{
modbusdata.timrun = 0;
//接收数据完毕
modbusdata.rece_flag = 1;
}
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}
}
主函数:
#include "stm32f10x.h" // Device header
#include "UsartModbusRTU.h"
typedef struct
{
uint8_t error;
}system_t;
extern Modbus_t modbusdata;
system_t sys_data;
int main(void)
{
Serial_UsartInit();
Modbus_Init();
Modbus_TimeIRQInit();
while (1)
{
//Modbus事件轮询并且接受错误码
sys_data.error = Modbus_Event();
if(sys_data.error != 0x05)
{
switch(sys_data.error)
{
//如果错误码为ERR_ADDR:表示地址错误
case ERR_ADDR:Serial_SendByte(0x03);break;
//如果错误码为ERR_CRC:表示校验错误
case ERR_CRC:Serial_SendByte(0x04);break;
default:break;
}
}
}
}
测试结果
串口助手发送 0x03功能码
串口助手发送 0x10功能码
再次发送 0x03可以看到数据已经改变
串口助手发送 0x06功能码 但是地址给个错误地址
可以看到错误码0x04表示地址错误
在此完结谢谢大家