一文看懂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为阻抗匹配电阻
一文看懂Modbus通信协议(下)
文章图片

1.2 硬件通信平台 Modbus通信实验平台搭建如下:将USB串口准换成TTL电平,再将TTL电平转换为RS-485差分信号之后连接于STM32从机1。
一文看懂Modbus通信协议(下)
文章图片

Modbus通信实验调试软件如下:
下载链接链接:https://pan.baidu.com/s/1ccJkBmZJQhuChypKy-qoug
提取码:ppe1
一文看懂Modbus通信协议(下)
文章图片

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通信接口标准介绍

    推荐阅读