MODBUS移植STM32,STM32做从机

MODBUS学习日志

一、MODBUS通信协议

1、通信协议

  1. 硬件层协议:解决传输问题,相当于路
  2. 串口通信协议 : RS232、RS485、CAN总线

1.1、三种通信方式

1.1.1、单工方式(simplex)

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

1.1.2、半双工方式(需要上层软件做协议)(half-duplex)

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

1.1.3、全双工方式(full-duplex)

全双工通信允许数据同时在两个方向上传输,即有两个信道,因此允许同时进行双向传输。全双工通信是两个单工通信方式的结合,要求收发双方都有独立的接收和发送能力。全双工通信效率高,控制简单,但造价高。计算机之间的通信是全双工方式。一般的电话、手机也是全双工的系统,因为在讲话时可以听到对方的声音。

参考链接: 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的两种传输方式: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方式

​ 1、从机地址 2、功能码(127个) 3、数据1~数据n 4、校验码(CRCL、CRCH)

其中: 1~3参与CRC16校验

从机是以接收数据停止时间达到3.5个字节以上,那么就认为主机的寻址帧完成,并开始处理。

例如:波特率:9600bt/s

所以每位数据传输的时间T=1000000us/9600=104us

一字节时间位=10T=1004us(起始位 8位 停止位)(串口格式)

所以时间为:3.5*10T=3645us

2.4、ASC方式

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);          // 暂时关闭定时器的时钟,等待使用
 }

基本定时器头文件

#ifdef  BASIC_TIM6                                           // 使用基本定时器TIM6
#define BASIC_TIM                   TIM6
#define BASIC_TIM_APBxClock_FUN     RCC_APB1PeriphClockCmd
#define BASIC_TIM_CLK               RCC_APB1Periph_TIM6
#define BASIC_TIM_IRQ               TIM6_IRQn
#define BASIC_TIM_IRQHandler        TIM6_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_USART                            USART2
#define DEBUG_USART_CLK                        RCC_APB1Periph_USART2
#define DEBUG_USART_APBxClkCmd                 RCC_APB1PeriphClockCmd
#define DEBUG_USART_BAUDRATE                   9600
// USART GPIO 引脚宏定义
#define DEBUG_USART_GPIO_CLK                   RCC_APB2Periph_GPIOA
#define DEBUG_USART_GPIO_APBxClkCmd            RCC_APB2PeriphClockCmd
#define DEBUG_USART_TX_GPIO_PORT               GPIOA
#define DEBUG_USART_TX_GPIO_PIN                GPIO_Pin_2
#define DEBUG_USART_RX_GPIO_PORT               GPIOA
#define DEBUG_USART_RX_GPIO_PIN                GPIO_Pin_3
// USART GPIO 中断
#define DEBUG_USART_IRQ                        USART2_IRQn
#define DEBUG_USART_IRQHandler                 USART2_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 char   myadd;          //本设备的地址
 unsigned char   rcbuf[100];     //MODBUS接收缓冲区
 unsigned int    timout;         //MODbus的数据断续时间	
 unsigned char   recount;        //MODbus端口已经收到的数据个数
 unsigned char   timrun;         //MODbus定时器是否计时的标志
 unsigned char   reflag;         //收到一帧数据的标志
 unsigned char   Sendbuf[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、功能码程序

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<i;j++)                             //通过串口逐个发送
   Usart_SendByte( DEBUG_USART,modbus.Sendbuf[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<Reglen;j++)
	{
	  modbus.Sendbuf[i++]=Reg[Regadd+j]/256;    //发送读取数据字节数的高位 
	  modbus.Sendbuf[i++]=Reg[Regadd+j]%256;	//发送读取数据字节数的低位 
	}
	crc=crc16(modbus.Sendbuf,i);                //CRC校验
	modbus.Sendbuf[i++]=crc/256;                //发送CRC的值高位
	modbus.Sendbuf[i++]=crc%256;                //发送CRC的值低位	
	for(j=0;j<i;j++)                            //通过串口逐个发送
   Usart_SendByte( DEBUG_USART,modbus.Sendbuf[j]);
}

三、试验现象

发布了4 篇原创文章 · 获赞 5 · 访问量 391

猜你喜欢

转载自blog.csdn.net/xiaoxiaodawei/article/details/105314738