文章目录

一、本文主要内容

  • 使用STM32设备作为Modbus-RTU通信中的从机设备
  • 使用Modbus-poll模拟上位机进行数据通信
  • STM32在运行中加入波特率的在线修改
  • 加入EEPROM进行数据存储(实现断电保护,设备重新上电时恢复到断电前的状态)
  • 实现03-06-16功能码的测试
  • 实现01-05-15功能码的补充和测试

在这里插入图片描述

  1. 测试01功能码(读取8个继电器的状态)

    读多个位,有8路继电器

  2. 测试03功能码(读取多个寄存器数据)

    读取多个寄存器数据

  3. 测试05功能码(实现对继电器的控制)

    写入FF00打开,写入0000关闭

  4. 测试06功能码(实现对单个寄存器数据的读写来控制继电器)

    写入1打开,写入0关闭(或者写入其他数值实现寄存器数据的修改)

  5. 测试15功能码(实现8路继电器的全关和全闭)

    只能控制8路继电器同时开或同时关闭(未加入其他处理代码)

  6. 测试16功能码(实现继电器的闪开和闪闭)
    最初版16功能码函数可以实现控制多个继电器状态(也就是一条指令修改多个寄存器的数据)
    修改版16功能码函数实现继电器的闪开和闪闭

本文内容是基于下面这篇博客为基础进行延伸介绍

STM32+RS485+Modbus-RTU(主机模式+从机模式)-标准库/HAL库开发-链接

二、使用modbus通信协议在线修改STM32波特率

(一)STM32标准库在线修改串口波特率

1. STM32f103ZET6(其他芯片通用)

STM32在运行过程中一般而言串口的波特率是不需要进行修改的,但是在某些情况下需要对正在运行的STM32进行波特率修改,以此来实现后续的数据通信。

其中STM32标准库下修改串口波特率参考链接如下:
STM32单片机修改串口波特率-参考博文

上文参考链接介绍的也比较清楚,就是相当于重新构造了一个串口初始化函数,在原来串口初始化函数的基础之上加入串口的失能和使能,在修改串口参数时先调用串口失能函数将对应的串口失能进行关闭串口,配置完参数之后,再调用串口使能函数将串口使能把串口给重新打开。

正点原子代码的串口初始化函数如下:

void uart_init(u32 bound){
    //GPIO端口设置
		GPIO_InitTypeDef GPIO_InitStructure;//GPIO结构体指针
		USART_InitTypeDef USART_InitStructure;//串口结构体指针
		NVIC_InitTypeDef NVIC_InitStructure;//中断分组结构体指针
		//1、使能串口时钟,串口引脚时钟 
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟
		
	//2、复位串口	
		USART_DeInit(USART1);  //复位串口1
	
	//3、发送接收引脚的设置
	 //USART1_TX   PA.9(由图 可知设置为推挽复用输出)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA9
   
    //USART1_RX	  PA.10(有图可知浮空输入)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);  //初始化PA10


   //4、USART 初始化设置

		USART_InitStructure.USART_BaudRate = bound;//一般设置为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(USART1, &USART_InitStructure); //初始化串口
		
#if EN_USART1_RX		  //如果使能了接收  
   //5、Usart1 NVIC 配置
		NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
		NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
		NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
		NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
		NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器
   
	  //6、开启中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
		
#endif
		//7、使能串口
    USART_Cmd(USART1, ENABLE);                    //使能串口 

}

提取里面的串口配置部分重新改一个修改波特率函数

修改串口参数的关键点所在是修改参数前先将串口关闭(也就是串口失能),修改完串口参数之后再去将串口重新打开(串口使能),之所以如此是为了防止数据传输错误。
在这里插入图片描述
串口波特率修改函数如下:

void uart_init_reset(u32 bound)
{
	
		GPIO_InitTypeDef GPIO_InitStructure;//GPIO结构体指针
		USART_InitTypeDef USART_InitStructure;//串口结构体指针

		USART_Cmd(USART1, DISABLE);                    //失能串口 
		//4、USART 初始化设置
		USART_InitStructure.USART_BaudRate = bound;//一般设置为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(USART1, &USART_InitStructure); //初始化串口
    USART_Cmd(USART1, ENABLE);                    //使能串口 

}

如果只需要修改为某个固定的波特率,直接调用函数写入就好,比如说将波特率修改为11500,则调用修改波特率函数写入相关参数的数值为115200即可:

huart_init_reset(115200)

修改串口波特率往往是通过指令来进行修改的,不同的指令代表不同的波特率。

根据相关的指令去配置波特率。指令会有很多个,所以还需要提前准备一个波特率数组存放波特率,当通过条件判断接收到不同的控制指令时,直接将波特率数组中相对应的波特率参数写入函数中即可。

//通过串口接收到的数据自定义修改波特率
uint32_t bound_table[]={4800,9600,19200,115200};

既然是串口测试,那就可以根据串口接收到的指令去修改stm32的波特率(直接在正点原子的串口实验进行修改)
通过串口助手分别给单片机发送a,b,c,d指令,其对应的波特率如下:

a---4800--------bound_table[0]
b---9600--------bound_table[1]
c---19200-------bound_table[2]
d---115200------bound_table[3]

可以通过if-else条件语句或者switch语句进行判断,此处使用if条件语句

if(USART_RX_BUF[0]=='a')
{
	uart_init_reset(bound_table[0]);	
}
if(USART_RX_BUF[0]=='b')
{
	uart_init_reset(bound_table[1]);
}
if(USART_RX_BUF[0]=='c')
{
		uart_init_reset(bound_table[2]);	
}
if(USART_RX_BUF[0]=='d')
{
		uart_init_reset(bound_table[3]);
}

综上:正点原子串口实验修改主函数代码如下:

int main(void)
 {		
 	u8 t;
	u8 len;	
	u16 times=0;
	delay_init();	    	 //延时函数初始化	  
	NVIC_Configuration(); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	uart_init(9600);	 //串口初始化为9600
 	LED_Init();			     //LED端口初始化
	KEY_Init();          //初始化与按键连接的硬件接口
 	while(1)
	{
		if(USART_RX_STA&0x8000)//最高位是1表示一次接收完毕
		{					   
			len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度(0011 1111 1111 1111)
			if(USART_RX_BUF[0]=='a')
			{
				uart_init_reset(bound_table[0]);
			
			}
			if(USART_RX_BUF[0]=='b')
			{
				uart_init_reset(bound_table[1]);
			
			}
				if(USART_RX_BUF[0]=='c')
			{
				uart_init_reset(bound_table[2]);
			
			}
				if(USART_RX_BUF[0]=='d')
			{
				uart_init_reset(bound_table[3]);
			
			}
			
			printf("\r\n您发送的消息为:\r\n\r\n");//打印数据到串口
			for(t=0;t<len;t++)
			{
				USART_SendData(USART1, USART_RX_BUF[t]);//向串口1发送数据
				
				while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
			}
			printf("\r\n\r\n");//插入换行
			USART_RX_STA=0;
		}else
		{
			times++;
			if(times%5000==0)
			{
				printf("\r\n战舰STM32开发板 串口实验\r\n");
				printf("正点原子@ALIENTEK\r\n\r\n");
			}
			if(times%200==0)printf("请输入数据,以回车键结束\n");  
			if(times%30==0)LED0=!LED0;//闪烁LED,提示系统正在运行.
			delay_ms(10);   
		}
	}	 
 }

测试过程中使用串口助手发送相关的字符指令实现波特率的修改,修改完波特率之后记得将串口助手修改为修改后的波特率重新打开串口即可继续进行数据通信
默认波特率:9600(发送字符时记得发送回车换行也就是把发送新行打上对号)
a—4800--------bound_table[0]
b—9600--------bound_table[1]
c—19200-------bound_table[2]
d—115200------bound_table[3]

在这里插入图片描述

(二)STM32HAL库-485-modbus-rtu通信在线修改串口波特率

在这里插入图片描述

1、STM32F103ZET6芯片

(1)HAL库下参考标准库形式修改波特率

在modbus-rtu-485通信上进行修改测试(参考hal库中串口初始化函数)
使用STM32CUBEMX直接生成的串口初始化函数如下

/* USART2 init function */
void MX_USART2_UART_Init(void)
{
  huart2.Instance = USART2;
  huart2.Init.BaudRate = 115200;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart2) != HAL_OK)
  {
    Error_Handler();
  }
}

对此进行修改也是学着标准库中的样子(首先关闭串口再去修改串口参数):

由于串口2对应着485通信(stm32作为从机设备,只有在正确接收到主机的指令时才会返回相关的数据),所以为了数据通信的正确性,先将485通信使能为发送模式,然后再去修改关闭串口,修改串口2的波特率(无需手动打开串口,代码中会自动打开的)

在这里插入图片描述
其中波特率数组和缓冲数组如下:Reg数组第9个元素是波特率修改位
在这里插入图片描述

完整代码如下
在modbus-RTU对应的06功能码函数中进行修改的

