MODBUS移植STM32,STM32做从机

MODBUS学习日志 一、MODBUS通信协议 1、通信协议

  1. 硬件层协议:解决传输问题,相当于路
  2. 串口通信协议 : RS232、RS485、CAN总线
1.1、三种通信方式 1.1.1、单工方式(simplex) MODBUS移植STM32,STM32做从机
文章图片

单工通信只支持信号在一个方向上传输(正向或反向),任何时候不能改变信号的传输方向。为保证正确传送数据信号,接收端要对接收的数据进行校验,若校验出错,则通过监控信道发送请求重发的信号。此种方式适用于数据收集系统,如气象数据的收集、电话费的集中计算等。例如计算机和打印机之间的通信是单工模式,因为只有计算机向打印机传输数据,而没有相反方向的数据传输。还有在某些通信信道中,如单工无线发送等。
1.1.2、半双工方式(需要上层软件做协议)(half-duplex) MODBUS移植STM32,STM32做从机
文章图片

半双工通信允许信号在两个方向上传输,但某一时刻只允许信号在一个信道上单向传输。因此,半双工通信实际上是一种可切换方向的单工通信。此种方式适用于问讯、检索、科学计算等数据通信系统;传统的对讲机使用的就是半双工通信方式。由于对讲机传送及接收使用相同的频率,不允许同时进行。因此一方讲完后,需设法告知另一方讲话结束(例如讲完后加上’OVER’),另一方才知道可以开始讲话。
1.1.3、全双工方式(full-duplex) MODBUS移植STM32,STM32做从机
文章图片

全双工通信允许数据同时在两个方向上传输,即有两个信道,因此允许同时进行双向传输。全双工通信是两个单工通信方式的结合,要求收发双方都有独立的接收和发送能力。全双工通信效率高,控制简单,但造价高。计算机之间的通信是全双工方式。一般的电话、手机也是全双工的系统,因为在讲话时可以听到对方的声音。
参考链接: https://blog.csdn.net/iningwei/article/details/100134783
1.2、主从模式:
主从模式,是数据库设计模式中最常见、也是大家日常设计工作中用的最多的一种模式,它描述了两个表之间的主从关系,是典型的“一对多”关系。
规定要求:
1. 系统中只有一个设备时主机 2. 系统中的所有从机不可以主动向主机发数据 3. 系统中的主机和所有从机上电后都处于监听状态 4. 任何一次的数据交换都要由主机发起 4.1、将自己转为发送状态 4.2、主机按照预先约定的格式,发出寻址数据帧 4.3、恢复自己的接受状态,等待所寻址的从机响应

1.3、软件层协议:解决传输的目的 1.3.1、主从模式
  1. 整个系统只能有一个主机,每个从机必须有一个唯一的地址(0~247)
  2. 其中0号地址位广播地址:主机向0号地址的设备发数据包,也就是要把该数据包发给所有的从设备。0号地址的数据包所有从机是不回应的。
2、MODBUS的主机寻址帧的格式
MODBUS移植STM32,STM32做从机
文章图片

>MODBUS的两种传输方式:RTU方式和ASC方式 > >RTU方式:也叫十六进制 例如:发送0x03:0000 0011 > >RTU方式:也叫十六进制 例如:发送0x03:0000 0011 > >ASC方式:0x03{发送0 :0x30:0011 0000 }{ 发送3:0x33:0011 0011} > >所以ASC的通信效率低,但是方便调试,使用实验; 工业上都采用RTU方式,效率高

2.3、RTU方式 MODBUS移植STM32,STM32做从机
文章图片

? 1、从机地址 2、功能码(127个) 3、数据1~数据n 4、校验码(CRCL、CRCH)
MODBUS移植STM32,STM32做从机
文章图片
其中: 1~3参与CRC16校验
从机是以接收数据停止时间达到3.5个字节以上,那么就认为主机的寻址帧完成,并开始处理。
例如:波特率:9600bt/s
所以每位数据传输的时间T=1000000us/9600=104us
一字节时间位=10T=1004us(起始位 8位 停止位)(串口格式)
所以时间为:3.5*10T=3645us
2.4、ASC方式 MODBUS移植STM32,STM32做从机
文章图片

