前言

参考文章/博主

CSDN博主–ONE_Day|

CRC循环冗余校验在线计算

Modbus-RTU通讯协议中CRC校验

RS485通信模块使用及代码【简】

用到的资料

阿里云盘

提取码:xbbL

RS-232

RS-232为 全双工 的通信传输接口,由电子工业协会(EIA)制定,是个人计算机上的通讯接口之一,通常以9个引脚(DB-9)出现,传输距离通常十几米。

  • 电气特性如下:

逻辑1: -3V~-15V

逻辑0: +3V~+15V

  • 应用电路

  • 电平转换芯片手册

MAX3232

  • 协议

UART

RS-485

RS485为 半双工(准双工) 的通信传输接口,采用差分传输(两条信号线上传输幅值相等相位相反的电信号),传输距离远至一千多米,该接口标准只规定了电气特性,并没有规定接插件、传输电缆与应用层通信协议。

  • 电气特性如下:

逻辑1: A-B >= 200mV

逻辑0: A- B<= 200mv

  • 应用电路

这个自动收发过程:

首先单片机默认下管脚是高电平,即Q2导通 ,集电极输出低电平,然后由于这个芯片是低电平有效处于接收模式(所以这个电路默认485处于一个接收模式)

发送的话,由于UART发送时起始位是发送一个0,然后Q2截止,RE’ 就由于上拉3.3就处于高电平,DI此时是接地,低电平,那设备接收的数据就是低电平了,然后就准备接收数据,此时UART发送0的话接收那边也是按照上面那样接收0,发1的话Q2导通,芯片就变成处于接收模式了,此时DI的低电平传不过去,此时右边的两个上下拉电阻就起到作用,就通过这两个上下拉来进行传输

  • 芯片手册

SP3485

Modbus协议

了解

Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而

发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。

Modbus协议属于应用层的报文传输协议,Modbus协议本身是个比较泛的说法,它有三种类型,分别是 Modbus ASCIIModbus RTUModbus TCP/IP,三者的协议并不相同,但有类似的地方

线圈:因为Modbus最初是为PLC服务的,所以线圈是PLC相关的术语,实际上就可以类比为开关量(继电器状态),每一个bit对应一个信号的开关状态,要么是1,要么是0;所以一个byte就可以同时控制8路的信号。比如控制外部8路io的高低。线圈寄存器支持读也支持写,写在功能码里面又分为写单个线圈和写多个线圈;

离散输入:如果线圈寄存器理解了这个自然也明白了。离散输入寄存器就相当于线圈的只读模式,他也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。比如我读取外部按键的按下还是松开。

保持寄存器:这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。一般对应参数设置,比如我设置时间年月日,不但可以写也可以读出来现在的时间。写也分为单个写和多个写。

输入寄存器:这个和保持寄存器类似,但是也是只支持读而不能写,一般是读取各种实时数据。一个寄存器也是占据两个byte的空间。类比我我通过读取输入寄存器获取现在的AD采集值。

注意(网络术语)

线圈 = 输出线圈 = 开关量输出 = 位状态

离散量输入 = 输入线圈 = 开关量输入

保持寄存器 = 输出寄存器 = 寄存器

Modbus数据模型 = PLC存储区

  • Modbus地址模型(真实物理存储区)

存储区范围:分为5位和6位,对应了标准地址和扩展地址;如果使用的是5位标准地址,则4种存储区分别用5位地址中的首位来区分,如线圈是0,离散量是1,输入寄存器是3,保持寄存器是4,剩下的4位就从1 ~ 9999开始编址,表示一片连续的地址

有些设备是6位的地址,编址方法跟5位的类似,只不过地址最大只能到65536

  • 连接方式

RS-485连接采用 菊花链 方式连接,而不能采用星型网络拓扑

  • RTU

  1. 地址码是每次通讯信息帧的第一字节(8位),从0到247。其中0为广播地址,从机的实际地址范围为 1 ~ 247;这个字节表明由用户设置地址的从机将接收由主机发送来的信息。每个从机都必须有唯一的地址码,并且只有符合地址码的从机才能响应回送信息。当从机回送信息时,回送数据均以各自的地址码开始。主机发送的地址码表明将发送到的从机地址,而从机返回的地址码表明回送的从机地址。相应的地址码表明该信息来自于何处。

  • 报文格式

  • CRC

一帧数据可能是8个字节或者更多,CRC占两个字节,所以计算CRC只需要计算一帧数据里CRC前面的字节即可

  • 设置