// Modbus 6号功能码函数
// Modbus 主机写入寄存器值
void Modbus_Func6()  
{
  uint16_t Regadd;//地址16位
	uint16_t val;//值
	uint16_t 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=Modbus_CRC16(modbus.sendbuf,i);//获取crc校验位
	modbus.sendbuf[i++]=crc/256;  //crc校验位加入包中
	modbus.sendbuf[i++]=crc%256;
	//数据发送包打包完毕
	RS485_TX_ENABLE;;//使能485控制端(启动发送)  
	for(j=0;j<i;j++)
	{
	 Modbus_Send_Byte(modbus.sendbuf[j]);
	}
	HAL_Delay(100);
	if(Regadd==0x08)
	{
		
			printf("%d=\r\n",baud_table[val]);
			__HAL_UART_DISABLE(&huart2);
			huart2.Instance = USART2;
			huart2.Init.BaudRate = baud_table[val];
			huart2.Init.WordLength = UART_WORDLENGTH_8B;
			huart2.Init.StopBits = UART_STOPBITS_1;
			huart2.Init.Parity = UART_PARITY_NONE;
			huart2.Init.Mode = UART_MODE_TX_RX;
			huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
			huart2.Init.OverSampling = UART_OVERSAMPLING_16;
			if (HAL_UART_Init(&huart2) != HAL_OK)
			{
				Error_Handler();
			}
	}
	RS485_RX_ENABLE;//失能485控制端(改为接收)
}

通过以上方法可以实现波特率的修改,通过改变Reg数组的第九个元素的数值来修改波特率即可,第九个元素地址为:0x08,值为0,1,2,3,4,5,对应的波特率如下:

0-2400
1-4800
2-9600
3-19200
4-115200
(2)直接修改波特率寄存器(modbus通信修改串口2的波特率)

参考代码为HAL库的串口初始化函数:

hal库生成的串口初始化函数中打开途中标注的HAL_UART_Init原函数
在这里插入图片描述
在这里插入图片描述
然后用鼠标往下翻阅查看函数内容直到看到串口参数配置函数,并查看**UART_SetConfig(huart)**原函数
在这里插入图片描述
在这里插入图片描述
跳转到串口参数配置函数后,查看其原函数内容并找到函数尾部的波特率寄存器BRR,可以看到是对波特率的修改
在这里插入图片描述

参考以上代码编写串口波特率修改函数,上面波特率设置的参数中涉及到了时钟clk,要根据不同串口所挂载的总线进行获取相应的时钟(此处使用的是串口2)

//串口1是APB2总线,其他串口是APB1总线
void USART_BRR_Configuration(UART_HandleTypeDef *huart, uint32_t BaudRate)
{
    uint32_t pclk;
    huart->Init.BaudRate = BaudRate;
   printf("11111");
		if(huart->Init.OverSampling == UART_OVERSAMPLING_16)
    {
        if (huart->Instance == USART2)
        {
          pclk = HAL_RCC_GetPCLK1Freq();
          huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
					printf("2222");
        }
        else
        {
          pclk = HAL_RCC_GetPCLK2Freq();
          huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
        }
    }
		else
    {
        if (huart->Instance == USART2 )
        {
          pclk = HAL_RCC_GetPCLK1Freq();
          huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
        }
        else
        {
          pclk = HAL_RCC_GetPCLK2Freq();
          huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
        }
    }
    
}					 

依旧是在modbus-rtu通信的06功能码处理函数中调用,修改代码如下:
在这里插入图片描述

2、stm32-简易版modbus协议通信实现波特率修改

主控芯片:stm32f013zet6,串口1通过modbus协议通信修改STM32波特率(单纯的串口通信,没有加入485通信)

  • 第一步:

    使用STM32cubemx生成包含LED引脚,串口1的代码初始化框架

  • 第二步:

    实现串口自定义形式的数据接收的指令判别,通过简单的modbus协议控制指令实现控制LED灯的亮灭。

  • 第三步:

    在控制LED灯亮灭的基础之上实现程序运行过程中对波特率的修改
    波特率修改效果如下:
    在这里插入图片描述

第一步:STM32cubemx对STM32F103ZET6的配置

  • 配置时钟RCC

    在这里插入图片描述

  • 配置sys选项

    如果不选择的话程序使用仿真器下载一次,后续将无法使用仿真器下载,(如果不小心忘了配置导致后续无法使用仿真器修改程序,则可以通过mcuisp下载程序,下载一次之后仿真器即可正常下载了)
    在这里插入图片描述

  • LED引脚配置:将PB8引脚设置为推挽输出模式

    在这里插入图片描述

  • 配置串口1,其他参数默认,并且使能串口中断

    在这里插入图片描述

    在这里插入图片描述

  • clock配置

    输入72键盘回车自动配置即可
    在这里插入图片描述
    完成以上步骤即可完成使用STM32cubemx的初始化配置,直接生成代码即可

第二步:通过简单的modbus协议实现对led灯的控制
串口接收的数据共四位:包含帧头(第一位aa)、帧尾(最后一位55)、设备码(第二位01)、数据指令(00或01)
指令如下:

串口助手发送 aa 01 00 55 是打开led
串口助手发送 aa 01 01 55 是关闭led
串口助手发送其他的报错提示重新输入

对main,c文件进行操作:

  • 定义变量

    在这里插入图片描述

     /* USER CODE BEGIN PV */
     uint8_t rxbuffer[4];//接收缓冲区
     uint8_t rx_flag=0;//接收完成标志:0表示接收未完成,1表示接收完成
     uint8_t errflag=0;//指令错误标志:0表示指令正确,1表示错误
     /* USER CODE END PV */
    
  • 重定义printf和串口接收回调函数的处理

    printf重定义记得要调用头文件#include “stdio.h”
    串口中断回调函数主要作用是清除数据接收完成标志和重新启动串口中断接收
    在这里插入图片描述

     /* USER CODE BEGIN 4 */
     
      //重定向c库函数scanf到串口DEBUG_USART,重写向后可使用scanf、getchar等函数
     int fgetc(FILE *f)
     {		
     	int ch;
     	HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 0xfff);	
     	return (ch);
     }
     
     //重定义fputc函数 
      int fputc(int ch, FILE *f)
      {         
     	 //采用轮询方式发送1字节数据
              HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xfff);
              return ch;
      }
     
     //接收回调函数
     void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
     {
     	if(huart->Instance==USART1)//判断发生接收中断的串口
     	{
     		rx_flag=1;//置位接收完成标志
     		HAL_UART_Receive_IT(&huart1,(uint8_t*)rxbuffer,4);//使能接收中断
     	}
     
      
     }
     /* USER CODE END 4 */
    
  • 对main主函数进行修改:

    while循环之前进行输入提示和使能接收中断(必须打开)
    在这里插入图片描述
    while循环里面就是在接收数据完成的情况下,判断数据帧头帧尾是否正确,如果正确则再去判断设备码是否正确,如果设备码也正确,再去判断收到的数据指令是打开LED还是关闭LED,代码如下

     /* USER CODE BEGIN 3 */
     if(rx_flag==1)//表示接收完成
     	{
     			rx_flag=0;//清除接收完成标志位
     	
     			if(rxbuffer[0]==0xaa&&rxbuffer[3]==0x55)//判断帧头帧尾
     			{
     				if(rxbuffer[1]==0x01)//判断设备码
     				{
     					if(rxbuffer[2]==0x00)//判断功能码-开启
     					{
     						HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);//开灯
     						printf("LED is open\r\n");
     					}
     					else if(rxbuffer[2]==0x01)//关闭
     					{
     						HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);//关灯
     						printf("LED is close\r\n");					
     					}
     					else//功能码错误
     					{
     						errflag=1;//置位错误标志
     					}
     				}
     				else//设备码错误
     				{
     						errflag=1;//置位错误标志
     				}
     			}
     			else//帧头帧尾错误
     			{
     					errflag=1;//置位错误标志
     			}
     		if(errflag==1)//发送错误提示信息
     		{
     			printf("communication error ~pelase send again!\r\n");
     		}
     		//清除接收缓冲区和错误标志,准备下一次接收
     		errflag=0;
     		rxbuffer[0]=0;
     		rxbuffer[1]=0;
     		rxbuffer[2]=0;
     		rxbuffer[3]=0;
     		}
       }
       /* USER CODE END 3 */
    
  • 编译烧写程序即可:

    指令如下(十六进制形式发送)
    串口助手发送 aa 01 00 55 是打开led
    串口助手发送aa 01 01 55 是关闭led
    输入其他的报错提示重新输入

第三步:在线修改stm32波特率
在上面基础之上通过串口1接收到的数据实现波特率的修改,波特率修改函数如下:

//在程序运行时修改串口波特率
//串口1是APB2总线,其他串口是APB1总线
void USART_BRR_Configuration(UART_HandleTypeDef *huart, uint32_t BaudRate)
{
		uint32_t pclk;
		UART_OVERSAMPLING_16;
    huart->Init.BaudRate = BaudRate;
    if (huart->Init.OverSampling == UART_OVERSAMPLING_16)
    {
        if (huart->Instance == USART1)
        {
          pclk = HAL_RCC_GetPCLK2Freq();
          huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
        }
        else
        {
          pclk = HAL_RCC_GetPCLK1Freq();
          huart->Instance->BRR = UART_BRR_SAMPLING16(pclk, huart->Init.BaudRate);
        }
    }
    else
    {	
				 if (huart->Instance == USART1)
        {
          pclk = HAL_RCC_GetPCLK2Freq();
          huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
        }
        else
        {
          pclk = HAL_RCC_GetPCLK1Freq();
          huart->Instance->BRR = UART_BRR_SAMPLING8(pclk, huart->Init.BaudRate);
        }
    }
}

主函数中修改波特率部分如图:
modbus通信指令(十六进制)

第一位aa表示串口接收数据帧的帧头
第二位01表示设备码
第三位是数据为00(打开led灯和修改波特率为4800)和01(关闭led灯和修改波特率为9600)
第四位数据是帧尾

使用串口助手分别发送一下数据指令(十六进制发送)
aa 01 00  55  打开led灯和修改波特率为4800
aa 01 00  55  关闭led灯和修改波特率为9600