1、: 2、地址 3、功能码 4、数据1~数据n 5、(地址数据)采用LRC校验=((地址+功能码+数据1数据n)%256)+1=(0255) 6、13 10(回车 换行)
2.5、CRC简述
1.将一个 16 位寄存器装入十六进制 FFFF (全 1). 将之称作 CRC 寄存器.
2.将报文的第一个 8 位字节与 16 位 CRC 寄存器的低字节异或,结果置于 CRC 寄存器.
3.将 CRC 寄存器右移 1 位 (向 LSB 方向), MSB 充零. 提取并检测 LSB.
4.(如果 LSB 为 0): 重复步骤 3 (另一次移位).
(如果 LSB 为 1): 对 CRC 寄存器异或多项式值 0xA001 (1010 0000 0000 0001).
5.重复步骤 3 和 4,直到完成 8 次移位。当做完此操作后,将完成对 8 位字节的完整操作。
6.对报文中的下一个字节重复步骤 2 到 5,继续此操作直至所有报文被处理完毕。
7.CRC 寄存器中的最终内容为 CRC 值.
8.当放置 CRC 值于报文时,高低字节必须交换。
3、从设备的回应数据包格式
  1. 回应数据包和主机查询的数据包格式包是一致的
  2. 正常回应时,功能码与主机发的功能码一致(1~127)
  3. 异常的回应,功能码要在收到的功能码基础上加上128 例如:发 0x03 收:0x03 +128
4、MODBUS从机协议实现
  1. 硬件上具备串口
  2. 硬件上需要定时器(精确到毫秒级)
?
二、MODBUS移植STM32流程 1、系统初始化设计流程
开始 配置系统时钟为72MHZ 配置基本定时器为1MS 配置串口为9600bts,并开启接受中断 使能定时器和串口中断,串口中断优先级>定时器中断优先级 1.1、配置系统时钟
SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9); //设置系统时钟,外部设置为72MHZ,内部设置为64MHZ

1.2、配置基本定时器的步骤
void BASIC_TIM_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; BASIC_TIM_APBxClock_FUN(BASIC_TIM_CLK, ENABLE); //开启定时器时钟,即内部时钟CK_INT=72M TIM_TimeBaseStructure.TIM_Period=TIM6_Period; //自动重装载寄存器周的值(计数值) // 累计TIM_Period 个频率后产生一个更新或者中断 // 时钟预分频数为71,则驱动计数器的时钟CK_CNT = CK_INT / (71+1)=1M TIM_TimeBaseStructure.TIM_Prescaler= TIM6_Prescaler; //TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; // 时钟分频因子 ,基本定时器没有,不用管 //TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; // 计数器计数模式,基本定时器只能向上计数,没有计数模式的设置 //TIM_TimeBaseStructure.TIM_RepetitionCounter=0; // 重复计数器的值,基本定时器没有,不用管 TIM_TimeBaseInit(BASIC_TIM, &TIM_TimeBaseStructure); // 初始化定时器 TIM_ClearFlag(BASIC_TIM, TIM_FLAG_Update); // 清除计数器中断标志位 TIM_ITConfig(BASIC_TIM,TIM_IT_Update,ENABLE); // 开启计数器中断 TIM_Cmd(BASIC_TIM, ENABLE); // 使能计数器 //BASIC_TIM_APBxClock_FUN(BASIC_TIM_CLK, DISABLE); // 暂时关闭定时器的时钟,等待使用 }

基本定时器头文件
#ifdefBASIC_TIM6// 使用基本定时器TIM6 #define BASIC_TIMTIM6 #define BASIC_TIM_APBxClock_FUNRCC_APB1PeriphClockCmd #define BASIC_TIM_CLKRCC_APB1Periph_TIM6 #define BASIC_TIM_IRQTIM6_IRQn #define BASIC_TIM_IRQHandlerTIM6_IRQHandler #define TIM6_Period(1000) #define TIM6_Prescaler(72-1)

定时器中断函数
void BASIC_TIM_IRQHandler (void)//定时器中断函数 { if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET ) { TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update); } }

配置定时器中断使能
void ALL_NVIC_Init(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 设置中断组为1 NVIC_InitStructure.NVIC_IRQChannel = BASIC_TIM_IRQ ; // 设置中断来源 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 设置主优先级为 1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 设置抢占优先级为3 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); C }

主程序结构
int main(void) { SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9); //设置系统时钟,外部设置为72MHZ,内部设置为64MHZ BASIC_TIM_Config(); //定时器配置为1MS ALL_NVIC_Init(); //配置中断优先级 }

运行程序,判断是否到定时器中断中的断点完成定时器1MS定时
1.3、配置串口GPIO口
void USART_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE); // 打开串口GPIO 的时钟 DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE); // 打开串口外设的时钟 // 将USART1 Tx 的GPIO 配置为推挽复用模式 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure); // 将USART Rx 的GPIO 配置为浮空输入模式 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure); // 配置串口的工作参数 USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE; // 配置波特率 USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 配置 针数据字长 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(DEBUG_USART, &USART_InitStructure); // 完成串口的初始化配置 USART_ITConfig(DEBUG_USART, USART_IT_RXNE, ENABLE); // 使能串口接收中断 USART_Cmd(DEBUG_USART, ENABLE); // 使能串口 }