一般工业使用的波特率是 9600 或者 4800,很少使用 115200,原因是工业一般环境比较恶劣,而且要求的数据必须要抗干扰,波特率太快虽然传输的速度快但是抗干扰能力就弱了

数据位是8,停止位是1,校验位:无,地址:1(每一个从设备都有一个固定的地址)

寄存器地址定义示例:

数据需要注意范围还有小数或者其他,需要编码,因为传输的是十六进制整数,所以一般把数据编码成十进制然后转十六进制,接收方接收到数据则进行解码

编程示例1

介绍:基于STM32F103ZET6,采用串口3,485型号采用 SP3485EN

  • 硬件连接

STM32IO 外设
USART3_RX(PB11) RO
PG10 DE_RE(芯片使能)
USART3_TX(PB10) DI
  • MX配置
  1. PG10默认初始状态低电平,即处于接收状态
  2. 发送和接收都使用DMA,但是接收不需要打开DMA中断( 方法2则需要打开并且设置优先级0)
  3. 注意NVIC需要配置一下优先级,否则可能通信有问题,把DMA优先级设置为最高0(搬运串口数据),串口3中断设置为1,其他的比如普通定时器计数则可以设置为2
  • 程序编写

初始化.c

Myinit.c
cpp
#include "AllHead.h"

void Hardware_Init(void)
{
    // 使能串口3空闲中断
    __HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
    // 串口3开启DMA接收
    HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, UART3_Rec_LENGTH);
}

通用串口头文件

UART.h
cpp
#ifndef __UART_H
#define __UART_H
#include "AllHead.h"

// 定义枚举类型
typedef enum
{
    TTL = (uint8_t)0,
    RS_485 = (uint8_t)1,
    RS_232 = (uint8_t)2,
} Interface_Type_t;

// 定义异步串口结构体类型
typedef struct
{
    uint8_t *pucSend_Buffer; // 发送缓存指针
    uint8_t *pucRec_Buffer;  // 接收缓存指针

    void (*SendArray)(uint8_t *, uint16_t); // 串口发送数组
    void (*SendString)(uint8_t *);          // 串口发送字符串

    uint8_t Interface_Type;           // 接口类型
    void (*RS485_Set_SendMode)(void); // RS-485接口设置为发送模式
    void (*RS485_Set_RecMode)(void);  // RS-485接口设置为接收模式

} UART_t;

#endif

自带的.c

方法2

  1. 硬件初始化那不需要再使能IDLE中断只需要使能DMA接收中断
  2. 也不需要在stm32f1xx_it.c里添加任何代码在USART3_IRQHandler()函数里
  3. 只需要在回调函数里添加即可
cpp
// 串口接收完成空闲中断回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if(huart->Instance == huart3.Instance)
    {
        //Modbus协议解析
        Modbus.Protocol_Analysis(&UART3);
        //继续接收数据
        HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, UART3_Rec_LENGTH);
    }
}
stm32f1xx_it.c
cpp
void USART3_IRQHandler(void)
{
  /* USER CODE BEGIN USART3_IRQn 0 */

	//检测串口空闲中断
	if(SET == __HAL_UART_GET_FLAG(&huart3,UART_FLAG_IDLE))
	{
        // 清除中断标志位
		__HAL_UART_CLEAR_IDLEFLAG(&huart3);
        // 调用用户自定义的函数
		HAL_UART_IdleCallback(&huart3);
	}
	
  /* USER CODE END USART3_IRQn 0 */
  HAL_UART_IRQHandler(&huart3);
  /* USER CODE BEGIN USART3_IRQn 1 */

  /* USER CODE END USART3_IRQn 1 */
}
UART3.h
cpp
#ifndef __UART3_H
#define __UART3_H
#include "AllHead.h"

// 发送数据的长度
#define UART3_Send_LENGTH  20
// 接收数据的长度
#define UART3_Rec_LENGTH 	 20

extern UART_t  UART3;

#endif
UART3.c
cpp
#include "AllHead.h"

/*====================================静态内部变量/函数声明区 BEGIN====================================*/
static uint8_t  ucSend_Buffer[UART3_Send_LENGTH] = {0x00};
static uint8_t  ucRec_Buffer [UART3_Rec_LENGTH]  = {0x00};

static void SendArray(uint8_t*,uint16_t);  //串口发送数组
static void SendString(uint8_t*);          //串口发送字符串

static void RS485_Set_SendMode(void); //RS-485接口设置为发送模式
static void RS485_Set_RecMode(void);  //RS-485接口设置为接收模式
/*====================================静态内部变量/函数声明区    END====================================*/