在这里插入图片描述

整个 主函数代码如下:

int main(void)
{
  /* USER CODE BEGIN 1 */
	int i=0;
  /* USER CODE END 1 */
  

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

	printf("*****communication frame*******\r\n");
	printf("please enter instruction:\r\n");
	HAL_UART_Receive_IT(&huart1,(uint8_t*)rxbuffer,4);//使能接收中断
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		if(rx_flag==1)//表示接收完成
		{
			rx_flag=0;//清除接收完成标志位
	
			if(rxbuffer[0]==0xaa&&rxbuffer[3]==0x55)//判断帧头帧尾
			{
				if(rxbuffer[1]==0x01)//判断设备码
				{
					if(rxbuffer[2]==0x00)//判断功能码-开启
					{

						HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);//开灯
						printf("LED is open\r\n");
						//3-修改波特率成功
						printf("boud=4800\r\n");
						USART_BRR_Configuration(&huart1,4800);
						
					}
					else if(rxbuffer[2]==0x01)//关闭
					{
						printf("boud=9600\r\n");
						USART_BRR_Configuration(&huart1,9600);
						
					
						HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);//关灯
						printf("LED is close\r\n");					
					}
					else//功能码错误
					{
						errflag=1;//置位错误标志
					}
				}
				else//设备码错误
				{
						errflag=1;//置位错误标志
				}
			}
			else//帧头帧尾错误
			{
					errflag=1;//置位错误标志
			}
		if(errflag==1)//发送错误提示信息
		{
			printf("communication error ~pelase send again!\r\n");
		}
		//清除接收缓冲区和错误标志,准备下一次接收
		errflag=0;
		rxbuffer[0]=0;
		rxbuffer[1]=0;
		rxbuffer[2]=0;
		rxbuffer[3]=0;
		}
  }
  /* USER CODE END 3 */
}

编译下载程序通过串口助手即可完成波特率的修改,效果如下:
在这里插入图片描述

3、STM32F030K6T6芯片在线修改波特率(和STM32F1有点差异)

stm32-hal库-modbus-RTU通信在线修改波特率-代码下载链接
因为手中modbus通信的主控芯片是STM32F030K6T6所以参考以上进行修改
修改波特率的过程依旧参考cubemx生成的串口初始化代码去模仿着自己写一个波特率修改函数

在这里插入图片描述
进入串口初始化原函数之后找到下面参数配置函数跳转
在这里插入图片描述
跳转到原函数之后找到波特率参数配置位置,和STM32F103的有点不一样,还是摘取有用的部分即可
在这里插入图片描述
在这里插入图片描述
参考以上代码仿写波特率修改函数如下:

//修改波特率函数			 
void USART_BRR_Configuration(UART_HandleTypeDef *huart,uint32_t BaudRate)
{
  huart->Init.BaudRate = BaudRate;
  uint32_t pclk;
  if (huart->Init.OverSampling == UART_OVERSAMPLING_8)
  {
        pclk = HAL_RCC_GetPCLK1Freq();
       
    /* USARTDIV must be greater than or equal to 0d16 */
    if (pclk != 0U)
    {
        huart->Instance->BRR =(uint16_t)(UART_DIV_SAMPLING8(pclk, huart->Init.BaudRate));
    }
  }
  else
  {
    pclk = HAL_RCC_GetPCLK1Freq();
    if (pclk != 0U)
    {
      /* USARTDIV must be greater than or equal to 0d16 */
   
        huart->Instance->BRR =(uint16_t)(UART_DIV_SAMPLING16(pclk, huart->Init.BaudRate));
    }
  }
  
}		

06功能码可以实现缓冲数组数据的修改,把数组中的某一位定义为了波特率位,通过06功能码对这个数据修改后即可实现对波特率的修改。
在这里插入图片描述

完整代码如下(红色框选为新加内容)

在这里插入图片描述

// Modbus 6号功能码函数
// Modbus 主机写入寄存器值
void Modbus_Func6()  
{
  uint16_t Regadd;
	uint16_t val;
	uint16_t 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=Modbus_CRC16(modbus.sendbuf,i);
	modbus.sendbuf[i++]=crc/256;  
	modbus.sendbuf[i++]=crc%256;
	//数据发送包打包完毕
	RS485_TX;//使能485控制端(启动发送)  
	for(j=0;j<i;j++)
	{
	 Modbus_Send_Byte(modbus.sendbuf[j]);
	}
	HAL_Delay(100);
	if(Regadd==0x09)
	{
		USART_BRR_Configuration(&huart1,baud_table[val]);
		__HAL_UART_ENABLE(&huart1);
	}
	RS485_RX;
}

使用STM32F0开发板进行测试:stm32作为modbus从机,modbus poll软件作为主机:
主机其实也可以使用串口助手模拟,不过就比较麻烦了,需要自己构造报文进行发送指令。

在这里插入图片描述

测试效果如下(波特率序号0-3分别表示2400,4800,9600,19200)

在这里插入图片描述
以下三个文件是一个下载链接(压缩在一起)
下载链接stm32运行中在标准库和HAL库下修改串口波特率
001实验是在正点原子串口实验(库函数)基础上实现串口修改波特率
002实验是在modbus-rtu协议基础之上增加串口2修改波特率函数通过06功能码实现串口波特率的修改
003实验是一个简单的modbus协议实现串口波特率的修改
在这里插入图片描述

三、EEPROM存储数据实现单片机断电时数据保护

(一)主要实现功能简介+演示

1、功能介绍

STM32-485-modbus-rtu通信的波特率,从机设备id,8个继电器的控制状态。
主要是实现数据的存储,防止断电导致的数据丢失,可以再设备重新上电时自动恢复继电器在断电前的控制状态,断电前所设置的波特率

当前主要演示:stm32作为从机,modbus poll模拟上位机测试

  • 实现继电器的控制

    使用03功能码实现对继电器(led亮灭)的控制,写入1打开,写入0关闭

  • 实现波特率的修改

    对运行中的STM32通过主机实现波特率的修改

  • 设备重新上电后的数据恢复

    将数据存储到EEPROM中,当设备断电重新上电后恢复到断电的控制状态

  • 数组中各个元素的含义

    reg[0]-reg[7]表示继电器,reg[8]是从机地址,reg[9]是波特率修改位

2、效果演示

最初版本的演示视频(后期拿到新板子再更新)

stm32--modbus-rtu-eeprom断电数据保护

(二)EEPROM简单读写测试

参考博客链接如下:
IIC原理超详细讲解—值得一看
STM32-CubuMX-HAL库学习(七)-- I2C实现EEPROM读取
AT24C04、AT24C08、AT24C16系列EEPROM芯片单片机读写驱动程序
BL24Cxx系列EEPROM测试总结

1、STM32cubemx配置IIC-USART-TIM-GPIO

(只介绍stm32cube中串口和IIc的配置)

将modbus.c文件的代码移植到此控制芯片配置步骤和修改步骤参考上一篇博客,博文链接:STM32+RS485+Modbus-RTU(主机模式+从机模式)-标准库/HAL库开发

  • RCC配置

    在这里插入图片描述

  • sys配置

    在这里插入图片描述

  • 定时器配置

    在这里插入图片描述

  • 使能定时器中断

    在这里插入图片描述

  • IIC配置

    在这里插入图片描述

  • 串口配置波特率9600,并且使能串口中断

    在这里插入图片描述
    在这里插入图片描述

  • clock配置

    在这里插入图片描述
    完成以上配置以后生成代码

2、代码编写测试

  • 添加变量

    在main.c中添加数组、变量、宏定义和简单的eeprom读取和写入函数

     /* USER CODE BEGIN PD */
     
     #define ADDR_24LCxx_Write 0xA0
     #define ADDR_24LCxx_Read 0xA1
     #define BufferSize 256
     uint8_t WriteBuffer[BufferSize],ReadBuffer[BufferSize];
     uint16_t i;
     uint8_t data;
     /* USER CODE END PD */
    
  • EEPROM相关读取和写入函数

     /* Private macro -------------------------------------------------------------*/
     /* USER CODE BEGIN PM */
     //读取和写入单个寄存器数据
     /***********************************************************
     *@fuction	:IIC_read_single_reg
     *@brief		:读取一个寄存器中的数据
     *@param		:reg 是要读取的寄存器地址
     *@return	:void
     *@date		:2022-12-24
     ***********************************************************/
     uint8_t IIC_read_single_reg(uint8_t reg)
     {
         uint8_t res = 0;
         HAL_I2C_Mem_Read(&hi2c1, 0xA1, reg,I2C_MEMADD_SIZE_8BIT,&res,1,10);
         return res;
     }
     /***********************************************************
     *@fuction	:IIC_write_single_reg
     *@brief		:向单个寄存器中写入1个数据
     *@param		:reg写入寄存器的地址,data寄存器中要写入的数据
     *@return	:void
     *@date		:2022-12-24
     ***********************************************************/
     void IIC_write_single_reg(uint8_t reg, uint8_t data)
     {
         HAL_I2C_Mem_Write(&hi2c1, 0xA0, reg,I2C_MEMADD_SIZE_8BIT,&data,1,10);
     }
     
     void IIC_write_read_test()
     {
     		for(i=0; i<16; i++)
     			WriteBuffer[i]=i;    /* WriteBuffer init */
     			
     		printf("\r\n**测试页写数据****\r\n");
     		//写入两页数据
     		for (int j=0; j<2; j++)
         {
     			 if(HAL_I2C_Mem_Write(&hi2c1, ADDR_24LCxx_Write, 8*j, I2C_MEMADD_SIZE_8BIT,WriteBuffer+8*j,8, 1000) == HAL_OK)
     				 {
     						printf("\r\n EEPROM 24C02 Write Test OK \r\n");
     						HAL_Delay(20);
     				 }
     			 else
     				{
     					 HAL_Delay(20);
     					 printf("\r\n EEPROM 24C02 Write Test False \r\n");
     				 }
     		}
     		//读取16个数据
     		HAL_I2C_Mem_Read(&hi2c1, ADDR_24LCxx_Read, 0, I2C_MEMADD_SIZE_8BIT,ReadBuffer,16, 0xff);
     
     		for(i=0; i<16; i++)//打印一下数据
     			printf("0x%02X  ",ReadBuffer[i]);
     				
     		printf("\r\n******测试单个写入和读出数据*****\r\n");
     		IIC_write_single_reg(0X05,0X33);
     		HAL_Delay(20)	;
     		data = IIC_read_single_reg(0X05);
     		printf("0x%02X  ",data);
     
     
     }
     
     /* USER CODE END PM */
    
  • 主函数中在while循环前提添加

     /* USER CODE BEGIN 2 */
     	
     	RS485_TX;
     	IIC_write_read_test();//EEPROM读取和写入测试函数
     
     	Modbus_Init();//本机作为从机使用时初始化
     	HAL_TIM_Base_Start_IT(&htim14);
     	RS485_RX;//使能接收
     	HAL_UART_Receive_IT(&huart1, (uint8_t *)&RES, 1);//调用接收中断函数
     
     	
       /* USER CODE END 2 */
    