串口头文件
// 串口2-USART1 #define DEBUG_USARTUSART2 #define DEBUG_USART_CLKRCC_APB1Periph_USART2 #define DEBUG_USART_APBxClkCmdRCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE9600 // USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLKRCC_APB2Periph_GPIOA #define DEBUG_USART_GPIO_APBxClkCmdRCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORTGPIOA #define DEBUG_USART_TX_GPIO_PINGPIO_Pin_2 #define DEBUG_USART_RX_GPIO_PORTGPIOA #define DEBUG_USART_RX_GPIO_PINGPIO_Pin_3 // USART GPIO 中断 #define DEBUG_USART_IRQUSART2_IRQn #define DEBUG_USART_IRQHandlerUSART2_IRQHandler

串口中断函数
void DEBUG_USART_IRQHandler(void) { uint8_t ucTemp; if (USART_GetITStatus(DEBUG_USART,USART_IT_RXNE)!=RESET)//判断是否有数据接收 { ucTemp = USART_ReceiveData( DEBUG_USART ); //将接收的一个字节保存 USART_SendData(DEBUG_USART,ucTemp); //保存后发送调试助手, } }

串口中断使能函数
void ALL_NVIC_Init(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 设置中断组为1 NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ ; // 设置中断来源 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 设置主优先级为 1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 设置抢占优先级为0 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }

主函数结构
int main(void) { SysClock_Configuration(RCC_PLLSource_HSE_Div1,RCC_CFGR_PLLMULL9); //设置系统时钟,外部设置为72MHZ,内部设置为64MHZ USART_Config(); ALL_NVIC_Init(); }

运行程序,判断是否到串口中断中的断点完成串口配置
1.4、配置定时器作用于串口
当串口接受完数据,开启定时器计数,当时间>8T就开始处理数据
此时需要配置MODBUS的参数,如下
typedef struct { unsigned charmyadd; //本设备的地址 unsigned charrcbuf[100]; //MODBUS接收缓冲区 unsigned inttimout; //MODbus的数据断续时间 unsigned charrecount; //MODbus端口已经收到的数据个数 unsigned chartimrun; //MODbus定时器是否计时的标志 unsigned charreflag; //收到一帧数据的标志 unsigned charSendbuf[100]; //MODbus发送缓冲区 }MODBUS; extern MODBUS modbus; //声明全局变量,然后在C文件中调用

串口中断配置如下
void DEBUG_USART_IRQHandler(void) { uint8_t ucTemp; if (USART_GetITStatus(DEBUG_USART,USART_IT_RXNE)!=RESET)//判断是否有数据接收 { ucTemp = USART_ReceiveData( DEBUG_USART ); //将接收的一个字节保存 modbus.rcbuf[modbus.recount++]=ucTemp; //保存到MODBUS的接收缓存区 modbus.timout=0; //串口接收数据的过程中,定时器不计时 if(modbus.recount==1)//收到主机发来的一帧数据的第一字节 { modbus.timrun=1; //启动定时 } } }

定时器中断配置如下
void BASIC_TIM_IRQHandler (void)//定时器中断函数 { if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET ) { if(modbus.timrun!=0)//串口发送数据是否结束,结束就让定时器定时 { modbus.timout++; //定时器定时1毫秒,并开始记时 if(modbus.timout>=8)//间隔时间达到了时间,假设为8T,实际3.5T即可 { modbus.timrun=0; //关闭定时器--停止定时 modbus.reflag=1; //收到一帧数据,开始处理数据 } } TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update); } }

运行程序,判断是否进入到定时器处理modbus.reflag=1;
1.5、配置处理数据包程序
void Mosbus_Event(void) { u16 crc; u16 rccrc; if(modbus.reflag==0)//没有收到MODbus的数据包 { return ; //没有收到处理指令,继续等待下一条数据 } crc= crc16(&modbus.rcbuf[0], modbus.recount-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)//广播地址,不处理 { } }//数据包不符合CRC校验规则 modbus.recount=0; //清除缓存计数 modbus.reflag=0; //重新开始执行处理函数C }

处理流程图
modbus.reflag==0 modbus.reflag==1 不符合 符合 不是 广播地址 是 开始 是否接受完数据,并开始处理 计算校验码 数据包是否符合CRC校验规则 确认数据发给本设备地址 数据包是否发给本设备地址 分析功能码功能 按功能处理数据 结束处理并将计数和使能关掉 1.6、功能码程序 MODBUS移植STM32,STM32做从机
文章图片
6号功能码程序
void Modbud_fun6()//6号功能码处理,写寄存器 { unsigned int Regadd; unsigned int val; unsigned int 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; //发送CRC的值高位 modbus.Sendbuf[i++]=crc%256; //发送CRC的值低位 for(j=0; j

3号功能码程序
void Modbud_fun3(void)//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; modbus.Sendbuf[i++]=byte%256; //发送要返回的数据字节数 for(j=0; j

三、试验现象 【MODBUS移植STM32,STM32做从机】MODBUS移植STM32,STM32做从机
文章图片

    推荐阅读