UART_t  UART3 = 
{
	ucSend_Buffer,
	ucRec_Buffer,

	SendArray,
	SendString,

	RS_485,
	RS485_Set_SendMode,
	RS485_Set_RecMode
};

/*
	* @name   SendArray
	* @brief  串口发送数组
	* @param  p_Arr:数组首地址,LEN:发送长度
	* @retval None      
*/
static void SendArray(uint8_t* p_Arr,uint16_t LEN) 
{
    // 设置为发送模式
	UART3.RS485_Set_SendMode();	
	HAL_UART_Transmit_DMA(&huart3,p_Arr,LEN);
    // 发送完成后会调用发送完成回调函数...
}

/*
	* @name   SendString
	* @brief  发送字符串
	* @param  p_Str:待发送字符串
	* @retval None      
*/
static void SendString(uint8_t* p_Str) 
{	
    // 设置为发送模式    
	UART3.RS485_Set_SendMode();
	HAL_UART_Transmit(&huart3, p_Str,strlen((const char*)p_Str), 10);
    // 发送完成设置为接收模式    
	UART3.RS485_Set_RecMode();
}

/*
	* @name   RS485_Set_SendMode
	* @brief  RS-485接口设置为发送模式
	* @param  None
	* @retval None      
*/
static void RS485_Set_SendMode()
{
	HAL_GPIO_WritePin(GPIOG,GPIO_PIN_10,GPIO_PIN_SET);
	HAL_Delay(1);
}

/*
	* @name   RS485_Set_RecMode
	* @brief  RS-485接口设置为接收模式
	* @param  None
	* @retval None      
*/
static void RS485_Set_RecMode()
{
	HAL_GPIO_WritePin(GPIOG,GPIO_PIN_10,GPIO_PIN_RESET);
	HAL_Delay(1);
}

// 发送完成回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == huart3.Instance)
    {
        // 设为接收模式
        UART3.RS485_Set_RecMode();
    }
}

// 串口3空闲中断回调函数
void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == huart3.Instance)
    {
        // 解析协议
        Modbus.Protocol_Analysis(&huart3);
    	// 串口3开启DMA接收
    	HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, UART3_Rec_LENGTH);
    }
}

Protocol_Analysis() 函数里选择新开一个结构体指针变量原因是使得该函数更加健壮和灵活,可以保证函数内部使用的指针变量不会被外部改变,从而避免出现意外的错误,也有助于提高代码的可读性和理解性

使用 const 关键字来修饰一个指针变量是为了防止在函数内部意外修改该指针所指向的内存区域,所以,在不需要改变传入参数的情况下,应该始终使用 const 修饰传入指针变量,这有助于提高代码的健壮性和可维护性

读寄存器的话是从开始地址 40001 开始的转换十六进制就是 0x9C41

发送放数据需要编码变成正数发送

判断接收的数据的话可以自定义地址,这里一开始定义了(这里写反了40003是继电器40004是蜂鸣器才对)

modbus.c
cpp
#include "AllHead.h"

#define FunctionCode_Read_Register 		(uint8_t)0x03
#define FunctionCode_Write_Register 	(uint8_t)0x06
#define Modbus_Order_LENGTH           (uint8_t)8
      
static void Protocol_Analysis(UART_t*);  //协议分析

static void Modbus_Read_Register(UART_t*);   //读寄存器
static void Modbus_Wrtie_Register(UART_t*);  //写寄存器

Modbus_t  Modbus = 
{
	1,
	
	Protocol_Analysis
};

