stm32f1系列单片机基于HAL库实现Modbus RTU协议的485通信,单片机做从站,串口软件或者维纶屏做主站

本文主要为自己复习485通信的实现使用。

一、RS-485通信概述

**RS-485** 是一种广泛应用于工业自动化、楼宇自动化、数据采集等领域的串行通信标准。它以差分信号传输为基础,具有抗干扰能力强、传输距离远、支持多点通信等特点。

二、RS-485 的特点

1. **差分信号传输**
   - 使用两根信号线(A 和 B)进行差分传输。
   - 差分信号可以有效抵抗电磁干扰,适合长距离通信。

2. **多点通信**
   - 支持多点通信(Multi-Drop),最多可连接 32 个设备(在标准模式下,扩展模式下可达 128 或更多)。
   - 每个设备通过地址区分,实现主从通信或多主机通信。

3. **长距离和高传输速率**
   - 最大传输距离:约 1200 米(低速率时)。
   - 最大传输速率:通常为 10 Mbps(短距离时)。

4. **半双工与全双工**
   - 半双工:使用一对差分信号线(A 和 B),需要切换发送和接收方向。
   - 全双工:使用两对差分信号线(A/B 和 Y/Z),可以同时发送和接收数据。

5. **电气特性**
   - 差分电压范围:±200mV 至 ±6V。
   - 接口芯片:常用芯片有 MAX485、SN75176 等。

三、RS-485 的通信协议

RS-485 本身只定义了物理层的标准,不包括通信协议。因此,开发者需要根据应用需求设计或选择合适的通信协议。以下是常见的通信协议:

1. **Modbus RTU**
   - 基于 RS-485 的最常见协议之一。
   - 数据帧格式:
     ```
     地址 + 功能码 + 数据 + CRC校验
     ```
   - 特点:简单易用,支持主从通信。

2. **自定义协议**
   - 根据项目需求设计简单的通信协议。
   - 示例帧格式:
     ```
     起始标志 + 地址 + 命令 + 数据长度 + 数据 + 校验
     ```

四、RS-485 的软件实现

modbus_rtu.c

相关函数简介:

rs485_usart2_init:

        485通信本质也是串口通信,我这边是通过串口2来实现的485通信(需要开发板支持)。

        串口初始化、GPIO复用配置、串口接收中断及NVIC配置

rs485_send_datas:

        借助串口发送函数实现485发送一字节函数

Modbus_CRC16:

        crc16校验,这个函数直接网上移植的,使用即可

Modbus_Service:

        解析接受到主机发送的数据,对其进行解析,数据的接收是配合接收中断和空闲中断实现的

Modbus_01_Solve:

        对主机功能码1的请求进行回复的函数

#include "main.h"

/*******************************************************
* @function :   rs485_usart2_init
* @param    :   uint32_t baud
* @retval   :   void
* @brief    :   rs485初始化
                RE(Receiver Enable)--active low
                DE(Driver Enable)  --active high
                PD7
********************************************************/
UART_HandleTypeDef huart2;
void rs485_usart2_init(uint32_t baud)
{
    //usart1
    __HAL_RCC_USART2_CLK_ENABLE();
    huart2.Instance        =   USART2;
    huart2.Init.BaudRate   =   baud;
    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;
    HAL_UART_Init(&huart2);
    
    //gpio ---后初始化GPIO,否则第一次会有乱码
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();
    GPIO_InitTypeDef GPIO_Init={0};
    GPIO_Init.Mode  =   GPIO_MODE_AF_PP;
    GPIO_Init.Pin   =   GPIO_PIN_2 ;
    GPIO_Init.Pull  =   GPIO_NOPULL;
    GPIO_Init.Speed =   GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA,&GPIO_Init);
    GPIO_Init.Mode  =   GPIO_MODE_AF_INPUT;
    GPIO_Init.Pin   =   GPIO_PIN_3 ;
    HAL_GPIO_Init(GPIOA,&GPIO_Init);
    GPIO_Init.Mode  =   GPIO_MODE_OUTPUT_PP;
    GPIO_Init.Pin   =   GPIO_PIN_7 ;
    GPIO_Init.Pull  =   GPIO_NOPULL;
    GPIO_Init.Speed =   GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOD,&GPIO_Init);
    REC_ENABLE;//默认接收状态

    __HAL_UART_ENABLE_IT(&huart2,UART_IT_RXNE);
    __HAL_UART_ENABLE_IT(&huart2,UART_IT_IDLE);
    HAL_NVIC_SetPriority(USART2_IRQn,2,0);
    HAL_NVIC_EnableIRQ(USART2_IRQn);
}


