一文看懂Modbus通信协议(下)
【一文看懂Modbus通信协议(下)】目录
前言
1. 硬件介绍
1.2 硬件电路介绍
1.2 硬件通信平台
2. 软件介绍
2.1 定时器程序设计
2.1.1 配置时钟函数
2.1.2 定时器中断服务子程序
2.2 串口程序设计
2.2.1 配置串口函数:
2.2.2 初始化中断服务子程序:
2.2.3 串口中断响应事件:
2.3 modbus程序编写
2.2.1 crc16较验程序
2.3.2 Modbus宏定义
2.3.3 Modbus函数初始化
2.3.4 Modbus事件函数
2.3.5 Modbus读功能码处理
2.3.6 Modbus写单个寄存器功能码处理
2.3.7 主函数
前言 首先回顾一下上一节介绍的Modbus通信协议基本理论,首先介绍了Modbus通信协议的主从通信模式特点,分析了Modbus通信的传输特点;其次介绍了两种Modbus通信协议基本的数据格式:Modbus-RTU协议和Modbus-ASCLL协议。Modbus通信协议是在RS-485串口实验的基础上实现的,简单说就是首先要实现RS-485的串口通信,对所收发的数据串按照Modbus的规则编写(比作数据的加密处理)因此在程序编写上主要分为3个步骤:1.实现1ms中断计时的定时器;2.实现发送和接收数据的串口;3.Modbus程序编写。本节将本着从理论落实到实践的角度对Modbus通信协议进行代码实现。
1. 硬件介绍
1.2 硬件电路介绍 微处理器选用:STM32F103;
RS485_IN和RS485_OUT为收发引脚:选用MCU的PB10和PB11引脚作为RS-485的接收引脚和发送引脚;
RS485_DE为收发状态控制引脚:当RS485_DE为高电平时,芯片处于发送状态;当RS485_DE为低电平时,芯片处于接收状态;
阻抗匹配电路:R44、R45、R46为阻抗匹配电阻
文章图片
1.2 硬件通信平台 Modbus通信实验平台搭建如下:将USB串口准换成TTL电平,再将TTL电平转换为RS-485差分信号之后连接于STM32从机1。
文章图片
Modbus通信实验调试软件如下:
下载链接链接:https://pan.baidu.com/s/1ccJkBmZJQhuChypKy-qoug
提取码:ppe1
文章图片
2. 软件介绍 要想实现Modbus的程序,首先应当完成三件事:
(1)实现1ms中断计时的定时器;
(2)实现发送和接收数据的串口;
(3)Modbus程序编写。
2.1 定时器程序设计 利用TIM2实现1ms的定时中断功能。
2.1.1 配置时钟函数
/******************************************************************
功能: 配置时钟函数
******************************************************************/
//通用定时器 2 中断初始化
void Timer2_Init()//1ms产生1次更新事件
{
TIM_TimeBaseInitTypeDefTIM_TimeBaseStructure;
//结构体类型初始化:包含自动重装载值,分频系数,计数方式
//①TIM2时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_DeInit(TIM2);
//定时器TIM2初始化
TIM_TimeBaseStructure.TIM_Period=1000-1;
// 自动重装载周期1ms
TIM_TimeBaseStructure.TIM_Prescaler=72-1;
// 分频系数72M/72=1MHZ-->1us
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
//设置时钟分频因子为不分频
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;
//计数方式为向上计数
//②初始化定时器参数
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
//③设置TIM2允许更新中断
TIM_ITConfig(TIM2, TIM_IT_Update,ENABLE);
//使能TIM2更新中断
//④使能TIM2
TIM_Cmd(TIM2,ENABLE);
}
2.1.2 定时器中断服务子程序
/******************************************************************
功能: 定时器中断服务子程序
******************************************************************/
void TIM2_IRQHandler()//定时器2的中断服务子函数1ms一次中断
{
u8 st;
st= TIM_GetFlagStatus(TIM2, TIM_FLAG_Update);
//检测TIM2中断更新标志位
if(st==SET)//如果TIM2满足中断标志
{
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
//清除TIM2中断更新标志位
//每一毫秒所要执行的任务
if(modbus.timrun!=0)
{
modbus.timout++;
if(modbus.timout>=4)//间隔时间达到了4毫秒时间
{
modbus.timrun=0;
//关闭定时器--停止定时
modbus.reflag=1;
//收到一帧数据
}
}
}
}
2.2 串口程序设计 从库函数操作层面结合寄存器的描述来设置串口,以达到我们最基本的通信功能。串口设置的一般步骤可以总结为如下几个步骤:
(1)串口时钟使能, GPIO 时钟使能;
(2)串口复位;
(3)GPIO 端口模式设置;
(4)串口参数初始化;
(5)开启中断并且初始化 NVIC;
(6)使能串口;
(7)编写中断处理函数。
2.2.1 配置串口函数:
void RS485_Init()
{
GPIO_InitTypeDefGPIO_InitStructure;
USART_InitTypeDefUSART_InitStructure;
//①串口时钟使能, GPIO 时钟使能,复用时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE);
//开启USART2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
//开启USART2时钟
//②串口复位
USART_DeInit(USART2);
//串口2复位
//RS485DE引脚初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP;
//复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
//初始化 GPIOA.5
RS485_RT_0;
//使MAX485芯片处于接收状态(收发控制引脚)
//③GPIO端口模式设置
//初始化485串口引脚以及串口配置
//USART1_TXPB.10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
//复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
//初始化 GPIOA.2
//USART1_RX PB.11
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
//初始化 GPIOA.3
//④串口参数初始化、结构体指针成员变量
USART_InitStructure.USART_BaudRate = 9600;
//设置波特率为9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
//字长为 8 位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;
//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;
//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
//收发模式
USART_Init(USART2, &USART_InitStructure);
//串口参数初始化
//⑤开启中断
USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
//开启串口响应中断,USART_IT_RXNE接收到数据中断
//⑥使能串口
USART_Cmd(USART2, ENABLE);
//串口使能
USART_ClearFlag(USART2,USART_FLAG_TC );
//清除串口TC发送完成中断标志
2.2.2 初始化中断服务子程序:
/******************************************************************
功能: 初始化NVIC
******************************************************************/
//⑤初始化NVIC(定时器中断+串口中断)
void NVIC_Init()
{
NVIC_InitTypeDefNVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
//中断优先级NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
//定时器产生更新事件中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;
//抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
//子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//IRQ 通道使能
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;
//抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
//子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//IRQ 通道使能
NVIC_Init(&NVIC_InitStructure);
}
2.2.3 串口中断响应事件:
/******************************************************************
功能: Modbus3字节接收中断处理
******************************************************************/
void USART2_IRQHandler() //MODBUS字节接收中断
{
u8 st,sbuf;
st=USART_GetITStatus(USART2, USART_IT_RXNE);
//判断读寄存器是否非空(RXNE)
if(st==SET)//返回值是 SET,说明是串口接收到数据完成中断发生
{
sbuf=USART2->DR;
if( modbus.reflag==1)//有数据包正在处理
{
return ;
}
modbus.rcbuf[modbus.recount++]=sbuf;
//利用数组存放接收的数据
modbus.timout=0;
if(modbus.recount==1)//收到主机发来的一帧数据的第一字节
{
modbus.timrun=1;
//启动定时
}
}
}
2.3 modbus程序编写 2.2.1 crc16较验程序
根据crc16的高位字节值表和低位字节值表编写校验程序。
/******************************************************************
功能: CRC16校验
******************************************************************/
uint crc16( uchar *puchMsg, uint usDataLen )
{
uchar uchCRCHi = 0xFF ;
// 高CRC字节初始化
uchar uchCRCLo = 0xFF ;
// 低CRC 字节初始化
unsigned long uIndex ;
// CRC循环中的索引while ( usDataLen-- )// 传输消息缓冲区
{
uIndex = uchCRCHi ^ *puchMsg++ ;
// 计算CRC
uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ;
uchCRCLo = auchCRCLo[uIndex] ;
}return ( uchCRCHi << 8 | uchCRCLo ) ;
}
2.3.2 Modbus宏定义
#define RS485_RT_1 GPIO_SetBits(GPIOA, GPIO_Pin_5)//485发送状态
#define RS485_RT_0 GPIO_ResetBits(GPIOA, GPIO_Pin_5)//485置接收状态
typedef struct
{
u8 myadd;
//本设备的地址
u8 rcbuf[64];
//Modbus接收缓冲区64个字节
u16 timout;
//Modbus的数据断续时间
u8 recount;
//Modbus端口已经收到的数据个数
u8 timrun;
//Modbus定时器是否计时的标志
u8 reflag;
//收到一帧数据的标志
u8 Sendbuf[64];
//Modbus发送缓冲区
}
MODBUS;
2.3.3 Modbus函数初始化
/******************************************************************
功能: Modbus函数初始化
******************************************************************/
void Modbus_Init()
{
modbus.myadd=4;
//本从设备的地址
modbus.timrun=0;
//Modbus定时器停止计时
RS485_Init();
}
2.3.4 Modbus事件函数
/******************************************************************
功能: Modbus事件函数
******************************************************************/
void Mosbus_Event()
{
u16 crc;
u16 rccrc;
if(modbus.reflag==0)//没有收到Modbus的数据
{
return ;
}
crc= crc16(&modbus.rcbuf[0], modbus.recount-2);
//计算校验码,-2去除两位校验码
rccrc=modbus.rcbuf[modbus.recount-2]*256 + modbus.rcbuf[modbus.recount-1];
//收到的校验码
if(crc == rccrc)//数据包符号CRC校验规则
{
if(modbus.rcbuf[0] == modbus.myadd)//确认数据包是否是发给本设备的 确认接收的地址是本机地址
{
switch(modbus.rcbuf[1])//分析功能码
{
case 0:break;
case 1:break;
case 2:break;
case 3:Modbud_fun3();
break;
//3号功能码处理
case 4:break;
case 5:break;
case 6:Modbud_fun6();
break;
//6号功能码处理
case 7:break;
//....
}
}
else if(modbus.rcbuf[0] == 0)//如果是广播地址则不处理
{
}
}
modbus.recount=0;
modbus.reflag=0;
}
2.3.5 Modbus读功能码处理
/******************************************************************
功能: Modbus3号功能码处理
******************************************************************/
void Modbud_fun3()//3号功能码处理---主机要读取本从机的寄存器
{
u16 Regadd;
//寄存器起始地址
u16 Reglen;
//寄存器个数
u16 byte;
u16 i,j;
u16 crc;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3];
//得到要读取的寄存器的首地址
Reglen=modbus.rcbuf[4]*256+modbus.rcbuf[5];
//得到要读取的寄存器的数量
i=0;
modbus.Sendbuf[i++]=modbus.myadd;
//本设备地址
modbus.Sendbuf[i++]=0x03;
//功能码
byte=Reglen*2;
//要返回的数据字节数
modbus.Sendbuf[i++]=byte%256;
for(j=0;
j
2.3.6 Modbus写单个寄存器功能码处理
/******************************************************************
功能: Modbus6写单个寄存器功能码处理
******************************************************************/
void Modbud_fun6()//6号功能码处理
{
u16 Regadd;
u16 val;
u16 i,crc,j;
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;
crc=crc16(modbus.Sendbuf,i);
modbus.Sendbuf[i++]=crc/256;
//
modbus.Sendbuf[i++]=crc%256;
RS485_RT_1;
for(j=0;
j
2.3.7 主函数
/******************************************************************
功能: 主函数
******************************************************************/
u16 Reg[]={0x0000,//本设备寄存器中的值
0x0001,
0x0002,
0x0003,
0x0004,
0x0005,
0x0006,
0x0007,
0x0008,
0x0009,
0x000A,
};
int main()
{
Timer2_Init();
//初始化定时器Timer2
Mosbus_Init();
//初始化定时器中断
Isr_Init();
while(1)
{
Mosbus_Event();
//处理MODbus数据
}
}
往期博客:
一文看懂Modbus通信协议(上)
通信的硬件层协议和软件层协议
RS-232、RS-485、RS-422通信接口标准介绍
推荐阅读
- mysql|一文深入理解mysql
- 数据技术|一文了解Gauss数据库(开发历程、OLTP&OLAP特点、行式&列式存储,及与Oracle和AWS对比)
- 一文弄懂MySQL中redo|一文弄懂MySQL中redo log与binlog的区别
- c语言|一文搞懂栈(stack)、堆(heap)、单片机裸机内存管理malloc
- 网络|一文彻底搞懂前端监控
- 【SpringCloud-Alibaba系列教程】8.一文学会使用sentinel
- 2020买重疾险看这篇就够了,一文明白重疾险怎么买|2020买重疾险看这篇就够了,一文明白重疾险怎么买,更划算
- 《繁凡的深度学习笔记》|一文绝对让你完全弄懂信息熵、相对熵、交叉熵的意义《繁凡的深度学习笔记》第 3 章 分类问题与信息论基础(中)(DL笔记整理
- 开源生态|GPL、MIT、Apache...开发者如何选择开源协议(一文讲清根本区别)
- 一二三四五,手把手教你看懂电影《无问西东》