/*
	* @name   Protocol_Analysis
	* @brief  协议分析
	* @param  UART -> 串口指针
	* @retval None
*/
static void Protocol_Analysis(UART_t *UART)
{
    UART_t *const  COM = UART;
    uint8_t i = 0, Index = 0;

    //串口3停止DMA接收
    HAL_UART_DMAStop(&huart3);

    //过滤干扰数据,首字节为modbus地址,共8字节
    for(i = 0; i < UART3_Rec_LENGTH; i++)
    {
        //检测键值起始数据Modbus.Addr
        if(Index == 0)
        {
            if(*(COM->pucRec_Buffer + i) != Modbus.Addr)
                // 跳过下面的代码i++进行下一次循环
                continue;
        }

        *(COM->pucRec_Buffer + Index) = *(COM->pucRec_Buffer + i);

        //已读取8个字节
        if(Index == Modbus_Order_LENGTH)
            break;

        Index++;
    }

    //计算CRC-16
    CRC_16.CRC_Value   =  CRC_16.CRC_Check(COM->pucRec_Buffer, 6); //计算CRC值
    CRC_16.CRC_H       = (uint8_t)(CRC_16.CRC_Value >> 8);
    CRC_16.CRC_L       = (uint8_t)CRC_16.CRC_Value;

    //校验CRC-16(为了兼容不同市面上的协议,高字节在前或者低字节在前)
    if(((*(COM->pucRec_Buffer + 6) == CRC_16.CRC_L) && (*(COM->pucRec_Buffer + 7) == CRC_16.CRC_H))
            ||
            ((*(COM->pucRec_Buffer + 6) == CRC_16.CRC_H) && (*(COM->pucRec_Buffer + 7) == CRC_16.CRC_L)))
    {
        //校验地址
        if((*(COM->pucRec_Buffer + 0)) == Modbus.Addr)
        {
            //处理数据
            if((*(COM->pucRec_Buffer + 1)) == FunctionCode_Read_Register)
            {
                Modbus_Read_Register(COM);
            }
            else if((*(COM->pucRec_Buffer + 1)) == FunctionCode_Write_Register)
            {
                Modbus_Wrtie_Register(COM);
            }
        }
    }

    //清缓存
    for(i = 0; i < UART3_Rec_LENGTH; i++)
    {
        *(COM->pucRec_Buffer + i) = 0x00;
    }
}

/*
	* @name   Modbus_Read_Register
	* @brief  读寄存器
	* @param  UART -> 串口指针
	* @retval None
*/
static void Modbus_Read_Register(UART_t *UART)
{
    UART_t *const  COM = UART;

    //校验地址
    if((*(COM->pucRec_Buffer + 2) == 0x9C) && (*(COM->pucRec_Buffer + 3) == 0x41))
    {
        ////回应数据
        //地址码
        *(COM->pucSend_Buffer + 0)  = Modbus.Addr;
        //功能码
        *(COM->pucSend_Buffer + 1)  = FunctionCode_Read_Register;
        //数据长度
        *(COM->pucSend_Buffer + 2)  = 8;
        //SHT30温度--先发高8位再发低8位
        *(COM->pucSend_Buffer + 3)  = ((uint16_t)((SHT30.fTemperature + 40) * 10)) / 256;
        *(COM->pucSend_Buffer + 4)  = ((uint16_t)((SHT30.fTemperature + 40) * 10)) % 256;
        //SHT30湿度
        *(COM->pucSend_Buffer + 5)  = 0;
        *(COM->pucSend_Buffer + 6)  = SHT30.ucHumidity;
        //继电器状态
        *(COM->pucSend_Buffer + 7)  = 0;
        *(COM->pucSend_Buffer + 8)  = Relay.Status;
        //蜂鸣器状态
        *(COM->pucSend_Buffer + 9)  = 0;
        *(COM->pucSend_Buffer + 10) = Buzzer.Status;

        //插入CRC
        CRC_16.CRC_Value = CRC_16.CRC_Check(COM->pucSend_Buffer, 11); //计算CRC值,因为CRC前有11个字节
        CRC_16.CRC_H     = (uint8_t)(CRC_16.CRC_Value >> 8);
        CRC_16.CRC_L     = (uint8_t)CRC_16.CRC_Value;
		// 低位在前高位在后
        *(COM->pucSend_Buffer + 11) = CRC_16.CRC_L;
        *(COM->pucSend_Buffer + 12) = CRC_16.CRC_H;

        //发送数据
        UART3.SendArray(COM->pucSend_Buffer, 13);
    }
}

/*
	* @name   Modbus_Read_Register
	* @brief  写寄存器
	* @param  UART -> 串口指针
	* @retval None
*/
static void Modbus_Wrtie_Register(UART_t *UART)
{
    UART_t *const  COM = UART;
    uint8_t i;

    ////回应数据
    //准备数据
    for(i = 0; i < 8; i++)
    {
        *(COM->pucSend_Buffer + i) = *(COM->pucRec_Buffer + i);
    }
    //发送数据
    UART3.SendArray(COM->pucSend_Buffer, 8);

    //提取数据
    //校验地址 -> 继电器
    if((*(COM->pucRec_Buffer + 2) == 0x9C) && (*(COM->pucRec_Buffer + 3) == 0x43))
    {
        //控制继电器
        if(*(COM->pucRec_Buffer + 5) == 0x01)
        {
            Relay.Relay_ON();
        }
        else
        {
            Relay.Relay_OFF();
        }
    }

    //校验地址 -> 蜂鸣器
    if((*(COM->pucRec_Buffer + 2) == 0x9C) && (*(COM->pucRec_Buffer + 3) == 0x44))
    {
        //控制蜂鸣器
        if(*(COM->pucRec_Buffer + 5) == 0x01)
        {
            Buzzer.ON();
        }
        else
        {
            Buzzer.OFF();
        }
    }
}
modbus.h
cpp
#ifndef __MODBUS_H
#define __MODBUS_H