(三)每个地址存储一个继电器的状态(存储8个继电器状态)实现上电后数据恢复

1、判断是执行下载程序还是执行断电恢复程序

当继电器的状态或者波特率发生变化时就会将新的数据存储到EEPROM中,当设备断电重新上电的情况下,会自动恢复到设备断电前的情况。如何进行判断是否需要进行数据恢复呢?因为STM32重新运行存在着两种情况:
情况1:程序重新运行是因为我们修改了程序中的一些内容和参数(编译后下载)
情况2:设备断电后重新上电重新运行(主要是恢复一些继电器在断电前的运行状态,和断电前所设置的通信波特率,而不至于全部恢复到默认状态)
如何对以上情况进行区分呢?只需要加入一个参数变量即可,参数的使用主要思想如下:

  • 下载程序/断电恢复标志位

     /*
     如果是修改完程序下载的程序,修改一下下面的数值:EEPROM_FLAG
     只有当下面的数值发生变化时,才会执行默认的波特率9600,STM32控制状态是默认状态
     如果不修改下面数值的话表明此次波特率恢复到断电前的波特率,恢复到断电前的控制状态
     */
     uint8_t EEPROM_FLAG=0xf0;//判断是下载程序还是断电恢复数据
    

2、main.c文件程序编写

(1)新建变量
	/* USER CODE BEGIN PD */
	//eeprom添加
	/*
	如果是修改完程序下载的程序,修改一下下面的数值:EEPROM_FLAG
	只有当下面的数值发生变化时,才会执行默认的波特率9600
	如果不修改下面数值的话表明此次波特率恢复到断电前的波特率
	*/
	uint8_t EEPROM_FLAG=0xf0;//判断是下载程序还是断电恢复数据
	
	#define ADDR_24C04_Write 0xA0
	#define ADDR_24C04_Read 0xA1
	#define BufferSize 256
	uint8_t WriteBuffer[BufferSize],ReadBuffer[BufferSize];
	
	
	通过串口接收到的数据自定义修改波特率
	//uint32_t baud_table[]={2400,4800,9600,19200,38400};
		uint16_t Baud_SetNum[]={0		,1		,2		,3		,4}; 
	
	/* USER CODE END PD */
(2)读取和写入单个寄存器函数
	//读取和写入单个寄存器数据
	/***********************************************************
	*@brief		:读取一个寄存器中的数据
	*@param		:reg 是要读取的寄存器地址
	*
	***********************************************************/
	uint8_t IIC_ReadSingleReg(uint8_t reg)
	{
	    uint8_t res = 0;
	    HAL_I2C_Mem_Read(&hi2c1, 0xA1, reg,I2C_MEMADD_SIZE_8BIT,&res,1,10);
	    return res;
	}
	
	/***********************************************************
	*@brief		:向单个寄存器中写入1个数据
	*@param		:reg写入寄存器的地址,data寄存器中要写入的数据
	***********************************************************/
	
	void IIC_WriteSingleReg(uint8_t reg, uint8_t data)
	{
	    HAL_I2C_Mem_Write(&hi2c1, 0xA0, reg,I2C_MEMADD_SIZE_8BIT,&data,1,10);
	}
(3)下载完程序需要执行的函数
主要是将继电器的默认状态写入到EEPROM中

	//刚下载完程序
	void KZX_STATE_Down()
	{
	
		
					 HAL_GPIO_WritePin(GPIOA, KZ1_Pin|KZ2_Pin|KZ3_Pin, GPIO_PIN_RESET);
					 HAL_GPIO_WritePin(GPIOB, KZ4_Pin|KZ5_Pin|KZ6_Pin|KZ7_Pin 
	                          |KZ8_Pin, GPIO_PIN_RESET);
					//将继电器的状态写入eeprom中
					if(HAL_I2C_Mem_Write(&hi2c1, ADDR_24C04_Write, 8, I2C_MEMADD_SIZE_8BIT,WriteBuffer,8, 1000) == HAL_OK)
					{
								printf("\r\n EEPROM 24C02 Write Test OK \r\n");
								HAL_Delay(20);
					}
					else
					{
								HAL_Delay(20);
							  printf("\r\n EEPROM 24C02 Write Test False \r\n");
					}
	}
(4)编写断电恢复需要执行的函数

首先就是读取相关寄存器存储位数据(断电前各个继电器的状态)
其次使用switch-case语句实现继电器的状态恢复(断电前保持打开状态的把它打开,关闭的保持关闭)

//这是断电恢复继电器状态
void KZX_POWER_RESTART()
{
			int kzi;
			Reg[8]=IIC_ReadSingleReg(0x00);
			//从地址8开始读取8个数据(存储到ReadBuffer数组中)
			HAL_I2C_Mem_Read(&hi2c1, ADDR_24C04_Read, 8, I2C_MEMADD_SIZE_8BIT,ReadBuffer,8, 0xff);
			for(kzi=0;kzi<8;kzi++)
			{
					Reg[kzi]=ReadBuffer[kzi];//更新到缓冲寄存器中
					if(Reg[kzi]==1)//打开
					{
						switch(kzi)
						{
							case 0:
								KZ1_K;break;
							case 1:
								KZ2_K;break;
							case 2:
								KZ3_K;break;
							case 3:
								KZ4_K;break;
							case 4:
								KZ5_K;break;
							case 5:
								KZ6_K;break;
							case 6:
								KZ7_K;break;
							case 7:
								KZ8_K;break;
							default:break;
						}
						
					}
					else//关闭
					{
						switch(kzi)
						{
							case 0:
								KZ1_G;break;
							case 1:
								KZ2_G;break;
							case 2:
								KZ3_G;break;
							case 3:
								KZ4_G;break;
							case 4:
								KZ5_G;break;
							case 5:
								KZ6_G;break;
							case 6:
								KZ7_G;break;
							case 7:
								KZ8_G;break;
							default:break;
						}
					
					}
			}
}
(5)条件判断函数

通过变量的数值进行判断目前是要执行下载程序的函数部分还是断电重新上电后数据恢复部分

void DeviceSetBaud()
{
		uint8_t CurrentBaod;
		if(EEPROM_FLAG==IIC_ReadSingleReg(0X02))//上电恢复
		{
			RS485_TX;		
			//读取寄存器中存储的波特率序号
			CurrentBaod=IIC_ReadSingleReg(0X04);//读取0x04地址中波特率的序号
			HAL_Delay(20);
			printf("上次断电时波特率数值为= %d \r\n ",baud_table[CurrentBaod]);//读取掉电前设置的波特率
			Reg[9]=CurrentBaod;
			USART_BRR_Configuration(&huart1,baud_table[CurrentBaod]);//波特率修改(恢复到断电前的波特率)
			__HAL_UART_ENABLE(&huart1);
			KZX_POWER_RESTART();//断电恢复数据
		}
		else//表明此时是修改完程序进行下载
		{
				IIC_WriteSingleReg(0X00,modbus.myadd);
				HAL_Delay(20)	;
				Reg[8]=IIC_ReadSingleReg(0x00);
				printf("reg[8]=%d \r\n",Reg[8]);
				HAL_Delay(20);
				IIC_WriteSingleReg(0X02,EEPROM_FLAG);
				HAL_Delay(20)	;
				IIC_WriteSingleReg(0X04,Baud_SetNum[2]);//默认波特率9600
				HAL_Delay(20)	;
				Reg[9]=Baud_SetNum[2];
				RS485_TX;
				printf("下载完程序默认波特率为9600\r\n");
				printf("设备地址为%d\r\n",Reg[8]);
				KZX_STATE_Down();//刚上电写入数据(继电器初始化)
		}
}

以上是下载程序时将数据(波特率和继电器默认状态)写入到EEPROM中和断电重新上电时实现断电前的状态恢复
我们还需要将继电器实时的控制状态和波特率状态写入到EEPROM中

3、modbus.c文件程序修改

