1.前言
因项目中需要用到485电路以及多设备通讯,采用Modbus协议通讯方式,本文写的目的就是记录笔记也提供给初学者一点参考。里面的内容可能会有错误,仅供参考。
2.485电路
上图是项目中的电路,也算一个最基本的485电路,没什么好讲的,这个博主讲的不错,可以参考他的。
3.Modbus协议
学习以前参考以下博文链接:
3.1 Modbus协议简介
Modbus协议是一种应用层报文传输协议,协议本身并没有定义物理层,所以支持多种电气接口,直接可以理解成他是软件层面的,各种的电气接口比如RS232、RS485、TCP/IP等,他们是硬件层面。他们之间互不影响。
3.2 Modbus通讯过程
Modbus是一主多从的通信协议
Modbus通信中只有一个设备可以发送请求。其他从设备接收主机发送的数据来进行响应,从机是任何外围设备,如I/O传感器,阀门,网络驱动器,或其他测量类型的设备。从站处理信息和使用Modbus将其数据发送给主站。
也就是说,不能Modbus同步进行通信,主机在同一时间内只能向一个从机发送请求,总线上每次只有一个数据进行传输,即主机发送,从机应答,主机不发送,总线上就没有数据通信。
从机不会自己发送消息给主站,只能回复从主机发送的消息请求。
3.3 Modbus功能码
Modbus协议同时规定了二十几种功能码,但是常用的只有3种,用于对存储区的读写,如下表所示:
我们主要就是用到03h读取从机寄存器的数据,06h对从机指定寄存器写入指定数据,10h对从机多个寄存器写入数据。
3.4 Modbus协议格式
我们主要就是学习它的协议格式,主要用到3种功能码,也就对应与3种发送数据的格式。
3.4.1 03H功能码-读取数据-协议格式
比如 :主机发送 01 03 00 01 00 01 D5 CA
主机一共发送8个字节。
0x01:表示主机要与从机地址是0x01的设备进行通讯
0x03:功能码,代表我们发送这个指令的作用是什么,03表示我们要读取从机的数据
0x00:要读取从机寄存器地址的高位
0x01:要读取从机寄存器地址的低位
0x00:要读取从机寄存器数量的高位
0x01:要读取从机寄存器数量的低位
D5:前6位数据效验的低位
CA:前6位数据效验的高位
总得来说这段代码的含义是:查询从机地址为0x01的0x0001寄存器地址的0x0001个数据。
从机收到这段协议后,应回复如下格式 01 03 02 00 03 F8 48
从机一共发送7个字节
0x01:表示主机要与从机地址是0x01的设备进行通讯
0x03:功能码,代表我们发送这个指令的作用是什么,03表示我们要读取从机的数据
0x02:返回的数据个数(要读取的寄存器个数*2)——>返回数据的字节都是寄存器的2倍
0x00:从机返回数据的高位
0x03:从机返回数据的低位
F8 48:前面几位数据的效验码
3.4.2 06H功能码-写入数据-单寄存器-协议格式
比如 :主机发送 01 06 00 01 00 17 98 04
主机一共发送8个字节。
0x01:表示主机要与从机地址是0x01的设备进行通讯
0x03:功能码,代表我们发送这个指令的作用是什么,06表示我们要向从机写入数据
0x00:要写入从机寄存器地址的高位
0x01:要写入从机寄存器地址的低位
0x00:要写入从机数据的高位
0x17:要写入从机数据的低位
98 04:前6位数据效验码
总得来说这段代码的含义是:向从机地址为0x01的0x0001地址写入数据0x0017
从机回复格式:01 06 00 01 00 17
从机回复的内容和主机发送的一样。
3.4.3 10H功能码-写入数据-多个寄存器-协议格式
比如 :主机发送 : 01 10 00 05 00 02 04 01 02 03 04 92 9F
主机一共发送11个字节。
0x01:表示主机要与从机地址是0x01的设备进行通讯
0x10:功能码,10表示我们要向从机多个寄存器写入数据
0x00:要写入从机寄存器地址的起始地址的高位
0x05:要写入从机寄存器地址的起始地址的低位
0x00:要写入寄存器个数的高位
0x02:要写入寄存器个数的低位
0x04:要写入的字节数
01 02:写入第一个寄存器的数据
03 04:写入第二个寄存器的数据
92 9F:前面数据效验码
从机回复:01 10 00 05 00 02 51 C9
从机返回的数据可以理解为就是主机发送的前6个字节加上自己数据的效验码。表面从机01从地址0x0005开始写入2个寄存器数据。
到这样发送数据协议格式介绍完毕。
4.STM32+485电路+Modbus协议代码
Modbus只是个协议,485电路时硬件层面,通讯还是串口通讯。
连线就是485的A B连接485转USB的A B脚,485芯片的连接如上面电路所示,连接STM32串口1。
4.1 STM32串口配置
4.1.1 串口初始化
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
4.1.2 串口发送封装代码
我们发送数据的格式主要就是以数组的方式。
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
4.1.3 串口中断接收
主要是将接收到的数据依次存放到对应的数组中,当modbus.reflag==1表示还有数据正在处理中,反之则进行数据存储,当开始存储第二个数据时开启定时器计时,主要目的判断接收数据是否完毕,如果超过一段时间没有数据,则表明这一次数据接收完毕
void USART1_IRQHandler(void)
{
u8 st,Res;
st = USART_GetITStatus(USART1, USART_IT_RXNE);
if(st == SET)//接收中断
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
if( modbus.reflag==1) //有数据包正在处理
{
return ;
}
modbus.rcbuf[modbus.recount++] = Res;
//USART_SendData(USART2, Res);//接受到数据之后返回给串口1
modbus.timout = 0;
if(modbus.recount == 1) //已经收到了第二个字符数据
{
modbus.timrun = 1; //开启modbus定时器计时
}
}
}
4.2定时器2配置
4.2.1 定时器初始化
配置定时器1ms进入一次中断。这里主要说明下1ms怎么配置的。
定时时间 = [(PSC +1) *(ARR+1)]/72M
这里的PSC指定就是分频倍数,ARR是溢出数,72M是时钟频率。
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 1000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //定时1ms
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2, ENABLE);
}
4.2.2 定时中断
定时器设置1ms进入中断1次,运行时间不为0的情况下开始计时,超过8ms则表明这一次接收数据完毕,将数据接收结束标志位置1处理(modbus.reflag = 1),当数据接收完毕,则STM32可以对接收到的数据进行数据分析和处理执行相应的操作了
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
if(modbus.timrun != 0)//运行时间!=0表明
{
modbus.timout++;
if(modbus.timout >=8)
{
modbus.timrun = 0;
modbus.reflag = 1;//接收数据完毕
}
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
4.3 Modbus协议处理代码
4.3.1 Modbus数据结构体
STM32作为从机时需要用到的数据。
typedef struct
{
//作为从机时使用
u8 myadd; //本设备从机地址
u8 rcbuf[100]; //modbus接受缓冲区
u8 timout; //modbus数据持续时间
u8 recount; //modbus端口接收到的数据个数
u8 timrun; //modbus定时器是否计时标志
u8 reflag; //modbus一帧数据接受完成标志位
u8 sendbuf[100]; //modbus接发送缓冲区
}MODBUS;
4.3.2 功能码03H处理函数
void Modbus_Func3(void)
{
uint16_t Regadd,Reglen;
uint8_t i,j;
//得到要读取寄存器的首地址
Regadd = modbus.rcbuf[2]*256+modbus.rcbuf[3];//读取的首地址
//得到要读取寄存器的数据长度
Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//读取的寄存器个数
//发送回应数据包
i = 0;
modbus.sendbuf[i++] = modbus.myadd; //ID号:发送本机设备地址
modbus.sendbuf[i++] = 0x03; //发送功能码
modbus.sendbuf[i++] = ((Reglen*2)%256); //返回字节个数
for(j=0;j<Reglen;j++) //返回数据
{
//reg是提前定义好的16位数组(模仿寄存器)
modbus.sendbuf[i++] = Reg[Regadd+j]/256;//高位数据
modbus.sendbuf[i++] = Reg[Regadd+j]%256;//低位数据
}
Modbus_CRC16(modbus.sendbuf,i); //计算要返回数据的CRC
modbus.sendbuf[i++] = Modbus_RCR[0];
modbus.sendbuf[i++] = Modbus_RCR[1];
//数据包打包完成
// 开始返回Modbus数据
Serial_SendArray(modbus.sendbuf,i);//发送从机数据
}
4.3.2.1 实验过程1
串口助手发送: 01 03 00 03 00 01 串口助手会自动计算前面的效验值,一起发送给STM32从机。查询寄存器地址为0x0003的1个数据 。
从机返回的是 :2个字节,指定地址的数据为0x0004。数据返回没问题
现在查询2个数据,发送 01 03 00 03 00 02
从机返回的是 :4个字节,指定地址的数据为0x0004和0x0005,数据返回没问题
4.3.3 功能码06H处理函数
void Modbus_Func6(void)
{
u16 Regadd;//地址16位
u16 val;//值
u16 i;
i=0;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要修改的地址
val=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //修改后的值(要写入的数据)
Reg[Regadd]=val; //修改本设备相应的寄存器
//以下为回应主机
modbus.sendbuf[i++]=modbus.myadd;//本设备地址
modbus.sendbuf[i++]=0x06; //功能码
modbus.sendbuf[i++]=Regadd/256;//写入的地址
modbus.sendbuf[i++]=Regadd%256;
modbus.sendbuf[i++]=val/256;//写入的数值
modbus.sendbuf[i++]=val%256;
Modbus_CRC16(modbus.sendbuf,i);//获取crc校验位
//crc校验位加入包中
modbus.sendbuf[i++] = Modbus_RCR[0];
modbus.sendbuf[i++] = Modbus_RCR[1];
//数据发送包打包完毕
Serial_SendArray(modbus.sendbuf,i);//发送从机数据
}
4.3.3.1 实验过程2
发送前我们先查询0x0001的本来的数据为多少,再进行写入,写入后再查询是否写入成功。
串口助手首先发送:01 03 00 01 00 01
串口助手再发送:01 06 00 01 00 06
串口助手最后发送:01 03 00 01 00 01
首先回复是0x0001寄存器的数据为 0x0002
其次修改0x0001寄存器数据为0x0006
最后查询0x0001寄存器的数据就是为0x0006,表明数据写入成功。
4.3.4 功能码10H处理函数
//这是往多个寄存器器中写入数据
//功能码0x10指令即十进制16
void Modbus_Func16(void)
{
uint16_t Regadd;//地址16位
uint16_t Reglen;
uint16_t i;
Regadd= modbus.rcbuf[2]*256+modbus.rcbuf[3]; //要修改内容的起始地址
Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//读取的寄存器个数
for(i=0;i<Reglen;i++)//向指定地址写数据
{
//接收数据的第7位开始是数据接收位
Reg[Regadd+i] = modbus.rcbuf[7+i*2]*256 + modbus.rcbuf[8+i*2];
}
//数据写入完成,下面进行打包回复数据
//回应主机的内容
modbus.sendbuf[0]=modbus.rcbuf[0];//本设备地址
modbus.sendbuf[1]=modbus.rcbuf[1]; //功能码
modbus.sendbuf[2]=modbus.rcbuf[2];//写入的高位地址
modbus.sendbuf[3]=modbus.rcbuf[3];//写入的低位地址
modbus.sendbuf[4]=modbus.rcbuf[4]; //寄存器个数高位
modbus.sendbuf[5]=modbus.rcbuf[5];//寄存器个数低位
Modbus_CRC16(modbus.sendbuf,6);//获取crc校验位
modbus.sendbuf[6]=Modbus_RCR[0]; //crc校验位加入包中
modbus.sendbuf[7]=Modbus_RCR[1]; //crc校验位加入包中
//数据发送包打包完毕
//发送数据包
Serial_SendArray(modbus.sendbuf,8);
}
4.3.4.1 实验过程3
发送前我们先查询0x0001寄存器地址的2个数据为多少,再进行写入,写入后再查询是否写入成功。
串口助手首先发送:01 03 00 01 00 02
串口助手再发送:01 10 00 01 00 02 04 00 08 00 90
串口助手最后发送:01 03 00 01 00 02
首先回复是0x0001起始寄存器的数据为 0x0002, 0x0003
其次修改0x0001起始寄存器数据为0x0008,0x0009
最后查询0x0001起始寄存器的数据就是为0x0008,0x0009,表明数据写入成功。
4.3.5 效验函数
随便百度的一个。
unsigned int 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; //异或多项式
}
}
}
//低位在前
Modbus_RCR[0] = (unsigned char) (CRC16 & 0x00FF);
Modbus_RCR[1] = (unsigned char) ((CRC16 & 0xFF00)>>8);
return CRC16;
}
4.3.6 事件处理函数
只有到数据接收完成才能进行数据处理
- 首先判断自主计算的CRC校验位和接收到数据的校验位是否一致
- 其次判断从机地址是不是自己的地址
- 数据传输正确且从机地址正确的情况下再根据不同的功能码去执行对应的函数操作
void Modbus_Event(void)
{
//uint16_t crc,rccrc;//crc和接收到的crc
//没有收到数据包
if(modbus.reflag == 0) //如果接收未完成则返回空
{
return;
}
//收到数据包(接收完成)
//通过读到的数据帧计算CRC
//参数1是数组首地址,参数2是要计算的长度(除了CRC校验位其余全算)
Modbus_CRC16(modbus.rcbuf,modbus.recount-2); //获取CRC校验位
// 读取数据帧的CRC
//printf("%x","%x",(crc & 0x00FF),((crc & 0xFF00)>>8));
//rccrc = modbus.rcbuf[modbus.recount-2]*256+modbus.rcbuf[modbus.recount-1];//计算读取的CRC校验位
//等价于下面这条语句
//rccrc=modbus.rcbuf[modbus.recount-1]|(((u16)modbus.rcbuf[modbus.recount-2])<<8);//获取接收到的CRC
if(Modbus_RCR[0] == modbus.rcbuf[modbus.recount-2] && Modbus_RCR[1] == modbus.rcbuf[modbus.recount-1]) //CRC检验成功 开始分析包
{
if(modbus.rcbuf[0] == modbus.myadd) // 检查地址是否时自己的地址
{
switch(modbus.rcbuf[1]) //分析modbus功能码
{
case 0: break;
case 1: break;
case 2: break;
case 3: Modbus_Func3(); break;//这是读取寄存器的数据
case 4: break;
case 5: break;
case 6: Modbus_Func6(); break;//这是写入单个寄存器数据
case 7: break;
case 8: break;
case 9: break;
case 16: Modbus_Func16(); break;//写入多个寄存器数据
}
}
else if(modbus.rcbuf[0] == 0) //广播地址不予回应
{
}
}
modbus.recount = 0;//接收计数清零
modbus.reflag = 0; //接收标志清零
}
4.3.7 Modbus初始化
定义一个Reg数组,来模拟STM32作为从机寄存器的数据。
将STM32作为从机的地址为0x01
uint16_t Reg[] =
{
0x0001,
0x0002,
0x0003,
0x0004,
0x0005,
};
void Modbus_Init(void)
{
modbus.myadd = 0x01; //从机设备地址为
modbus.timrun = 0; //modbus定时器停止计算
}
4.4 main函数
int main(void)
{
Serial_Init();
Timer_Init();
Modbus_Init();
while (1)
{
Modbus_Event();
}
}
5.总结
到此,STM32作为从机的代码写完了,也验证成功。下一部分将记录STM32作为主机给另外的一个STM32作为从机进行通讯。以上内容仅供参考。