本文目录
- 软件学习前言
- 代码思路
- 实操练习
软件学习前言
本篇介绍Modbus RTU的一些基本操作指令规则与代码示例讲解,Modbus RTU通常采用RS-485作为物理层,是一套业内共认的通讯协议,免费使用。主要的操作码有0x03、0x06、0x10,另外为了方面实际使用,我加了一个自定义操作码0xFF,用来跳过从机地址匹配情况,直接修改设备的从机地址,软件中也介绍了如何将从机地址写入单片机内部的Flash中去,掉电保存。本文通过讲解软件思路与实际编写代码,希望帮助大家快速地把Modbus RTU应用起来。
介绍一下本篇前面的一些软件介绍,之前讲解了时基操作、串口通讯、本篇属于串口通讯的上层应用。
(软件01)时基处理,单片机一种不错的代码思路https://blog.csdn.net/BEXZJ/article/details/134686717?spm=1001.2014.3001.5501
(软件03)单片机串口处理思路,超时接收的方法https://blog.csdn.net/BEXZJ/article/details/134827083
老规矩,一张封面图进入今天的分享。
代码思路
1.Modbus RTU简介
2.Modbus RTU常用操作码介绍
0x03:读多个寄存器,示例(01从机地址)如下
发送:01 03 00 00 00 03 CRC_H8 CRC_L8
发送解析:
01:从机地址
03:读多个操作码
00 00:寄存器开始地址
00 03:读3个数据
CRC_H8 CRC_L8:ModbusCRC16校验,H8为高八位,L8为低八位。
回复:01 03 06 AB AB CD CD EF EF CRC_H8 CRC_L8
回复解析:
01:从机地址
03:读多个操作码
06:数据个数
AB AB:寄存器00 00的值
CD CD:寄存器00 01的值
EF EF:寄存器00 02的值
CRC_H8 CRC_L8:ModbusCRC16校验,H8为高八位,L8为低八位。
0x06:写单个寄存器,示例(01从机地址)如下
发送:01 06 00 00 00 01 CRC_H8 CRC_L8
发送解析:
01:从机地址
06:写单个操作码
00 00:目标寄存器地址
00 03:写入的数据
CRC_H8 CRC_L8:ModbusCRC16校验,H8为高八位,L8为低八位。
回复:01 06 00 00 00 01 CRC_H8 CRC_L8
回复解析:与发送一致。
0x10:写多个寄存器
发送:01 10 00 00 00 03 06 AA BB CC DD EE FF CRC_H8 CRC_L8
发送解析:
01:从机地址
10:写多个操作码
00 00:目标寄存器开始地址
00 03:寄存器数量
06 :数据个数
AA BB:写入寄存器00 00的值
CC DD:写入寄存器00 01的值
EE FF:写入寄存器00 02的值
CRC_H8 CRC_L8:ModbusCRC16校验,H8为高八位,L8为低八位。
回复:01 10 00 00 00 03 CRC_H8 CRC_L8
回复解析:
01:从机地址
10:写多个操作码
00 00:目标寄存器开始地址
00 03:寄存器数量
CRC_H8 CRC_L8:ModbusCRC16校验,H8为高八位,L8为低八位。
3.代码设计
接收一帧的串口数据存在BUFF数组中,判断BUFF[0]是否为对应的从机地址,或者判断
BUFF[1]的操作码是否为自己定义的0xFF(走后门用,用来修改跳过从机地址修改从机地址,有点儿绕)。接着,对接收一帧的串口数据进行CRC校验,校验通过后,根据操作码进行Switch-Case分支进行处理,0x03的一类、0x06的一类、0x10的一类、以及我们自定义的0xFF的一类。再根据各自报文的解析规则、回复规则进行设计即可。
4.亮点设计
知道从机地址时,可以通过0x06进行改写从机地址;
不知道从机地址时,可以通过0xFF进行改写从机地址;
从机地址修改完后触发Flash写操作,把从机地址写入单片机片内Flash,掉电保存,下次上电后从Flash读取回来,作为从机地址。
实操练习
回顾之前的硬件篇,需要通过485来控制电机的运行,我们定义需要控制的Modbus点表。(硬件02)按键+电位器+485控制的电机调速电路实战,上篇https://blog.csdn.net/BEXZJ/article/details/134784629
代码部分(随着代码越来越复杂了,就不再像之前一样详细放出来了,这次就放一些关键的,感兴趣的粉丝可以留言评论,我私信给你,另外提一下,最近好多类似机器人的粉丝,我判断是正常的粉丝的话我会互关你的!)
串口接收处理源码
void zj_app_uart_send(uint8_t *pBuf,uint8_t mLen)
{
while(mLen--)
{
USART_SendData(BSP_485_USART,*pBuf++); //库函数,串口发送单个字节数据
while(USART_GetFlagStatus(BSP_485_USART, USART_FLAG_TRAC) == RESET);
}
}
void zj_app_uart_process(void)
{
if((zj_485_uart.rx_overtime_flag == TRUE) && (zj_485_uart.rx_overtime_ms==0))
{
//rx_overtime_flag=TRUE :有超时接收,rx_overtime_ms=0 :最后一个字节接收完成
zj_485_uart.rx_len = zj_485_uart.rx_index; //接收到的长度
zj_485_uart.rx_index = 0; //接收BUFF回到首位,为下次接收做准备
zj_485_uart.rx_overtime_flag = FALSE; //重置超时接收标记,为下次接收做准备
zj_485_uart.rx_finish_flag = TRUE; //标记接收完成
}
}
void zj_app_uart_10ms_process(void)
{
if(zj_485_uart.rx_finish_flag) //根据接收完成标记处理数据
{
zj_485_uart.rx_finish_flag = FALSE;//重置开始接收标记,为下次处理数据做准备
zj_app_uart_modbus_process(); //modbus
}
}
Modbus RTU 实例代码
#include "zj_public.h"
/*
寄存器地址 / 值 / 读写 / 值说明
0x0000 / 0-1 / WR / 开关控制:1开0关 默认关
0x0001 / 0-1 / WR / 顺逆方向:1逆0顺 默认顺
0x0002 / 0-1000 / WR / 速度等级千分比: 500代表一半的速度
0xFFFF / 1-247 /WR / 从机地址:01代表默认01的从机地址
uint8_t motor_onoff;
uint8_t motor_cwccw;
uint8_t motor_speed_level;
*/
void zj_app_uart_modbus_process(void)
{
uint16_t nCRC_Recv;
uint16_t nCRC_Check;
uint8_t nIndex = 0;
uint8_t CMD_REG_BUFF[64] = {0};
if(zj_485_uart.rx_buff[0] == zj_app_info.modbus_address || zj_485_uart.rx_buff[1] == 0xFF)
{
nCRC_Recv = zj_485_uart.rx_buff[zj_485_uart.rx_len-2] *256 + zj_485_uart.rx_buff[zj_485_uart.rx_len-1];
nCRC_Check = CRC16_Modbus_Calculate(zj_485_uart.rx_buff, zj_485_uart.rx_len-2);
if(nCRC_Check == nCRC_Recv)
{
switch(zj_485_uart.rx_buff[1])
{
case 0x03:
if(zj_485_uart.rx_buff[5] <= 14 && zj_485_uart.rx_buff[4] == 0)
{
zj_485_uart.tx_buff[0] = zj_app_info.modbus_address;
zj_485_uart.tx_buff[1] = 0x03;
zj_485_uart.tx_buff[2] = zj_485_uart.rx_buff[5]*2;
zj_485_uart.tx_len = zj_485_uart.tx_buff[2] + 5;
CMD_REG_BUFF[nIndex++] = zj_app_info.motor_onoff / 256 % 256;
CMD_REG_BUFF[nIndex++] = zj_app_info.motor_onoff % 256;
CMD_REG_BUFF[nIndex++] = zj_app_info.motor_cwccw / 256 % 256;
CMD_REG_BUFF[nIndex++] = zj_app_info.motor_cwccw % 256;
CMD_REG_BUFF[nIndex++] = zj_app_info.motor_speed_level / 256 % 256;
CMD_REG_BUFF[nIndex++] = zj_app_info.motor_speed_level % 256;
memcpy(&zj_485_uart.tx_buff[3],&CMD_REG_BUFF[zj_485_uart.rx_buff[3]*2],zj_485_uart.tx_buff[2]);
nCRC_Check = CRC16_Modbus_Calculate(zj_485_uart.tx_buff,zj_485_uart.tx_len - 2);
zj_485_uart.tx_buff[zj_485_uart.tx_len - 2] = nCRC_Check / 256 % 256;
zj_485_uart.tx_buff[zj_485_uart.tx_len - 1] = nCRC_Check % 256;
zj_app_uart_send(zj_485_uart.tx_buff,zj_485_uart.tx_len);
}
break;
case 0x06:
if(zj_485_uart.rx_buff[2] == 0)
{
switch(zj_485_uart.rx_buff[3])
{
case 0x00:
if(zj_485_uart.rx_buff[4] == 0x00 && zj_485_uart.rx_buff[5] == 0x01)
{
zj_app_info.motor_onoff = TRUE;//开始
zj_app_uart_send(zj_485_uart.rx_buff,8);
}
else if(zj_485_uart.rx_buff[4] == 0x00 && zj_485_uart.rx_buff[5] == 0x00)
{
zj_app_info.motor_onoff = FALSE;//停止
zj_app_uart_send(zj_485_uart.rx_buff,8);
}
break;
case 0x01:
if(zj_485_uart.rx_buff[4] == 0x00 && zj_485_uart.rx_buff[5] == 0x01)
{
zj_app_info.motor_cwccw = TRUE; //逆时针
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
zj_app_uart_send(zj_485_uart.rx_buff,8);
}
else if(zj_485_uart.rx_buff[4] == 0x00 && zj_485_uart.rx_buff[5] == 0x00)
{
zj_app_info.motor_cwccw = FALSE; //顺时针
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
zj_app_uart_send(zj_485_uart.rx_buff,8);
}
break;
case 0x02:
if((zj_485_uart.rx_buff[4] * 256 + zj_485_uart.rx_buff[5]) <= 1000)
{
zj_app_info.motor_speed_level = zj_485_uart.rx_buff[4] * 256 + zj_485_uart.rx_buff[5];
zj_app_uart_send(zj_485_uart.rx_buff,8);
}
break;
default:
break;
}
}
else
{
if((zj_485_uart.rx_buff[2] == 0xFF) && (zj_485_uart.rx_buff[3] == 0xFF))
{
if(zj_485_uart.rx_buff[4] == 0x00)
{
if(zj_485_uart.rx_buff[5] > 0 && zj_485_uart.rx_buff[5] <= 247)
{
zj_app_info.modbus_address = zj_485_uart.rx_buff[5];
zj_app_info.flash_save_flag = TRUE;
zj_app_info.flash_save_s_time = FLASH_SAVE_WAIT_S_TIME;
zj_app_uart_send(zj_485_uart.rx_buff,8);
}
}
}
}
break;
case 0x10:
if(zj_485_uart.rx_buff[2] == 0)
{
if(zj_485_uart.rx_buff[5]*2 != zj_485_uart.rx_buff[6])
return;
switch(zj_485_uart.rx_buff[5])
{
case 1:
switch(zj_485_uart.rx_buff[3])
{
case 0x00:
if(zj_485_uart.rx_buff[7]*256+zj_485_uart.rx_buff[8] == 0x0001)
{
zj_app_info.motor_onoff = TRUE;//开始
}
else if(zj_485_uart.rx_buff[7]*256+zj_485_uart.rx_buff[8] == 0x0000)
{
zj_app_info.motor_onoff = FALSE;//停止
}
break;
case 0x01:
if(zj_485_uart.rx_buff[7] == 0x00 && zj_485_uart.rx_buff[8] == 0x01)
{
zj_app_info.motor_cwccw = FALSE;//
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
}
else if(zj_485_uart.rx_buff[7] == 0x00 && zj_485_uart.rx_buff[8] == 0x00)
{
zj_app_info.motor_cwccw = TRUE;//
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
}
break;
case 0x02:
if((zj_485_uart.rx_buff[7] * 256 + zj_485_uart.rx_buff[8]) <= 1000)
{
zj_app_info.motor_speed_level = zj_485_uart.rx_buff[7] * 256 + zj_485_uart.rx_buff[8];
}
default:break;
}
break;
case 2:
switch(zj_485_uart.rx_buff[3])
{
case 0x00:
if(zj_485_uart.rx_buff[7]*256+zj_485_uart.rx_buff[8] == 0x0001)
{
zj_app_info.motor_onoff = TRUE;//开始
}
else if(zj_485_uart.rx_buff[7]*256+zj_485_uart.rx_buff[8] == 0x0000)
{
zj_app_info.motor_onoff = FALSE;//停止
}
if(zj_485_uart.rx_buff[9] == 0x00 && zj_485_uart.rx_buff[10] == 0x01)
{
zj_app_info.motor_cwccw = FALSE;//
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
}
else if(zj_485_uart.rx_buff[9] == 0x00 && zj_485_uart.rx_buff[10] == 0x00)
{
zj_app_info.motor_cwccw = TRUE;//
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
}
break;
case 0x01:
if(zj_485_uart.rx_buff[7] == 0x00 && zj_485_uart.rx_buff[8] == 0x01)
{
zj_app_info.motor_cwccw = FALSE;//
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
}
else if(zj_485_uart.rx_buff[7] == 0x00 && zj_485_uart.rx_buff[8] == 0x00)
{
zj_app_info.motor_cwccw = TRUE;//
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
}
if((zj_485_uart.rx_buff[9] * 256 + zj_485_uart.rx_buff[10]) <= 1000)
{
zj_app_info.motor_speed_level = zj_485_uart.rx_buff[9] * 256 + zj_485_uart.rx_buff[10];
}
break;
case 0x02:
//不满足数量
break;
default:break;
}
break;
case 3:
switch(zj_485_uart.rx_buff[3])
{
case 0x00:
if(zj_485_uart.rx_buff[7]*256+zj_485_uart.rx_buff[8] == 0x0001)
{
zj_app_info.motor_onoff = TRUE;//开始
}
else if(zj_485_uart.rx_buff[7]*256+zj_485_uart.rx_buff[8] == 0x0000)
{
zj_app_info.motor_onoff = FALSE;//停止
}
if(zj_485_uart.rx_buff[9] == 0x00 && zj_485_uart.rx_buff[10] == 0x01)
{
zj_app_info.motor_cwccw = FALSE;//
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
}
else if(zj_485_uart.rx_buff[9] == 0x00 && zj_485_uart.rx_buff[10] == 0x00)
{
zj_app_info.motor_cwccw = TRUE;//
zj_app_info.motor_onoff = FALSE; //切换正反转需要让电机停下来
}
if((zj_485_uart.rx_buff[11] * 256 + zj_485_uart.rx_buff[12]) <= 1000)
{
zj_app_info.motor_speed_level = zj_485_uart.rx_buff[11] * 256 + zj_485_uart.rx_buff[12];
}
break;
case 0x01:
//不满足数量
break;
case 0x02:
//不满足数量
break;
default:break;
}
break;
default:
break;
}
memcpy(&zj_485_uart.tx_buff,zj_485_uart.rx_buff,6);
nCRC_Check = CRC16_Modbus_Calculate(zj_485_uart.tx_buff,6);
zj_485_uart.tx_buff[6] = nCRC_Check / 256 % 256;
zj_485_uart.tx_buff[7] = nCRC_Check % 256;
zj_app_uart_send(zj_485_uart.tx_buff,8);
}
break;
case 0xFF:
if(zj_485_uart.rx_buff[2] == 0xFF && zj_485_uart.rx_buff[3] == 0xFF)
{
zj_app_info.modbus_address = zj_485_uart.rx_buff[5];
zj_app_info.flash_save_flag = TRUE;
zj_app_info.flash_save_s_time = FLASH_SAVE_WAIT_S_TIME;
zj_app_uart_send(zj_485_uart.rx_buff,8);
}
break;
default:
break;
}
}
}
}
从机地址保存到Flash处理,我设计的思路是每一次收到要保存到Flash的操作时,都给其一个倒计时FLASH_SAVE_WAIT_S_TIME = 3秒,在最后一需要保存的3秒后进行保存,如此可以减少Flash的擦写次数,让单片机更加耐用。
void zj_app_config_para_flash_write(void)
{
FLASH_Unlock();
FLASH_ErasePage(FLASH_PARA_SAVE_ADDRESS);
PARA_SAVE_BUFF[0] = zj_app_info.modbus_address % 256;;
FLASH_ProgramByte(FLASH_PARA_SAVE_ADDRESS+0,PARA_SAVE_BUFF[0]);
FLASH_Lock();
}
void zj_app_config_para_flash_read(void)
{
memcpy((uint8_t *)PARA_SAVE_BUFF ,(uint8_t *)FLASH_PARA_SAVE_ADDRESS, PARA_SAVE_LEN);
if(PARA_SAVE_BUFF[0] == 0xFF)
{
zj_app_info.modbus_address = 0x01;
}
else
zj_app_info.modbus_address = PARA_SAVE_BUFF[0];
zj_app_config_para_flash_write();
}
Flash相关时基处理
static void TimeProcess_1000MS(void)
{
if(zj_app_info.flash_save_flag)
{
if(zj_app_info.flash_save_s_time)
zj_app_info.flash_save_s_time--;
else
{
zj_app_info.flash_save_flag = 0;
zj_app_config_para_flash_write();
}
}
}
整个工程对应的头文件zj_public.h
#ifndef __ZJ_PUBLIC_H__
#define __ZJ_PUBLIC_H__
#include <at32f4xx.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include "at32f4xx_syscfg.h"
#include "at32f4xx_tim.h"
#include "at32f4xx_usart.h"
#include "at32f4xx_rcc.h"
#include "at32f4xx_iwdg.h"
#include "CRC16_Algorithm.h"
#define DEBUG 1
#define TRUE 1
#define FALSE 0
#define HAL_Delay_nMS Delay_ms
#define Delay_Us Delay_us
#define BSP_UART1_TX_GPIO_PIN GPIO_Pins_6
#define BSP_UART1_TX_GPIO_AF1_Source GPIO_PinsSource6
#define BSP_UART1_TX_GPIO_PORT GPIOB
#define BSP_UART1_RX_GPIO_PIN GPIO_Pins_7
#define BSP_UART1_RX_GPIO_AF1_Source GPIO_PinsSource7
#define BSP_UART1_RX_GPIO_PORT GPIOB
#define BSP_485_USART USART1
#define BSP_485_USART_IRQHandler USART1_IRQHandler
#define BSP_485_USART_BANDRATE 115200
#define BSP_485_USART_IRQN USART1_IRQn
#define BSP_485_USART_IRQN_LEVEL 2
#define BSP_485_USART_IRQN_HANDLER USART1_IRQHandler
//时基定时器
#define BSP_TIME_BASE_TIMER TMR6
#define BSP_TIME_BASE_TIMER_ARR 10-1
#define BSP_TIME_BASE_TIMER_PSC 7200-1
#define BSP_TIME_BASE_IRQN TMR6_GLOBAL_IRQn
#define BSP_TIME_BASE_IRQN_LEVEL 0
#define BSP_TIME_BASE_IRQN_HANDLER TMR6_GLOBAL_IRQHandler
//LED灯
#define BSP_LED_RUN_GPIO_PIN GPIO_Pins_10
#define BSP_LED_RUN_GPIO_PORT GPIOA
#define BSP_LED_RUN_ON GPIO_ResetBits(BSP_LED_RUN_GPIO_PORT, BSP_LED_RUN_GPIO_PIN)
#define BSP_LED_RUN_OFF GPIO_SetBits(BSP_LED_RUN_GPIO_PORT, BSP_LED_RUN_GPIO_PIN)
//串口数据缓存长度
#define UART_DATA_BUFF_LEN 512
/*
* @arg GPIO_AF_0: EVENTOUT, TIM15, SPI1, TIM17, MCO, SWDAT, SWCLK, TIM14,
* USART1, IR_OUT, SPI2
* @arg GPIO_AF_1: USART2, TMR3, USART1, USART2, EVENTOUT, I2C1, I2C2, TMR15, IR_OUT
* @arg GPIO_AF_2: TMR2, TMR1, EVENTOUT, TMR16, TMR17
* @arg GPIO_AF_3: USART2, I2C1, TMR15, EVENTOUT
* @arg GPIO_AF_4: I2C2, TMR14, USART2, I2C1
* @arg GPIO_AF_5: TMR1, TMR15, TMR16, TMR17, I2C2, MCO
* @arg GPIO_AF_6: EVENTOUT, SPI2
* @arg GPIO_AF_7: COMP1 OUT, COMP2 OUT, I2C2, SPI2
*/
//ADC
#define BSP_ADC ADC1
#define BSP_ADC_CHANNEL_NUMBER 3
#define BSP_ADC_0_7_PORT GPIOA
#define BSP_ADC_SPEED_IN_PIN GPIO_Pins_0
#define BSP_ADC_SPEED_IN_CHANNEL ADC_Channel_0
#define BSP_ADC_SPEED_IN_CHANNEL_INDEX 1
#define BSP_ADC_DMA_IRQN DMA1_Channel1_IRQn
#define BSP_ADC_DMA_IRQN_LEVEL 1
#define BSP_ADC_DMA_IRQN_HANDLER DMA1_Channel1_IRQHandler
#define FLASH_SAVE_WAIT_S_TIME 3 //3秒后保存
#define FLASH_PARA_SAVE_ADDRESS 0x800F000
#define PARA_SAVE_LEN 16
#define FLASH_APP_START_ADDRESS 0x8000000 // 0x8003000
typedef struct
{
uint8_t rx_buff[UART_DATA_BUFF_LEN];
uint16_t rx_index;
uint8_t rx_finish_flag;
uint8_t rx_overtime_flag;
uint16_t rx_overtime_ms;
uint16_t rx_len;
uint8_t tx_buff[UART_DATA_BUFF_LEN];
uint16_t tx_len;
}UART_INFO;
extern UART_INFO zj_485_uart;
typedef struct
{
uint8_t modbus_address;
uint8_t motor_onoff;
uint8_t motor_cwccw;
uint8_t motor_speed_level;
uint8_t flash_save_flag;
uint8_t flash_save_s_time;
}APP_INFO;
extern APP_INFO zj_app_info;
extern volatile uint64_t gTimeBase;
extern __IO uint16_t adc1_ordinary_valuetab[30];
extern uint8_t PARA_SAVE_BUFF[PARA_SAVE_LEN];
/*Delay function*/
void Delay_init(void);
void Delay_us(u32 nus);
void Delay_ms(u16 nms);
void Delay_sec(u16 sec);
void RCC_Configuration(void);
void GPIO_Configuration(void);
void NVIC_Configuration(void);
void EXTI_Configuration(void);
void zj_bsp_config(void);
void zj_app_init(void);
void zj_app_timebase_process(void);
void zj_app_timebase_1ms_process(void);
void zj_app_adc_avrg_process(void);
void zj_app_adc_ms_process(void);
void zj_app_uart_send(uint8_t *pBuf,uint8_t mLen);
void zj_app_uart_process(void);
void zj_app_uart_10ms_process(void);
void zj_app_uart_modbus_process(void);
void zj_app_config_para_flash_write(void);
#endif
如此,我们便实现了基于串口485的Modbus RTU通讯示例,支持0x03、0x06、0x10操作码。网上我看很少有0x10写多个的源码介绍的,我之前公司产品是也写了一个,如果写入的寄存器比较多的话,整个代码量是很大的,复制过程中也容易出错,重要是要预想到具体写多少个寄存器,一共10个寄存器可写的话,判断写1个有10个分支,写2个的有9个分支,3个的有8个分支...4个7分支...5个6分支...5分支...4分支...3分支...2分支...1分支,一共就55个分支了,想想都有点头大!
好了,11.27开始写博客分享到现在10天了,5K阅读量,100粉丝,感谢大家支持了,欢迎大家继续点点关注,有问题疑惑的也欢迎评论区留言,与大家共同学习。
小弟感谢大家的关注!
(利他之心,原创分享)