根据继电器状态变化更新存储中的状态值和实现波特率的切换(modbus主机下达控制指令)

(1)缓冲数组数据清零

缓冲寄存器全部设为0(表示继电器默认关闭状态),如果相关位为1则表示继电器打开

reg[0]-reg[7]存储的分别是继电器1-继电器8的开关状态(0-关闭,1-开启)
在这里插入图片描述

(2)EEPROM中写入数据函数(数据写入+继电器控制)

向EEPROM中写入数据函数(并且同时根据接收到的数据指令控制继电器)
只有当EEPROM中继电器的状态和当前接收到的指令状态不一样时才会更新EEPROM中的数据(regadd是数组地址0-7分别对应8个继电器)

//EEPROM中写入数据(接收单个寄存器数据存储)
void write_data( uint16_t Regadd,uint16_t val)
{
	switch(Regadd)
	{
		case 0:
			if(Reg[Regadd]!=IIC_ReadSingleReg(0X08))
			{
				IIC_WriteSingleReg(0X08,Reg[Regadd]);//地址0X02写入eeprom判断标志
				HAL_Delay(5);
				if(val==1)
				{
					KZ1_K;
				}
				else
				{
					KZ1_G;
				}
			}
			break;
		case 1:
			if(Reg[Regadd]!=IIC_ReadSingleReg(0X09))
			{
				IIC_WriteSingleReg(0X09,Reg[Regadd]);//地址0X02写入eeprom判断标志
				HAL_Delay(5);
				if(val==1)
				{
					KZ2_K;
				}
				else
				{
					KZ2_G;
				}
			}
			break;	
		case 2:
			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0A))
			{
				IIC_WriteSingleReg(0X0A,Reg[Regadd]);//地址0X02写入eeprom判断标志
				HAL_Delay(5);
					if(val==1)
				{
					KZ3_K;
				}
				else
				{
					KZ3_G;
				}
			}
			break;
		case 3:
			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0B))
			{
				IIC_WriteSingleReg(0X0B,Reg[Regadd]);//地址0X02写入eeprom判断标志
				HAL_Delay(5);
				if(val==1)
				{
					KZ4_K;
				}
				else
				{
					KZ4_G;
				}
			}
			break;
		case 4:
			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0C))
			{
				IIC_WriteSingleReg(0X0C,Reg[Regadd]);//地址0X02写入eeprom判断标志
				if(val==1)
				{
					KZ5_K;
				}
				else
				{
					KZ5_G;
				}
			}
			break;
		case 5:
			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0D))
			{
				IIC_WriteSingleReg(0X0D,Reg[Regadd]);//地址0X02写入eeprom判断标志
				if(val==1)
				{
					KZ6_K;
				}
				else
				{
					KZ6_G;
				}
			}
			break;
		case 6:
			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0E))
			{
				IIC_WriteSingleReg(0X0E,Reg[Regadd]);//地址0X02写入eeprom判断标志
				if(val==1)
				{
					KZ7_K;
				}
				else
				{
					KZ7_G;
				}
			}
			break;
		case 7:
			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0F))
			{
				IIC_WriteSingleReg(0X0F,Reg[Regadd]);//地址0X02写入eeprom判断标志
				if(val==1)
				{
					KZ8_K;
				}
				else
				{
					KZ8_G;
				}
			}
			break;
	}

}
(3)MODBUS-06功能码函数(加入波特率在线修改+调用数据存储函数)

MODBUS-RTU的06功能码函数附加了修改波特率函数(将波特率序号写入到EEPRM中)和继电器控制(将继电器状态写入到EEPROM中)

06功能码是修改单个寄存器的数据
在这里插入图片描述
代码如下:

// Modbus 6号功能码函数
// Modbus 主机写入寄存器值
void Modbus_Func6()  
{
  uint16_t Regadd;
	uint16_t val;
	uint16_t 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=Modbus_CRC16(modbus.sendbuf,i);
	modbus.sendbuf[i++]=crc/256;  
	modbus.sendbuf[i++]=crc%256;
	//数据发送包打包完毕
	RS485_TX;//使能485控制端(启动发送)  
	for(j=0;j<i;j++)
	{
	 Modbus_Send_Byte(modbus.sendbuf[j]);
	}
	HAL_Delay(100);
	if(Regadd==0x09)
	{
		IIC_WriteSingleReg(0X04,Reg[9]);//将新的波特率数据对应的序号写入到eeprom中
		HAL_Delay(10)	;
		USART_BRR_Configuration(&huart1,baud_table[val]);//波特率修改
		__HAL_UART_ENABLE(&huart1);
	}
	write_data(Regadd,val);
	
	RS485_RX;
}
(4)MODBUS-16功能码函数(加入数据存储)

MODBUS-RTU的16功能码实现多个寄存器数据的修改(可以实现一条指令对多个继电器进行控制了)

修改程序如下:
在这里插入图片描述

//这是往多个寄存器器中写入数据
//功能码0x10指令即十进制16
void Modbus_Func16()
{
		uint16_t Regadd;//地址16位
		uint16_t Reglen;
		uint16_t i,crc,j;
		
		Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3];  
		Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];
		for(i=0;i<Reglen;i++)
		{
		
			Reg[Regadd+i]=modbus.rcbuf[7+i*2]*256+modbus.rcbuf[8+i*2];
			
			write_data(Regadd+i,Reg[Regadd+i]);
			HAL_Delay(5);
			
		}
		
		//以下为回应主机内容
		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];
		crc=Modbus_CRC16(modbus.sendbuf,6);
		modbus.sendbuf[6]=crc/256; 
		modbus.sendbuf[7]=crc%256;
		//数据发送包打包完毕
		
		RS485_TX;//使能485控制端(启动发送)  
		for(j=0;j<8;j++)
		{
			Modbus_Send_Byte(modbus.sendbuf[j]);
		}
		
		RS485_RX;//失能485控制端(改为接收)

}
(5)综上实现的主要功能
  • stm32作为modbus从机,使用modbus poll作为主机进行测试
  • 模拟器默认波特率改为9600,前8个寄存器数据表示继电器状态,后两个寄存器数据分别代表从机地址,和波特率设置位
  • 通过modbus poll主机修改寄存器的数值去控制继电器或者修改波特率(并且将波特率和继电器状态写入到存储中)

(四)每个地址存储8个继电器的状态(1字节=8位=8个继电器)

1、main.c文件程序编写

(1)定义变量
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
//eeprom添加
/*
如果是修改完程序下载的程序,修改一下下面的数值:EEPROM_FLAG
只有当下面的数值发生变化时,才会执行默认的波特率9600
如果不修改下面数值的话表明此次波特率恢复到断电前的波特率
*/
uint8_t EEPROM_FLAG=0xf4;//判断是下载程序还是断电恢复数据

#define ADDR_24C04_Write 0xA0
#define ADDR_24C04_Read 0xA1
#define BufferSize 256
uint8_t WriteBuffer[BufferSize],ReadBuffer[BufferSize];


通过串口接收到的数据自定义修改波特率
//uint32_t baud_table[]={2400,4800,9600,19200,38400};
	uint16_t Baud_SetNum[]={0		,1		,2		,3		,4}; 

	
	
	
	
	//使用bit位进行操作添加部分代码
	// 0 0 0 0 0 0 0 0 
	//共8位,第一位表示第一个开关,第二位表示第二个开关,第三位表示第三个开关
	uint8_t Switch_value;
	uint8_t bit_add=20,bit_value;
/* USER CODE END PD */
(2)读取和写入单个寄存器函数(同上不变)
/* USER CODE BEGIN PM */
	
//读取和写入单个寄存器数据
/***********************************************************
*@brief		:读取一个寄存器中的数据
*@param		:reg 是要读取的寄存器地址
*
***********************************************************/
uint8_t IIC_ReadSingleReg(uint8_t reg)
{
    uint8_t res = 0;
    HAL_I2C_Mem_Read(&hi2c1, 0xA1, reg,I2C_MEMADD_SIZE_8BIT,&res,1,10);
    return res;
}

/***********************************************************
*@brief		:向单个寄存器中写入1个数据
*@param		:reg写入寄存器的地址,data寄存器中要写入的数据
***********************************************************/

void IIC_WriteSingleReg(uint8_t reg, uint8_t data)
{
    HAL_I2C_Mem_Write(&hi2c1, 0xA0, reg,I2C_MEMADD_SIZE_8BIT,&data,1,10);
}
(3)下载完程序需要执行的函数

与上文略有差异(只需要一个地址的寄存器存储数据)

	//刚下载完程序
	void KZX_STATE_Down_bit()
	{
	
					//引脚全部初始化为低电平(继电器关)
					 HAL_GPIO_WritePin(GPIOA, KZ1_Pin|KZ2_Pin|KZ3_Pin, GPIO_PIN_RESET);
					 HAL_GPIO_WritePin(GPIOB, KZ4_Pin|KZ5_Pin|KZ6_Pin|KZ7_Pin 
	                          |KZ8_Pin, GPIO_PIN_RESET);
					//将继电器的状态写入eeprom中(一个字节存储八个继电器状态)
					IIC_WriteSingleReg(bit_add, bit_value);//
	}
(4)断电恢复需要执行的函数

上电后恢复继电器断电前的状态
主要读取EEPROM地址0x00获取从机的地址,读取地址bit_add获取一个字节数据(8位表示8个继电器),然后将每一位的状态进行提取之后放入缓冲数组中,最后根据每个位的数值去恢复继电器的控制状态。

	//把读取到的一个字节数据的8个位依次分离出来(例如:0x34=0011  0100使用右移从低位到高位依次提取出来)
	for(kzi=0;kzi<8;kzi++)//把每一位数值提取出来(共8位)
	{
		 return_bit_value=(return_value>>kzi)&0x01;
		 Reg[kzi]=return_bit_value;//将数据存储到reg缓冲数组中
	}