/*******************************************************
* @function :   rs485_send_datas
* @param    :   uint8_t *data,uint16_t len
* @retval   :   void
* @brief    :   485发送一字节数据
********************************************************/
void rs485_send_datas(uint8_t *data,uint16_t len)
{
    if(HAL_UART_Transmit(&huart2,(const uint8_t *)data,len,50)!=HAL_OK)
    {
        REC_ENABLE;
        return ;
    };
}



/*******************************************************
* @function :   Modbus_CRC16
* @param    :   uint8_t *data, uint16_t length
* @retval   :   void
* @brief    :   CRC16校验
********************************************************/
uint16_t Modbus_CRC16(uint8_t *data, uint16_t length) 
{

    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < length; i++) 
    {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) 
        {
            if (crc & 0x0001) 
            {
                crc >>= 1;
                crc ^= 0xA001;
            } 
            else 
            {
                crc >>= 1;
            }
        }
    }
    return crc;
}



/*******************************************************
* @function :   Modbus_Service
* @param    :   void
* @retval   :   void
* @brief    :   解析主机请求
********************************************************/
uint16_t startRegAddr;
uint16_t RegNum;
uint16_t calCRC;
void Modbus_Service(void)
{
	uint16_t recCRC;
	if(g_usart2_rs485.flag==1)//接收完成标志位
	{
        SEND_ENABLE;
//        printf("\r\nmodbus_rec:");
//        for(int i=0;i>8);// 先发送高字节--在发送低字节
			modbus_tx_buff[4+i*2]=(Modbus_register[startRegAddr+i]); 
		}
		calCRC=Modbus_CRC16(modbus_tx_buff,RegNum*2+3);
		modbus_tx_buff[RegNum*2+3]=(calCRC)&0xFF;
		modbus_tx_buff[RegNum*2+4]=(calCRC>>8)&0xFF;
		rs485_send_datas(modbus_tx_buff,RegNum*2+5);
	}
	else//寄存器地址+数量超出范围
	{
		modbus_tx_buff[0]=g_usart2_rs485.buff[0];
		modbus_tx_buff[1]=g_usart2_rs485.buff[1];
		modbus_tx_buff[2]=0x02; //异常码
		rs485_send_datas(modbus_tx_buff,3);
	}
}

/*******************************************************
* @function :   Modbus_03_Solve
* @param    :   void
* @retval   :   void
* @brief    :   功能码3
********************************************************/
void Modbus_03_Solve(void)
{
	uint8_t i;	
    static uint8_t num=0;
    
    if(num==12)
    {
        num=0;
    }
    num++;
    Modbus_register[0x00] = 0x01<>8);// 先发送高字节--在发送低字节
			modbus_tx_buff[4+i*2]=(Modbus_register[startRegAddr+i]); 
		}
		calCRC=Modbus_CRC16(modbus_tx_buff,RegNum*2+3);
		modbus_tx_buff[RegNum*2+3]=(calCRC)&0xFF;
		modbus_tx_buff[RegNum*2+4]=(calCRC>>8)&0xFF;
		rs485_send_datas(modbus_tx_buff,RegNum*2+5);
	}
	else//寄存器地址+数量超出范围
	{
		modbus_tx_buff[0]=g_usart2_rs485.buff[0];
		modbus_tx_buff[1]=g_usart2_rs485.buff[1];
		modbus_tx_buff[2]=0x02; //异常码
		rs485_send_datas(modbus_tx_buff,3);
	}
}