#include "AllHead.h"

//定义结构体类型
typedef struct
{
	uint16_t Addr;                       //地址
	
	void (*Protocol_Analysis)(UART_t*);  //协议分析
} Modbus_t;

extern Modbus_t Modbus;

#endif
CRC_16.c
cpp
#include "AllHead.h"

static uint16_t CRC_Check(uint8_t *, uint8_t); //CRC校验

// 初始化结构体
CRC_16_t  CRC_16 = {0, 0, 0, CRC_Check};

/*******************************************************
说明:CRC添加到消息中时,低字节先加入,然后高字

CRC计算方法:
 1.预置1个16位的寄存器为十六进制FFFF(即全为1);称此寄存器为CRC寄存器;
 2.把第一个8位二进制数据(既通讯信息帧的第一个字节)与16位的CRC寄存器的低
 8位相异或,把结果放于CRC寄存器;
 3.把CRC寄存器的内容右移一位(朝低位)用0填补最高位,并检查右移后的移出位;
 4.如果移出位为0:重复第3步(再次右移一位);
 如果移出位为1:CRC寄存器与多项式A001(1010 0000 0000 0001)进行异或;
 5.重复步骤3和4,直到右移8次,这样整个8位数据全部进行了处理;
 6.重复步骤2到步骤5,进行通讯信息帧下一个字节的处理;
 7.将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低
 字节进行交换;
********************************************************/

/*
	* @name   CRC_Check
	* @brief  CRC校验
	* @param  CRC_Ptr->数组指针,LEN->长度
	* @retval CRC校验值
*/
static uint16_t CRC_Check(uint8_t *CRC_Ptr, uint8_t LEN)
{
    uint16_t CRC_Value = 0;
    uint8_t  i         = 0;
    uint8_t  j         = 0;

    CRC_Value = 0xffff;
    for(i = 0; i < LEN; i++)
    {
        CRC_Value ^= *(CRC_Ptr + i);
        for(j = 0; j < 8; j++)
        {
            if(CRC_Value & 0x00001)
                CRC_Value = (CRC_Value >> 1) ^ 0xA001;
            else
                CRC_Value = (CRC_Value >> 1);
        }
    }
    CRC_Value = ((CRC_Value >> 8) +  (CRC_Value << 8)); //交换高低字节

    return CRC_Value;
}
CRC_16.h
cpp
#ifndef __CRC_16_H
#define __CRC_16_H
#include "AllHead.h"

//定义结构体类型
typedef struct
{
	uint16_t CRC_Value; //CRC校验值
	uint8_t  CRC_H;     //高位
	uint8_t  CRC_L;     //地位
	uint16_t (*CRC_Check)(uint8_t*,uint8_t);  //CRC校验
} CRC_16_t;

extern CRC_16_t CRC_16;

#endif

实验现象

上位机打开后就会默认开始发送数据,8个字节假设 01 03 9C 41 00 04 4D 3A,单片机接收到后会调用 Protocol_Analysis 函数进行解析,先判断主地址是否一致,然后判断CRC校验是否一致,一致则判断功能码,03则调用 Modbus_Read_Register函数,06则调用 Modbus_Wrtie_Register 函数,这里的话调用前者,进入Read函数里先进行判断地址是否一致,一致则进行把单片机相关状态写入一个数组里按照协议格式发送回去,上位机也可以进行控制单片机,上位机点击打开蜂鸣器的话就会进行写寄存器操作,发送数据 01 06 9C 44 00 01 4F 26,单片机接收到后会触发Write函数,在里面先进行回传一模一样的数据,然后进行判断是不是蜂鸣器的地址,是则判断数据,根据数据来决定是否要响

编程示例2

介绍:基于MSP430F149,采用串口1,485型号采用 SP3485EEN-L

  • 硬件连接

  • 协议定义

配合MSP上位机