代码如下

//这是断电恢复继电器状态
void KZX_POWER_RESTART_bit()
{
			int kzi;
			int return_value;//读取eeprom返回的数值(开关值)
			int return_bit_value;//某一位是0还是1
			Reg[8]=IIC_ReadSingleReg(0x00);//读取从机地址
			
			return_value=IIC_ReadSingleReg(bit_add);//读取继电器的状态(8个继电器的状态)
	
			printf("上电读取的数值:0x%02X  ",return_value);
			for(kzi=0;kzi<8;kzi++)//把每一位数值提取出来(共8位)
			{
				 return_bit_value=(return_value>>kzi)&0x01;
				
				 Reg[kzi]=return_bit_value;//将数据存储到reg缓冲数组中
				
				 if(return_bit_value ==1)//表明打开
				 {
						switch(kzi)
						{
							case 0:
								KZ1_K;break;
							case 1:
								KZ2_K;break;
							case 2:
								KZ3_K;break;
							case 3:
								KZ4_K;break;
							case 4:
								KZ5_K;break;
							case 5:
								KZ6_K;break;
							case 6:
								KZ7_K;break;
							case 7:
								KZ8_K;break;
							default:break;
						}
						
					}
					else//关闭
					{
						switch(kzi)
						{
							case 0:
								KZ1_G;break;
							case 1:
								KZ2_G;break;
							case 2:
								KZ3_G;break;
							case 3:
								KZ4_G;break;
							case 4:
								KZ5_G;break;
							case 5:
								KZ6_G;break;
							case 6:
								KZ7_G;break;
							case 7:
								KZ8_G;break;
							default:break;
						}
				 }
			
			}	
}
(5)条件判断函数

判断是执行上电恢复数据还是下载完程序运行(如果下载程序必须修改下相关变量值,保证和上一次时数值不一样,否则即使下载程序也默认是断电恢复数据)

//中途断电波特率恢复函数
void DeviceSetBaud()
{
		uint8_t CurrentBaod;
		if(EEPROM_FLAG==IIC_ReadSingleReg(0X02))//上电恢复
		{
			RS485_TX;		
			//读取寄存器中存储的波特率序号
			CurrentBaod=IIC_ReadSingleReg(0X04);//读取0x04地址中波特率的序号
			HAL_Delay(20);
			printf("上次断电时波特率数值为= %d \r\n ",baud_table[CurrentBaod]);//读取掉电前设置的波特率
			Reg[9]=CurrentBaod;
			USART_BRR_Configuration(&huart1,baud_table[CurrentBaod]);//波特率修改(恢复到断电前的波特率)
			__HAL_UART_ENABLE(&huart1);
//			KZX_POWER_RESTART();//断电恢复数据
			KZX_POWER_RESTART_bit();
		}
		else//表明此时是修改完程序进行下载
		{
				IIC_WriteSingleReg(0X00,modbus.myadd);
				HAL_Delay(20)	;
				Reg[8]=IIC_ReadSingleReg(0x00);
				printf("reg[8]=%d \r\n",Reg[8]);
				HAL_Delay(20);
				IIC_WriteSingleReg(0X02,EEPROM_FLAG);
				HAL_Delay(20)	;
				IIC_WriteSingleReg(0X04,Baud_SetNum[2]);//默认波特率9600
				HAL_Delay(20)	;
				Reg[9]=Baud_SetNum[2];
				RS485_TX;
				printf("下载完程序默认波特率为9600\r\n");
				printf("设备地址为%d\r\n",Reg[8]);
//				KZX_STATE_Down();//刚上电写入数据(继电器初始化)
			KZX_STATE_Down_bit();
		}
}
(6)main函数
/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */
  

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_TIM14_Init();
  MX_I2C1_Init();
  MX_IWDG_Init();
  /* USER CODE BEGIN 2 */
	Modbus_Init();//本机作为从机使用时初始化

	DeviceSetBaud();//EEPROM上电读取波特率和恢复数据

	
	HAL_TIM_Base_Start_IT(&htim14);
	RS485_RX;//使能接收
	HAL_UART_Receive_IT(&huart1, (uint8_t *)&RES, 1);//调用接收中断函数

	
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		

			Modbus_Event();
		 HAL_IWDG_Refresh(&hiwdg); //喂狗:重装看门狗数据为4095.
		
  }
  /* USER CODE END 3 */
}

2、modbus.c文件程序编写(原来基础之上)

(1)变量保持不变

在这里插入图片描述

(2)数据存储+继电器控制函数

EEPROM中写入数据并且控制继电器状态

首先读取一下bit_add读取的数值,然后判断新接收的一位数据值,依次判断相关的位是置1操作还是置0操作,再对存储的数值进行更新
C语言中对于二进制数据位清0和置1操作(普通变量+数组+指针)

//EEPROM中写入数据(接收单个寄存器数据存储)
void write_data_bit( uint16_t Regadd,uint16_t val)
{
	Switch_value=IIC_ReadSingleReg(bit_add);//读取寄存器中原来继电器状态的值
	if(val==1)//相关位置1操作
	{
		Switch_value|=(0x1<<Regadd);//把第regadd位置1操作
		IIC_WriteSingleReg(bit_add,Switch_value);//地址bit_add写入处理后的结果
	}
	else//相关位置0操作
	{
		Switch_value&=~(0x1<<Regadd);//把第regadd位置0操作
		IIC_WriteSingleReg(bit_add,Switch_value);//地址bit_add写入处理后的结果
	}
	switch(Regadd)
	{
		case 0:
			
				if(val==1)
				{
					KZ1_K;
				}
				else
				{
					KZ1_G;
				}
	
			break;
		case 1:

	
				if(val==1)
				{
					KZ2_K;
				}
				else
				{
					KZ2_G;
				}

			break;	
		case 2:
			
					if(val==1)
				{
					KZ3_K;
				}
				else
				{
					KZ3_G;
				}

			break;
		case 3:
		
				if(val==1)
				{
					KZ4_K;
				}
				else
				{
					KZ4_G;
				}

			break;
		case 4:
			
				if(val==1)
				{
					KZ5_K;
				}
				else
				{
					KZ5_G;
				}
	
			break;
		case 5:
		
				if(val==1)
				{
					KZ6_K;
				}
				else
				{
					KZ6_G;
				}
		
			break;
		case 6:
		
				if(val==1)
				{
					KZ7_K;
				}
				else
				{
					KZ7_G;
				}
	
			break;
		case 7:
		
				if(val==1)
				{
					KZ8_K;
				}
				else
				{
					KZ8_G;
				}
			break;
	}

}
(3)MODBUS-06功能码函数(原基础修改)

在这里插入图片描述

// Modbus 6号功能码函数
// Modbus 主机写入寄存器值
void Modbus_Func6()  
{
  uint16_t Regadd;
	uint16_t val;
	uint16_t 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=Modbus_CRC16(modbus.sendbuf,i);
	modbus.sendbuf[i++]=crc/256;  
	modbus.sendbuf[i++]=crc%256;
	//数据发送包打包完毕
	RS485_TX;//使能485控制端(启动发送)  
	for(j=0;j<i;j++)
	{
	 Modbus_Send_Byte(modbus.sendbuf[j]);
	}
	HAL_Delay(100);
	if(Regadd==0x09)
	{
		IIC_WriteSingleReg(0X04,Reg[9]);//将新的波特率数据对应的序号写入到eeprom中
		HAL_Delay(10)	;
		USART_BRR_Configuration(&huart1,baud_table[val]);//波特率修改
		__HAL_UART_ENABLE(&huart1);
	}
//	write_data(Regadd,val);
	write_data_bit(Regadd,val);
	RS485_RX;
}
(4)modbus-16功能码函数(原基础修改)

在这里插入图片描述

//这是往多个寄存器器中写入数据
//功能码0x10指令即十进制16
void Modbus_Func16()
{
		uint16_t Regadd;//地址16位
		uint16_t Reglen;
		uint16_t i,crc,j;
		
		Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3];  
		Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];
		for(i=0;i<Reglen;i++)
		{
		
			Reg[Regadd+i]=modbus.rcbuf[7+i*2]*256+modbus.rcbuf[8+i*2];
			
//			write_data(Regadd+i,Reg[Regadd+i]);
			write_data_bit(Regadd+i,Reg[Regadd+i]);
			HAL_Delay(5);
			
		}
		
		
		//以下为回应主机内容
		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];
		crc=Modbus_CRC16(modbus.sendbuf,6);
		modbus.sendbuf[6]=crc/256; 
		modbus.sendbuf[7]=crc%256;
		//数据发送包打包完毕
		
		RS485_TX;//使能485控制端(启动发送)  
		for(j=0;j<8;j++)
		{
			Modbus_Send_Byte(modbus.sendbuf[j]);
		}
		
		RS485_RX;//失能485控制端(改为接收)

}

四、modbus协议功能码06和10进行完善(加入对继电器的控制+数据存储)

(一)两个数据存储函数

对第二部分内容进行提取拆分(06功能功能码和10功能码可以调用分别调用以下两个函数)