/*******************************************************
* @function :   Modbus_03_Solve
* @param    :   void
* @retval   :   void
* @brief    :   功能码3
********************************************************/
void Modbus_16_Solve(void)
{
	uint8_t i;
	RegNum= (((uint16_t)g_usart2_rs485.buff[4])<<8)|((g_usart2_rs485.buff[5]));//获取寄存器数量
	if((startRegAddr+RegNum)>8)&0xFF;
		rs485_send_datas(modbus_tx_buff,8);
	}
	else//寄存器地址+数量超出范围
	{
		modbus_tx_buff[0]=g_usart2_rs485.buff[0];
		modbus_tx_buff[1]=g_usart2_rs485.buff[1]|0x80;
		modbus_tx_buff[2]=0x02; //异常码
		rs485_send_datas(modbus_tx_buff,3);
	}
}



modbus_rtu.h

#ifndef _MODBUS_RTU_H
#define _MODBUS_RTU_H

/*include*/
#include "main.h"

/*define*/
#define SEND_ENABLE HAL_GPIO_WritePin(GPIOD,GPIO_PIN_7,GPIO_PIN_SET)
#define REC_ENABLE  HAL_GPIO_WritePin(GPIOD,GPIO_PIN_7,GPIO_PIN_RESET)
#define Modbus_Addr  1
/*function*/
void rs485_usart2_init(uint32_t baud);
void Modbus_Service(void);
void Modbus_01_Solve(void);
void Modbus_03_Solve(void);
void Modbus_16_Solve(void);

/*extern*/
extern UART_HandleTypeDef huart2;

#endif


nvic.c
  

/*******************************************************
* @function :   USART2_IRQHandler
* @param    :   void
* @retval   :   void
* @brief    :   USART2--MODBUS RS485中断服务函数
********************************************************/
void USART2_IRQHandler(void)
{
    //receive IT
    if(__HAL_UART_GET_FLAG(&huart2,UART_FLAG_RXNE))
    {
        __HAL_UART_CLEAR_FLAG(&huart2,UART_FLAG_RXNE);
        g_usart2_rs485.buff[g_usart2_rs485.len++]=USART2->DR;
                
    }
    //IDLE IT
    if(__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE))
    {
        USART2->SR;
        USART2->DR; 
        g_usart2_rs485.buff[g_usart2_rs485.len]=0;
//        g_usart2_rs485.len=0;
        g_usart2_rs485.flag=1;
//        printf("rec:%s\r\n",g_usart2_rs485.buff);
    }
}

相关全局变量

任意.h文件,建议和下面.C文件保持一致
typedef struct usart
{
    uint8_t flag;
    uint8_t buff[1024];
    uint16_t len;
}g_strcut_usart;
#define MODBUS_REGISTER_NUM 0XFF


任意.c文件,可以在485初始配置文件添加
g_strcut_usart g_usart2_rs485;  //串口2MODBUS RS485 结构体信息
uint8_t modbus_tx_buff[1024];   //modbus rtu发送
uint16_t Modbus_register[MODBUS_REGISTER_NUM]; //MODBUS寄存器

五、成果展示:

寄存器对应位:

stm32f1系列单片机基于HAL库实现Modbus RTU协议的485通信,单片机做从站,串口软件或者维纶屏做主站_第1张图片

维纶屏配置:

stm32f1系列单片机基于HAL库实现Modbus RTU协议的485通信,单片机做从站,串口软件或者维纶屏做主站_第2张图片

stm32f1系列单片机基于HAL库实现Modbus RTU协议的485通信,单片机做从站,串口软件或者维纶屏做主站_第3张图片

也可以使用modbus poll 软件,或者串口软件sscom.

注:本文只是初步实现了通信和简单的设备数据读取。

你可能感兴趣的:(单片机,stm32,Modbus,rtu)