读VIN电压值、BAT电压值、路灯亮度、充电状态指令:
发 -> 01 03 9C 41 00 04 4D 3A (其中4D 3A为CRC_L CRC_H)
收 -> 01 03 06 VIN电压值 BAT电压值 路灯亮度值 充电状态 CRC_L CRC_H

设置路灯亮度指令:
发 -> 01 06 9C 43 亮度值 CRC_L CRC_H
收 -> 01 06 9C 43 亮度值 CRC_L CRC_H

最亮: 01 06 9C 43 00 00 4E 56
80%亮:01 06 9C 43 03 E8 F0 56
60%亮:01 06 9C 43 07 D0 E2 55
40%亮:01 06 9C 43 0B B8 0C 51
20%亮:01 06 9C 43 0F A0 C6 53
灭: 01 06 9C 43 13 89 D8 9A

  • 程序编写
usart1.h
cpp
#ifndef __USART1_H
#define __USART1_H
#include <main.h>

// 接收和发送最大字节
#define USART1_Send_LEN 20
#define USART1_Rec_LEN  20

typedef struct
{
  uint8_t volatile ucRec_Flag;  // 接收标志位
  uint8_t volatile ucRec_Cnt;   // 接收计数
  uint8_t* pucSend_Buffer;      // 发送缓存指针
  uint8_t* pucRec_Buffer;       // 接收缓存指针
  
  void (*vUSART1_Init)(void);   // 串口1初始化
  void (*vUSART1_SendArray)(uint8_t*, uint16_t);        // 发送数组
  void (*vUSART1_SendString)(uint8_t*); // 发送字符串
  void (*vUSART1_Protocol)(void);       // 接口协议
  
}USART1_t;


extern USART1_t USART1;

#endif
usart1.c
cpp
/***************************************************************************
 * File          : usart1.c
 * Author        : Luckys.
 * Date          : 2023-06-06
 * description   : 串口1  
****************************************************************************/

#include <main.h>

/*====================================variable definition declaration area BEGIN=================================*/
static uint8_t ucSend_Buffer[USART1_Send_LEN];  //发送数组
static uint8_t ucRec_Buffer[USART1_Rec_LEN];    // 接收数组
/*====================================variable definition declaration area   END=================================*/


/*====================================static function declaration area BEGIN====================================*/
static void vUSART1_Init(void);   // 串口1初始化
static void vUSART1_SendArray(uint8_t*, uint16_t);        // 发送数组
static void vUSART1_SendString(uint8_t*); // 发送字符串
static void vUSART1_SendData(uint8_t);  // 发送字符
static void vUSART1_Protocol(void);     // 接口协议
/*====================================static function declaration area   END====================================*/

USART1_t USART1 = 
{
  FALSE,
  0,
  ucSend_Buffer,
  ucRec_Buffer,
  
  vUSART1_Init,
  vUSART1_SendArray,
  vUSART1_SendString,
  vUSART1_Protocol,
};

/*
* @function     : vUSART1_Init
* @param        : None
* @retval       : None
* @brief        : 串口1初始化
*/
static void vUSART1_Init(void)
{
  P3SEL |= BIT6 + BIT7; // 开启复用引脚功能P36(TX) P3(RX)
  // 参数设置
  UCTL1 |= SWRST;       // 模块处于复位状态(默认已经置1,此行可要可不要)
  ME2 |= UTXE1 + URXE1; // 使能串口1发送和接收
  UCTL1 |= CHAR;        // 数据长度选择8位
  // 波特率设置 -- 手册查询可知:9600pcs 对应 00 03 4A
  UTCTL1 |= SSEL0;      // 配置ACLK
  UBR11 = 0x00; // UxBR1
  UBR01 = 0x03; // UxBR0
  UMCTL1 = 0x4A;        // UxMCTL
  UCTL1 &= ~SWRST;      // 把SWRST置0,启动模块
  // 开启接收中断
  IE2 |= URXIE1;
}

/*
* @function     : vUSART1_SendArray
* @param        : p_Arr --> 要发送的数组 Arr_len --> 数据的长度
* @retval       : None
* @brief        : 发送数组
*/
static void vUSART1_SendArray(uint8_t* p_Arr, uint16_t Arr_len)
{
  uint16_t i;
  
  for (i = 0; i < Arr_len; i++)
  {
    vUSART1_SendData(*(p_Arr + i));
  }
}

/*
* @function     : vUSART1_SendString
* @param        : p_Str --> 要发送的字符串
* @retval       : None
* @brief        : 发送数组
*/
static void vUSART1_SendString(uint8_t* p_Str)
{
  while (*p_Str)
  {
    vUSART1_SendData(*(p_Str++));
  }
}