void write_data( uint16_t Regadd,uint16_t val)
void write_data_bit( uint16_t Regadd,uint16_t val)

  • 调用 write_data( uint16_t Regadd,uint16_t val)函数

    是一个地址存储一个继电器状态(8个继电器状态存储到8个连续的地址中),当接收到对继电器的控制指令时需要对存储的数据进行字节更新。

  • 调用 write_data_bit( uint16_t Regadd,uint16_t val)函数

    使用一个地址存储(一个字节共8位,每一位存储一个继电器状态),如果接收到继电器控制指令时实现对这个字节数据相应的位置1或者置0处理来表示继电器打开还是关闭,当接收到新的控制指令时需要对这个字节的某个位进行存储修改,重新存储起来。

     //EEPROM中写入数据(接收单个寄存器数据存储)
     void write_data( uint16_t Regadd,uint16_t val)
     {
     	switch(Regadd)
     	{
     		case 0:
     			if(Reg[Regadd]!=IIC_ReadSingleReg(0X08))
     			{
     				IIC_WriteSingleReg(0X08,Reg[Regadd]);//地址0X02写入eeprom判断标志
     				HAL_Delay(5);
     				if(val==1)
     				{
     					KZ1_K;
     				}
     				else
     				{
     					KZ1_G;
     				}
     			}
     			break;
     		case 1:
     			if(Reg[Regadd]!=IIC_ReadSingleReg(0X09))
     			{
     				IIC_WriteSingleReg(0X09,Reg[Regadd]);//地址0X02写入eeprom判断标志
     				HAL_Delay(5);
     				if(val==1)
     				{
     					KZ2_K;
     				}
     				else
     				{
     					KZ2_G;
     				}
     			}
     			break;	
     		case 2:
     			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0A))
     			{
     				IIC_WriteSingleReg(0X0A,Reg[Regadd]);//地址0X02写入eeprom判断标志
     				HAL_Delay(5);
     					if(val==1)
     				{
     					KZ3_K;
     				}
     				else
     				{
     					KZ3_G;
     				}
     			}
     			break;
     		case 3:
     			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0B))
     			{
     				IIC_WriteSingleReg(0X0B,Reg[Regadd]);//地址0X02写入eeprom判断标志
     				HAL_Delay(5);
     				if(val==1)
     				{
     					KZ4_K;
     				}
     				else
     				{
     					KZ4_G;
     				}
     			}
     			break;
     		case 4:
     			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0C))
     			{
     				IIC_WriteSingleReg(0X0C,Reg[Regadd]);//地址0X02写入eeprom判断标志
     				if(val==1)
     				{
     					KZ5_K;
     				}
     				else
     				{
     					KZ5_G;
     				}
     			}
     			break;
     		case 5:
     			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0D))
     			{
     				IIC_WriteSingleReg(0X0D,Reg[Regadd]);//地址0X02写入eeprom判断标志
     				if(val==1)
     				{
     					KZ6_K;
     				}
     				else
     				{
     					KZ6_G;
     				}
     			}
     			break;
     		case 6:
     			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0E))
     			{
     				IIC_WriteSingleReg(0X0E,Reg[Regadd]);//地址0X02写入eeprom判断标志
     				if(val==1)
     				{
     					KZ7_K;
     				}
     				else
     				{
     					KZ7_G;
     				}
     			}
     			break;
     		case 7:
     			if(Reg[Regadd]!=IIC_ReadSingleReg(0X0F))
     			{
     				IIC_WriteSingleReg(0X0F,Reg[Regadd]);//地址0X02写入eeprom判断标志
     				if(val==1)
     				{
     					KZ8_K;
     				}
     				else
     				{
     					KZ8_G;
     				}
     			}
     			break;
     
     		
     	}
     
     }
     
     //EEPROM中写入数据
     void write_data_bit( uint16_t Regadd,uint16_t val)
     {
     	Switch_value=IIC_ReadSingleReg(bit_add);//读取寄存器中原来继电器状态的值
     	if(val==1)//相关位置1操作
     	{
     		Switch_value|=(0x1<<Regadd);//把第regadd位置1操作
     		IIC_WriteSingleReg(bit_add,Switch_value);//地址bit_add写入处理后的结果
     	}
     	else//相关位置0操作
     	{
     		Switch_value&=~(0x1<<Regadd);//把第regadd位置0操作
     		IIC_WriteSingleReg(bit_add,Switch_value);//地址bit_add写入处理后的结果
     	}
     	switch(Regadd)
     	{
     		case 0:
     			
     				if(val==1)
     				{
     					KZ1_K;
     				}
     				else
     				{
     					KZ1_G;
     				}
     	
     			break;
     		case 1:
     
     	
     				if(val==1)
     				{
     					KZ2_K;
     				}
     				else
     				{
     					KZ2_G;
     				}
     
     			break;	
     		case 2:
     			
     					if(val==1)
     				{
     					KZ3_K;
     				}
     				else
     				{
     					KZ3_G;
     				}
     
     			break;
     		case 3:
     		
     				if(val==1)
     				{
     					KZ4_K;
     				}
     				else
     				{
     					KZ4_G;
     				}
     
     			break;
     		case 4:
     			
     				if(val==1)
     				{
     					KZ5_K;
     				}
     				else
     				{
     					KZ5_G;
     				}
     	
     			break;
     		case 5:
     		
     				if(val==1)
     				{
     					KZ6_K;
     				}
     				else
     				{
     					KZ6_G;
     				}
     		
     			break;
     		case 6:
     		
     				if(val==1)
     				{
     					KZ7_K;
     				}
     				else
     				{
     					KZ7_G;
     				}
     	
     			break;
     		case 7:
     		
     				if(val==1)
     				{
     					KZ8_K;
     				}
     				else
     				{
     					KZ8_G;
     				}
     			break;
     	}
     
     }
    

(二)主要实现修改单个寄存器数据(控制继电器状态1-打开,0-关闭)

实现单个寄存器的数据修改并且将继电器状态和修改的波特率写入到数据存储中

// Modbus 6号功能码函数
// Modbus 主机写入寄存器值
void Modbus_Func6()  
{
  uint16_t Regadd;
	uint16_t val;
	uint16_t 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=Modbus_CRC16(modbus.sendbuf,i);
	modbus.sendbuf[i++]=crc/256;  
	modbus.sendbuf[i++]=crc%256;
	//数据发送包打包完毕
	RS485_TX;//使能485控制端(启动发送)  
	for(j=0;j<i;j++)
	{
	 Modbus_Send_Byte(modbus.sendbuf[j]);
	}
	HAL_Delay(100);
	if(Regadd==0x09)
	{
		IIC_WriteSingleReg(0X04,Reg[9]);//将新的波特率数据对应的序号写入到eeprom中
		HAL_Delay(10)	;
		USART_BRR_Configuration(&huart1,baud_table[val]);//波特率修改
		__HAL_UART_ENABLE(&huart1);
	}
//	write_data(Regadd,val);
	write_data_bit(Regadd,val);
	RS485_RX;
}

(三)实现10功能码控制多个继电器的打开和关闭

实现多个寄存器的数据修改并且将数据保存起来

//这是往多个寄存器器中写入数据
//功能码0x10指令即十进制16
void Modbus_Func16()
{
		uint16_t Regadd;//地址16位
		uint16_t Reglen;
		uint16_t i,crc,j;
		
		Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3];  
		Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];
		for(i=0;i<Reglen;i++)
		{
		
			Reg[Regadd+i]=modbus.rcbuf[7+i*2]*256+modbus.rcbuf[8+i*2];
			
//			write_data(Regadd+i,Reg[Regadd+i]);
			write_data_bit(Regadd+i,Reg[Regadd+i]);
			HAL_Delay(5);
			
		}
		
		
		//以下为回应主机内容
		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];
		crc=Modbus_CRC16(modbus.sendbuf,6);
		modbus.sendbuf[6]=crc/256; 
		modbus.sendbuf[7]=crc%256;
		//数据发送包打包完毕
		
		RS485_TX;//使能485控制端(启动发送)  
		for(j=0;j<8;j++)
		{
			Modbus_Send_Byte(modbus.sendbuf[j]);
		}
		RS485_RX;//失能485控制端(改为接收)
}

五、根据说明书功能进行修改完善

接下来的板子演示(01-05-15功能码)是不带EEPROM存储的,在旧版上调试完成(右侧板子带有eeprom,板子已坏)
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

上一篇博客已经详细介绍了MODBUS-RTU通信的03功能码,06功能码,16(十六进制10)功能码函数的使用和测试,这三个功能码都是对单个寄存器或多个寄存器的读取和写入。
目前接下来要实现的是实现对单个位或者多个位的写入和读取,即功能码01,05,15
STM32-MODBUS-RTU通信(各个功能码测试)-代码链接

(一)加入功能码0x01实现继电器的状态查询或者光耦输入状态查询

01功能码实现的是位读取,其实和03功能码寄存器读取是类似的,只需要对03功能码函数稍作修改即可
在这里插入图片描述
主机发送八路查询状态指令解读
在这里插入图片描述
从机返回数据解读
在这里插入图片描述

03功能码函数到01功能码的变化
在这里插入图片描述

// Modbus 1号功能码函数
// Modbus 主机读取8个继电器状态
void Modbus_Func1()
{
  uint16_t Regadd,Reglen,crc;
	uint8_t i,j;
	Regadd = modbus.rcbuf[2]*256+modbus.rcbuf[3];//0000

	Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//0008
	
	//发送回应数据包
	i = 0;
	modbus.sendbuf[i++] = modbus.myadd;     
	modbus.sendbuf[i++] = 0x01;  //功能码  01          
  modbus.sendbuf[i++] = 0x01; //1个字节数据
	
	modbus.sendbuf[i++]=Switch_state_pro(Regadd);//数据
	

	crc = Modbus_CRC16(modbus.sendbuf,i);    
	modbus.sendbuf[i++] = crc/256;
	modbus.sendbuf[i++] = crc%256;
	//数据包打包完成
	RS485_TX;
	for(j=0;j<i;j++)//发送数据
	{
	  Modbus_Send_Byte(modbus.sendbuf[j]);	
	}
	RS485_RX;

}