/*
* @function     : vUSART1_SendData
* @param        : ch --> 要发送的字符数据
* @retval       : None
* @brief        : 发送字符
*/
static void vUSART1_SendData(uint8_t ch)
{
  while (!(IFG2 & UTXIFG1));    // 等待为空才能发送
  TXBUF1 = ch;
}

// putchar函数 重定向
extern int putchar(int c)
{
  vUSART1_SendData((uint8_t)c);
  
  return c;
}

// 串口接收中断
#pragma vector = UART1RX_VECTOR
__interrupt void USART1_RX_isr(void)
{
  uint8_t Rec_Data;
  
  if (USART1.ucRec_Cnt < USART1_Rec_LEN)
  {
    // 提取数据
    Rec_Data = RXBUF1; 
    ucRec_Buffer[USART1.ucRec_Cnt++] = Rec_Data;
  }
  // 置位接收标志位
  USART1.ucRec_Flag = TRUE;
}

/*
* @function     : vUSART1_Protocol
* @param        : None
* @retval       : None
* @brief        : 发送字符
*/
static void vUSART1_Protocol(void)
{
  if (USART1.ucRec_Flag == TRUE)
  {
    // 过滤干扰数据
    if (ucRec_Buffer[0] != 0)
    {
      TimerA.usUSART1_Delay_Timer = 0;
      while (USART1.ucRec_Cnt < 8)
      {
        if (TimerA.usUSART1_Delay_Timer >= TimerA_100ms)
        {
          break;
        }
      }
      // 协议分析
      Modbus.vModbus_Protocol_Analysis(&USART1);
    }
    // 重新接收
    USART1.ucRec_Cnt = 0;
    USART1.ucRec_Flag = FALSE;
  }
}

modbus.h
cpp
#ifndef __MODBUS_H
#define __MODBUS_H
#include <main.h>

// 功能码
#define Modbus_Function_NUM_Read        (uint8_t)0x03
#define Modbus_Function_NUM_Write       (uint8_t)0x06

typedef struct
{
  uint16_t CRC; // CRC校验值
  uint16_t CRC_H;       // 高位
  uint16_t CRC_L;       // 低位
  uint16_t (*CRC_16_Check)(uint8_t*, uint8_t);  // CRC校验
}CRC_16_t;

typedef struct
{
  uint16_t Modbus_Addr;        // 地址
  
  void (*vModbus_Protocol_Analysis)(USART1_t*); // Modbus协议解析
}Modbus_t;


extern CRC_16_t CRC_16;
extern Modbus_t Modbus;

#endif
modbus.c
cpp
/***************************************************************************
 * File          : modbus.c
 * Author        : Luckys.
 * Date          : 2023-06-09
 * description   : modbus协议
****************************************************************************/


#include <main.h>


/*====================================static function declaration area BEGIN====================================*/
static uint16_t CRC_16_Check(uint8_t*, uint8_t);        // CRC校验
static void vModbus_Protocol_Analysis(USART1_t*);       // Modbus协议解析
static void vModbus_Read_Register(USART1_t*);   // 读寄存器
static void vModbus_Write_Register(USART1_t*);   // 写寄存器
/*====================================static function declaration area   END====================================*/


CRC_16_t CRC_16 = 
{
  0,
  0,
  0,
  CRC_16_Check,
};

Modbus_t Modbus = 
{
  1,
  vModbus_Protocol_Analysis,
};


/*
* @function     : CRC_16_Check
* @param        : p_Arr -> 数组指针 LEN -> 数组长度
* @retval       : None
* @brief        : CRC校验
*/
static uint16_t CRC_16_Check(uint8_t* p_Arr, uint8_t LEN)
{
  uint16_t CRC_Value = 0;
  uint8_t i = 0,j = 0;
  
  CRC_Value = 0xFFFF;
  
  for (i = 0; i < LEN; i++)
  {
    CRC_Value ^= *(p_Arr + i);
    for (j = 0; j < 8; j++)
    {
      if (CRC_Value & 0x00001)
      {
        CRC_Value = (CRC_Value >> 1) ^ 0xA001;
      }
      else
      {
        CRC_Value = (CRC_Value >>1);
      }
    }
  }
  CRC_Value = (CRC_Value >>8) + (CRC_Value <<8);        // 交换高低字节
  
  return CRC_Value;
}

/*
* @function     : vModbus_Protocol_Analysis
* @param        : UARTx -> 串口指针
* @retval       : None
* @brief        : Modbus协议解析
*/
static void vModbus_Protocol_Analysis(USART1_t* UART)
{
  USART1_t* const COM = UART;
  
  // 计算CRC
  CRC_16.CRC = CRC_16.CRC_16_Check(COM->pucRec_Buffer,6);
  CRC_16.CRC_H = (uint8_t)(CRC_16.CRC >> 8);
  CRC_16.CRC_L = (uint8_t)CRC_16.CRC;
  
  // 检验
  if(((*(COM->pucRec_Buffer + 6) == CRC_16.CRC_L) && (*(COM->pucRec_Buffer + 7) == CRC_16.CRC_H)) || 
     ((*(COM->pucRec_Buffer + 6) == CRC_16.CRC_H) && (*(COM->pucRec_Buffer + 7) == CRC_16.CRC_L)))
  {
    // 校验地址
    if ((*(COM->pucRec_Buffer + 0)) == Modbus.Modbus_Addr)
    {
      // 处理数据
      if ((*(COM->pucRec_Buffer + 1)) == Modbus_Function_NUM_Read)        // 读寄存器
      {
        vModbus_Read_Register(COM);
      }
      else if ((*(COM->pucRec_Buffer + 1)) == Modbus_Function_NUM_Write)  // 写寄存器
      {
        vModbus_Write_Register(COM);
      }
    }
  }
}

/*
* @function     : vModbus_Read_Register
* @param        : UARTx -> 串口指针
* @retval       : None
* @brief        : 读寄存器
*/
static void vModbus_Read_Register(USART1_t* UART)
{
  USART1_t* const COM = UART;
  
  // 回应数据
  *(COM->pucSend_Buffer + 0) = Modbus.Modbus_Addr;      // 地址码
  *(COM->pucSend_Buffer + 1) = Modbus_Function_NUM_Read;      // 功能码
  *(COM->pucSend_Buffer + 2) = 8;      // 数据长度
  // VIN电压值
  *(COM->pucSend_Buffer + 3) = (uint16_t)(ADC.fVIN_VOltage * 10) / 256;      
  *(COM->pucSend_Buffer + 4) = (uint16_t)(ADC.fVIN_VOltage * 10) % 256;
  // BAT电压值
  *(COM->pucSend_Buffer + 5) = 0;      
  *(COM->pucSend_Buffer + 6) = (uint8_t)ADC.fBAT_Voltage * 10;  
  // 路灯亮度值
  *(COM->pucSend_Buffer + 7) = Pwm.LED_Duty / 256;      
  *(COM->pucSend_Buffer + 8) = Pwm.LED_Duty % 256;  
  // 充电状态
  *(COM->pucSend_Buffer + 9) = 0;      
  *(COM->pucSend_Buffer + 10) = (uint8_t)Power.Charge_Status;  
  // 插入CRC
  CRC_16.CRC = CRC_16.CRC_16_Check(COM->pucSend_Buffer,11);     // 计算CRC
  CRC_16.CRC_H = (uint8_t)(CRC_16.CRC >> 8);
  CRC_16.CRC_L = (uint8_t)CRC_16.CRC;  
  // CRC
  *(COM->pucSend_Buffer + 11) = CRC_16.CRC_L;      
  *(COM->pucSend_Buffer + 12) = CRC_16.CRC_H;    
  // 发送数据
  USART1.vUSART1_SendArray(COM->pucSend_Buffer,13);
}

/*
* @function     : vModbus_Write_Register
* @param        : UARTx -> 串口指针
* @retval       : None
* @brief        : 写寄存器
*/
static void vModbus_Write_Register(USART1_t* UART)
{
  USART1_t* const COM = UART;
  uint8_t i;
  
  for (i = 0; i < 8; i++)
  {
    *(COM->pucSend_Buffer + i) = *(COM->pucRec_Buffer + i);
  }
  // 发送数据
  USART1.vUSART1_SendArray(COM->pucSend_Buffer,8);
  
  // 提取数据 0x4003 = 0x9C43
  if ((*(COM->pucRec_Buffer + 2) == 0x9C) && (*(COM->pucRec_Buffer + 3) == 0x43))
  {
    // 更新占空比
    Pwm.LED_Duty = (PWM_Duty_t)((*(COM->pucRec_Buffer + 4)) * 256 + (*(COM->pucRec_Buffer + 5)));
    CCR2 = Pwm.LED_Duty;        
  }
  
}