1号功能码中调取的函数

对Reg数组规划是:0-7表示继电器,8-9表示从机地址和波特率设置位,10-17表示输入的光耦状态读取

//读取八个继电器或者光耦(继电器开始地址0,光耦开始地址10)
uint8_t Switch_state_pro(uint16_t Regadd)
{
		uint8_t Read_pin_state=0x00;//一个8位变量用来存储8个继电器的状态
		int i;
	if(Regadd==0||Regadd==0x0a)//表示读取连续的八个继电器或者8个光耦
	{
			//读取reg数据求就可以
			for(i=0;i<8;i++)//支持读取0-7八个继电器状态
			{
					if(Reg[Regadd+i]!=0)//表示收到的指令ff00是打开,相关位置置1操作
					{
							Read_pin_state|=(0x1<<i);//把第regadd位置1操作
					}
					else if(Reg[Regadd+i]==0)//表示收到的指令0000关闭置0操作
					{
							Read_pin_state&=~(0x1<<i);//把第regadd位置0操作
					}
			}
	}

		return Read_pin_state;//返回值就是8个继电器的状态(1个字节=8位=8个继电器)
}

在这里插入图片描述
返回的数据30转化为二进制表示0011 0000从低位到高位依次对应继电器0-7的状态
在这里插入图片描述
双击通过on或off按钮即可实现对继电器的控制(使用05功能码发送)
在这里插入图片描述

01功能码-查询8个继电器的状态

(二)加入功能码0x05实现继电器的控制

05功能码:实现继电器的控制(数据是ff00表示打开,数据是0000表示关闭)
在这里插入图片描述

主机发送控制指令解读
在这里插入图片描述
从机返回数据解读
(其实和06功能码一样,主机发送什么,从机将数据原路返回)
在这里插入图片描述
从左到右是06功能码改05功能码
在这里插入图片描述
05和06功能码如下

	// Modbus 5号功能码函数-add
// Modbus 主机写入寄存器值--控制单个继电器的打开和关闭-测试ok--没有写入到内存中
void Modbus_Func5()  
{
  uint16_t Regadd;
	uint16_t val;
	uint16_t 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++]=0x05;        
  modbus.sendbuf[i++]=Regadd/256;
	modbus.sendbuf[i++]=Regadd%256;
	modbus.sendbuf[i++]=val/256;
	modbus.sendbuf[i++]=val%256;
	crc=Modbus_CRC16(modbus.sendbuf,i);
	modbus.sendbuf[i++]=crc/256;  
	modbus.sendbuf[i++]=crc%256;
	//数据发送包打包完毕
	RS485_TX;//使能485控制端(启动发送)  
	for(j=0;j<i;j++)
	{
	 Modbus_Send_Byte(modbus.sendbuf[j]);
	}
	HAL_Delay(100);
	if(Regadd==0x09)
	{
		IIC_WriteSingleReg(0X04,Reg[9]);//将新的波特率数据对应的序号写入到eeprom中
		HAL_Delay(10)	;
		USART_BRR_Configuration(&huart1,baud_table[val]);//波特率修改
		__HAL_UART_ENABLE(&huart1);
	}
//	write_data(Regadd,val);//有内部存储
	
	PIN_set05(Regadd,val);//无内部存储
	
	RS485_RX;
}



// Modbus 6号功能码函数
// Modbus 主机写入寄存器值
void Modbus_Func6()  
{
  uint16_t Regadd;
	uint16_t val;
	uint16_t 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=Modbus_CRC16(modbus.sendbuf,i);
	modbus.sendbuf[i++]=crc/256;  
	modbus.sendbuf[i++]=crc%256;
	//数据发送包打包完毕
	RS485_TX;//使能485控制端(启动发送)  
	for(j=0;j<i;j++)
	{
	 Modbus_Send_Byte(modbus.sendbuf[j]);
	}
	HAL_Delay(100);
	if(Regadd==0x09)
	{
		IIC_WriteSingleReg(0X04,Reg[9]);//将新的波特率数据对应的序号写入到eeprom中
		HAL_Delay(10)	;
		USART_BRR_Configuration(&huart1,baud_table[val]);//波特率修改
		__HAL_UART_ENABLE(&huart1);
	}
//	write_data(Regadd,val);//有存储的情况
	PIN_set_06(Regadd,val);
	RS485_RX;
}

调用控制继电器函数如下:

//contro
void PIN_set_06( uint16_t Regadd,uint16_t val)
{
	switch(Regadd)
	{
		case 0:
		
				if(val==1)
				{
					KZ1_K;
				}
				else
				{
					KZ1_G;
				}
			break;
			
		case 1:
				if(val==1)
				{
					KZ2_K;
				}
				else
				{
					KZ2_G;
				}
			break;	
		case 2:
			
					if(val==1)
				{
					KZ3_K;
				}
				else
				{
					KZ3_G;
				}

			break;
		case 3:
				if(val==1)
				{
					KZ4_K;
				}
				else
				{
					KZ4_G;
				}
			break;
		case 4:
				if(val==1)
				{
					KZ5_K;
				}
				else
				{
					KZ5_G;
				}
			break;
		case 5:
				if(val==1)
				{
					KZ6_K;
				}
				else
				{
					KZ6_G;
				}
			break;
		case 6:
				if(val==1)
				{
					KZ7_K;
				}
				else
				{
					KZ7_G;
				}
			break;
		case 7:
				if(val==1)
				{
					KZ8_K;
				}
				else
				{
					KZ8_G;
				}
			break;

		
	}
}


void PIN_set05( uint16_t Regadd,uint16_t val)
{
	switch(Regadd)
	{
		case 0:
		
				if(val!=0)
				{
					KZ1_K;
				}
				else
				{
					KZ1_G;
				}
			break;
			
		case 1:
				if(val!=0)
				{
					KZ2_K;
				}
				else
				{
					KZ2_G;
				}
			break;	
		case 2:
			
				if(val!=0)
				{
					KZ3_K;
				}
				else
				{
					KZ3_G;
				}

			break;
		case 3:
				if(val!=0)
				{
					KZ4_K;
				}
				else
				{
					KZ4_G;
				}
			break;
		case 4:
			if(val!=0)
				{
					KZ5_K;
				}
				else
				{
					KZ5_G;
				}
			break;
		case 5:
				if(val!=0)
				{
					KZ6_K;
				}
				else
				{
					KZ6_G;
				}
			break;
		case 6:
			if(val!=0)
				{
					KZ7_K;
				}
				else
				{
					KZ7_G;
				}
			break;
		case 7:
			if(val!=0)
				{
					KZ8_K;
				}
				else
				{
					KZ8_G;
				}
			break;
	}
}

modbus-06功能码+05功能码演示

或者是切换到01功能码模式直接双击操作05写入控制(更便捷)
在这里插入图片描述
在这里插入图片描述

(三)加入功能码0x0F实现继电器的全开或者全闭功能

15功能码只实现了全开和全闭功能(只能同时控制8路),没加入同时控制多个继电器的代码

在这里插入图片描述
在这里插入图片描述
参照以上指令解析实现对继电器的全开和全闭

//功能码0x0f指令即十进制15
//实现全开和全闭合-ok
void Modbus_Func15()
{
		uint16_t Regadd;//地址16位
		uint16_t Reglen;
		uint16_t i,crc,j;
		uint16_t value;
		
		Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3];  //地址
		Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//寄存器个数(控制的个数)
		
		value=modbus.rcbuf[7];//FF是全开,00是全关

			if(value==0xff)//继电器全开
			{
					for(int i=0;i<8;i++)
					{
					
							Reg[i]=1;
							PIN_set05(i,0xff00);
					}
			}
			else if(value==0)//继电器全部关闭
			{
					for(int i=0;i<8;i++)
					{
					
							Reg[i]=0;
							PIN_set05(i,0);
					}
			}
			
		
		//以下为回应主机内容
		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];
		crc=Modbus_CRC16(modbus.sendbuf,6);
		modbus.sendbuf[6]=crc/256; 
		modbus.sendbuf[7]=crc%256;
		//数据发送包打包完毕
		
		RS485_TX;//使能485控制端(启动发送)  
		for(j=0;j<8;j++)
		{
			Modbus_Send_Byte(modbus.sendbuf[j]);
		}
		RS485_RX;//失能485控制端(改为接收)
	
}

modbus-15功能码演示

也可以在01功能码获取继电器状态的模式下(通过15功能码实现全开全闭----8路继电器)
全开状态:打上对勾(数据位显示1)
在这里插入图片描述

全闭合状态:不打对勾(数据位显示0)
在这里插入图片描述
在这里插入图片描述

(四)修改功能码0x10实现继电器的闪开和闪闭功能

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
内容已经删除(代码有问题)-有时间补充修改后的细节
使用delay进行延时会出现无法按时喂狗设备复位的情况,需要使用定时器中断的形式进行修改。
主要实现对n个继电器实现闪开和闪断功能(n<=8)
在这里插入图片描述

设置一个0.1s的定时器中断,当接收到闪开或者闪断的功能后打开定时器开始计时,到达设置的定时时间时关闭定时器,根据函数控制指令判别执行打开继电器还是关闭继电器。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