【STM32】RTT-Studio中HAL库开发教程一:RS485串行通信

一、串口介绍

1、串口是一个泛称,常用的串口设备:UART、TTL、RS232、RS485和RS422:

  • UART(通用异步收发传输器)设备:是一种集成电路,负责处理串行通信协议中的时序生成、数据编码解码等功能,是嵌入式系统中常见的用于实现串行通信的硬件模块。UART本身并不规定具体的电气特性,而是产生遵循串行通信时序的信号(如启动位、数据位、校验位和停止位)。
  • TTL(晶体管-晶体管逻辑)电平:是一种逻辑电平标准,通常在集成电路内部或者集成电路之间近距离通信时使用,它的高低电平相对较低,通常为3.3V或5V表示逻辑1,0V表示逻辑0。
  • RS232设备:是一种早期广泛应用于计算机和终端设备之间的串行通信接口标准,它规定了详细的电气特性,如逻辑1(负电压,通常为-3V ~ -15V)和逻辑0(正电压,通常为+3V ~ +15V)。尽管逻辑电平与TTL电平不同,但可以通过电平转换器将UART产生的TTL电平转换为RS232电平进行远距离传输。
  • RS422设备:是一种全双工、差分传输的串行通信标准,它具有较高的抗干扰能力和较长的传输距离,支持多点传输,每个信号都有明确的方向(发送和接收分离),常用于工业控制领域。
  • RS485:也是一种差分传输的串行通信标准,与RS422类似,但增加了多点通信的能力,支持多个设备通过同一条线路进行通信,但同一时间内只能有一个设备发送数据。

综上所述,UART是生成串行通信时序的硬件模块,而TTL、RS232、RS422和RS485则分别代表了不同的电气接口标准和逻辑电平标准。在实际应用中,UART产生的TTL电平信号通常需要通过电平转换器转化为RS232、RS422或RS485标准的信号,以便在不同的物理环境中进行可靠的串行通信。

2、USB转RS485串口线定义:
(1)USB口引脚定义
在这里插入图片描述
(2)RS422/RS485的DB9口引脚定义
在这里插入图片描述
(3)RS422/RS485转接板引脚定义
在这里插入图片描述
3、USB转RS422/485通讯接线示意图
(1)RS422到RS422通讯
在这里插入图片描述
(2)RS422点对多点/四线全双工通讯
在这里插入图片描述
(3)RS485到RS485通讯
在这里插入图片描述
(4)RS485点对多点半双工通讯
在这里插入图片描述

二、串口协议和Modbus协议

1、串口基础协议(Serial Port Basic Protocol)通常指的是用于串行通信的基本规则,它定义了数据在串行链路上如何进行传输,包括但不限于以下几个关键要素:

  • 通信时序:起始位、数据位(一般8位)、校验位(奇偶)、停止位(一般1位)
  • 波特率:每秒传输的位数,常见的有9600、19200、115200等
  • 通信方向:可以是全双工(同时收发)、半双工(同一时间只能发送或接收)、单工(只能发送或只能接收)

2、Modbus协议则建立在串口基础协议之上,是一种应用层协议,用于在不同设备间进行数据交换,特别是工业控制系统中的现场设备如PLC、智能仪表、传感器和执行器等。Modbus协议的特点包括:

  • 主从结构:一个主设备(主控制器)向多个从设备发起请求,从设备响应请求。
  • 功能码:Modbus协议定义了一系列功能码,每个功能码对应一种操作,如读取线圈状态、寄存器值,写入线圈状态、寄存器值等。
  • 数据包:Modbus协议中的数据传输:设备地址、功能码、数据区(数据长度根据功能码定义)和校验码(如RTU模式CRC校验)。
  • 数据传输模式:Modbus支持ASCII、RTU(Remote Terminal Unit)和TCP/IP三种传输模式。其中,ASCII模式适合于低速通信和易于调试,RTU模式适用于高速通信且效率较高,TCP/IP模式则支持在网络环境中传输Modbus协议。

3、Modbus协议的数据帧结构:

  • Modbus RTU模式:设备地址、功能码、数据区(长度根据功能码决定)、CRC校验码。
  • Modbus ASCII模式:在RTU模式基础上增加了起始字符、结束字符和LRC校验码。
  • Modbus TCP/IP模式:以TCP报文的形式封装Modbus 数据,包含设备地址(在TCP连接中隐含,不再在报文中携带)、功能码、数据区、CRC校验码(TCP层不需要,Modbus -TCP层可选)。

在实际应用中,串口基础协议提供的是物理层和链路层的通信基础,而Modbus协议则是更高层次的应用层协议,它规定了如何在串口通信的基础上构造有意义的消息结构,从而实现设备间复杂的控制和数据交换。Modbus协议在实际应用中,主设备可以向从设备发出读写请求,从设备在接收到请求后根据功能码执行相应操作,并将结果返回给主设备。由于其简洁、通用和开放的特性,MODBUS在工业自动化、楼宇自动化、能源管理系统等多个领域得到广泛应用。

三、RS485串口介绍

特点:

  • RS485是串行通信标准,使用差分信号传输,抗干扰能力强,常用于工控领域;
  • RS485具有强的组网功能,在串口基础协议上还制定了Modbus协议;
  • 接口电平低,不易损坏芯片,传输速率高,传输距离远,支持多节点传输。
  • 通信方式:半双工或全双工通信
  • 一对差分信号线(A和B),电平标准:1 - A相对于B为正 0 - B相对于A为正,典型差分电压范围:±200mv ~ ±2v。
  • 逻辑 1:两根信号线(A+和B-)的电压差在 +2V~+6V 之间。逻辑 0:两根信号线(A+和B-)的电压差在 -2V~-6V 之间。
  • 通信距离:长达1200米,通信速率可达10Nbps,但常用为9600bps ~ 115200bps。
    在这里插入图片描述
    阻抗匹配
  • 在RS485_A和RS485_B的两端(或靠近设备端)通常会并联一个120欧姆左右的终端电阻,目的是吸收信号反射,确保RS485总线的稳定性,抑制噪声,增强信号质量。

常用RS485通信电路
在这里插入图片描述

  • RO (接收器输出端):当RS485总线上的差分信号满足一定的阈值条件时,RO会根据接收到的差分信号输出对应的逻辑电平。

如果 A - B 的电压差大于等于 +0.2V,表明总线上接收到了逻辑“1”,因此RO输出高电平(逻辑“1”)。
如果 A - B 的电压差小于等于 - 0.2V,表明总线上接收到了逻辑“0”,因此RO输出低电平(逻辑“0”)。

  • RE (接收器使能端):低电平有效,当RE为低电平时,允许接收器工作,可以正常接收总线上的数据;反之,当RE为高电平时,接收器被禁止接收数据。
  • DE (驱动器使能端):高电平有效,当DE为高电平时,允许驱动器工作,可以向总线发送数据;反之,当DE为低电平时,驱动器停止发送数据,进入高阻态,不影响总线上的其他设备通信。
  • DI (驱动器输入端):与微控制器的TTL/CMOS电平输出相连,用于决定驱动器要发送的数据。

当DI为低电平时,驱动器会让A线变低,B线变高,从而在总线上形成逻辑“0”的差分信号
当DI为高电平时,驱动器会让A线变高,B线变低,从而在总线上形成逻辑“1”的差分信号

  • A 和 B:对称的差分信号线,A和B通常通过双绞线连接,共同构成RS485通信的物理层基础

RS485通信波形图

  • 发送端:
    如果发送1:即A是高电平,B为低电平
    如果发送0:即A是低电平,B为高电平
  • 接收端:
    如果A - B ≥ +0.2V,R0则为高电平
    如果A - B ≤ +0.2V,R0则为低电平
    在这里插入图片描述

四、RS485串口通信

1、先对串口进行初始化配置,需要注意的是:接收数据采用中断的等方式进行接收,也可以采用DMA的方式进行接收数据,并且在配置的时候需要对控制应交进行配置,默认为接收模式。使用其他串口的时候需要对引脚进行指定,使用gpio_initure.Alternate = GPIO_AF7_USART3; 这种来进行指定。

/**
 * @brief RS485初始化函数
 * @note  函数主要是初始化串口
 * @param baudrate: 波特率, 根据自己需要设置波特率值
 */
void rs485_init(uint32_t baudrate)
{
    /* IO 及 时钟配置 */
    RS485_RE_GPIO_CLK_ENABLE(); /* 使能 RS485_RE DE 脚时钟 */
    RS485_TX_GPIO_CLK_ENABLE(); /* 使能 串口TX脚 时钟 */
    RS485_RX_GPIO_CLK_ENABLE(); /* 使能 串口RX脚 时钟 */
    RS485_UX_CLK_ENABLE();      /* 使能 串口 时钟 */

    GPIO_InitTypeDef gpio_initure;
    gpio_initure.Pin = RS485_TX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_PP;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    gpio_initure.Alternate = GPIO_AF7_USART3;
    HAL_GPIO_Init(RS485_TX_GPIO_PORT, &gpio_initure); /* 串口TX 脚 模式设置 */

    gpio_initure.Pin = RS485_RX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_PP;
    HAL_GPIO_Init(RS485_RX_GPIO_PORT, &gpio_initure); /* 串口RX 脚 必须设置成输入模式 */

    gpio_initure.Pin = RS485_RE_GPIO_PIN | RS485_DE_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &gpio_initure);              /* RS485_RE 脚 模式设置 */

    /* USART 初始化设置 */
    g_rs458_handler.Instance = RS485_UX;                  /* 选择485对应的串口 */
    g_rs458_handler.Init.BaudRate = baudrate;             /* 波特率 */
    g_rs458_handler.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
    g_rs458_handler.Init.StopBits = UART_STOPBITS_1;      /* 一个停止位 */
    g_rs458_handler.Init.Parity = UART_PARITY_NONE;       /* 无奇偶校验位 */
    g_rs458_handler.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
    g_rs458_handler.Init.Mode = UART_MODE_TX_RX;          /* 收发模式 */
    HAL_UART_Init(&g_rs458_handler);                      /* HAL_UART_Init()会使能UART3 */

#if RS485_EN_RX /* 如果使能了接收 */
    /* 使能接收中断 */
    __HAL_UART_ENABLE_IT(&g_rs458_handler, UART_IT_RXNE); /* 开启接收中断 */
    HAL_NVIC_EnableIRQ(RS485_UX_IRQn);                    /* 使能USART3中断 */
    HAL_NVIC_SetPriority(RS485_UX_IRQn, 0, 0);            /* 抢占优先级3,子优先级3 */
#endif

    RS485_RE(0); /* 默认为接收模式 */
}

2、编写RS485的发送函数

/**
 * @brief RS485发送len个字节
 * @param buf : 发送区首地址
 * @param len : 发送的字节数(为了和本代码的接收匹配,这里建议不要超过 RS485_REC_LEN 个字节)
 */
void rs485_send_data(uint8_t *buf, uint8_t len)
{
    RS485_RE(1);                                         /* 进入发送模式 */
    HAL_UART_Transmit(&g_rs458_handler, buf, len, 1000); /* 串口3发送数据 */
    g_RS485_rx_cnt = 0;
    RS485_RE(0); /* 进入接收模式 */
}

3、编写接收中断函数

/**
 * @brief RS485查询接收到的数据
 * @param buf     : 接收缓冲区首地址
 * @param len     : 接收到的数据长度 0 - 表示没有接收到任何数据,其他 - 表示接收到的数据长度
 */
void rs485_receive_data(uint8_t *buf, uint8_t *len)
{
    uint8_t rxlen = g_RS485_rx_cnt;
    uint8_t i = 0;
    *len = 0;     /* 默认为0 */
    HAL_Delay(10); /* 等待10ms,连续超过10ms没有接收到一个数据,则认为接收结束 */

    if (rxlen == g_RS485_rx_cnt && rxlen) /* 接收到了数据,且接收完成了 */
    {
        for (i = 0; i < rxlen; i++)
        {
            buf[i] = g_RS485_rx_buf[i];
        }

        *len = g_RS485_rx_cnt; /* 记录本次数据长度 */
        memset(g_RS485_rx_buf, 0, RS485_REC_LEN);
        g_RS485_rx_cnt = 0;    /* 清零 */
    }
}

/**
 * @brief RS485接收中断函数
 * @param huart 串口句柄
 */
void RS485_UX_IRQHandler(UART_HandleTypeDef *huart)
{
    uint8_t res;

    if ((__HAL_UART_GET_FLAG(&g_rs458_handler, UART_FLAG_RXNE) != RESET)) /* 接收到数据 */
    {
        HAL_UART_Receive(&g_rs458_handler, &res, 1, 1000);

        if (g_RS485_rx_cnt < RS485_REC_LEN)         /* 缓冲区未满 */
        {
            g_RS485_rx_buf[g_RS485_rx_cnt] = res;   /* 记录接收到的值 */
            g_RS485_rx_cnt++;                       /* 接收数据增加1 */
        }
    }
}

4、总体代码

  • rs485.c
#include "rs485.h"

UART_HandleTypeDef g_rs458_handler;     /* RS485控制句柄(串口) */

uint8_t g_RS485_rx_buf[RS485_REC_LEN]; /* 接收缓冲, 最大 RS485_REC_LEN 个字节. */
uint8_t g_RS485_rx_cnt = 0;            /* 接收到的数据长度 */

/**
 * @brief RS485初始化函数
 * @note  函数主要是初始化串口
 * @param baudrate: 波特率, 根据自己需要设置波特率值
 */
void rs485_init(uint32_t baudrate)
{
    /* IO 及 时钟配置 */
    RS485_RE_GPIO_CLK_ENABLE(); /* 使能 RS485_RE DE 脚时钟 */
    RS485_TX_GPIO_CLK_ENABLE(); /* 使能 串口TX脚 时钟 */
    RS485_RX_GPIO_CLK_ENABLE(); /* 使能 串口RX脚 时钟 */
    RS485_UX_CLK_ENABLE();      /* 使能 串口 时钟 */

    GPIO_InitTypeDef gpio_initure;
    gpio_initure.Pin = RS485_TX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_PP;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    gpio_initure.Alternate = GPIO_AF7_USART3;
    HAL_GPIO_Init(RS485_TX_GPIO_PORT, &gpio_initure); /* 串口TX 脚 模式设置 */

    gpio_initure.Pin = RS485_RX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_PP;
    HAL_GPIO_Init(RS485_RX_GPIO_PORT, &gpio_initure); /* 串口RX 脚 必须设置成输入模式 */

    gpio_initure.Pin = RS485_RE_GPIO_PIN | RS485_DE_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &gpio_initure);              /* RS485_RE 脚 模式设置 */

    /* USART 初始化设置 */
    g_rs458_handler.Instance = RS485_UX;                  /* 选择485对应的串口 */
    g_rs458_handler.Init.BaudRate = baudrate;             /* 波特率 */
    g_rs458_handler.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
    g_rs458_handler.Init.StopBits = UART_STOPBITS_1;      /* 一个停止位 */
    g_rs458_handler.Init.Parity = UART_PARITY_NONE;       /* 无奇偶校验位 */
    g_rs458_handler.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
    g_rs458_handler.Init.Mode = UART_MODE_TX_RX;          /* 收发模式 */
    HAL_UART_Init(&g_rs458_handler);                      /* HAL_UART_Init()会使能UART3 */

#if RS485_EN_RX /* 如果使能了接收 */
    /* 使能接收中断 */
    __HAL_UART_ENABLE_IT(&g_rs458_handler, UART_IT_RXNE); /* 开启接收中断 */
    HAL_NVIC_EnableIRQ(RS485_UX_IRQn);                    /* 使能USART3中断 */
    HAL_NVIC_SetPriority(RS485_UX_IRQn, 0, 0);            /* 抢占优先级3,子优先级3 */
#endif

    RS485_RE(0); /* 默认为接收模式 */
}

/**
 * @brief RS485发送len个字节
 * @param buf : 发送区首地址
 * @param len : 发送的字节数(为了和本代码的接收匹配,这里建议不要超过 RS485_REC_LEN 个字节)
 */
void rs485_send_data(uint8_t *buf, uint8_t len)
{
    RS485_RE(1);                                         /* 进入发送模式 */
    HAL_UART_Transmit(&g_rs458_handler, buf, len, 1000); /* 串口3发送数据 */
    g_RS485_rx_cnt = 0;
    RS485_RE(0); /* 进入接收模式 */
}

/**
 * @brief RS485查询接收到的数据
 * @param buf     : 接收缓冲区首地址
 * @param len     : 接收到的数据长度 0 - 表示没有接收到任何数据,其他 - 表示接收到的数据长度
 */
void rs485_receive_data(uint8_t *buf, uint8_t *len)
{
    uint8_t rxlen = g_RS485_rx_cnt;
    uint8_t i = 0;
    *len = 0;     /* 默认为0 */
    HAL_Delay(10); /* 等待10ms,连续超过10ms没有接收到一个数据,则认为接收结束 */

    if (rxlen == g_RS485_rx_cnt && rxlen) /* 接收到了数据,且接收完成了 */
    {
        for (i = 0; i < rxlen; i++)
        {
            buf[i] = g_RS485_rx_buf[i];
        }

        *len = g_RS485_rx_cnt; /* 记录本次数据长度 */
        memset(g_RS485_rx_buf, 0, RS485_REC_LEN);
        g_RS485_rx_cnt = 0;    /* 清零 */
    }
}

/**
 * @brief RS485接收中断函数
 * @param huart 串口句柄
 */
void RS485_UX_IRQHandler(UART_HandleTypeDef *huart)
{
    uint8_t res;

    if ((__HAL_UART_GET_FLAG(&g_rs458_handler, UART_FLAG_RXNE) != RESET)) /* 接收到数据 */
    {
        HAL_UART_Receive(&g_rs458_handler, &res, 1, 1000);

        if (g_RS485_rx_cnt < RS485_REC_LEN)         /* 缓冲区未满 */
        {
            g_RS485_rx_buf[g_RS485_rx_cnt] = res;   /* 记录接收到的值 */
            g_RS485_rx_cnt++;                       /* 接收数据增加1 */
        }
    }
}
  • rs485.h
#ifndef APPLICATIONS_RS485_H_
#define APPLICATIONS_RS485_H_

#include <drv_common.h>
#include <string.h>

/******************************************************************************************/
/* RS485 引脚 和 串口 定义
 */
#define RS485_RE_GPIO_PORT                  GPIOB
#define RS485_RE_GPIO_PIN                   GPIO_PIN_15
#define RS485_DE_GPIO_PORT                  GPIOB
#define RS485_DE_GPIO_PIN                   GPIO_PIN_14
#define RS485_RE_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define RS485_TX_GPIO_PORT                  GPIOD
#define RS485_TX_GPIO_PIN                   GPIO_PIN_8
#define RS485_TX_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)   /* PD口时钟使能 */

#define RS485_RX_GPIO_PORT                  GPIOD
#define RS485_RX_GPIO_PIN                   GPIO_PIN_9
#define RS485_RX_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)   /* PD口时钟使能 */

#define RS485_UX                            USART3
#define RS485_UX_IRQn                       USART3_IRQn
#define RS485_UX_IRQHandler                 USART3_IRQHandler
#define RS485_UX_CLK_ENABLE()               do{ __HAL_RCC_USART3_CLK_ENABLE(); }while(0)  /* USART3 时钟使能 */

/******************************************************************************************/

/* 控制RS485_RE脚, 控制RS485发送/接收状态
 * RS485_RE = 0, 进入接收模式
 * RS485_RE = 1, 进入发送模式
 */
#define RS485_RE(x)   do{ x ? \
                          HAL_GPIO_WritePin(GPIOB, RS485_RE_GPIO_PIN | RS485_DE_GPIO_PIN, GPIO_PIN_SET) :  \
                          HAL_GPIO_WritePin(GPIOB, RS485_RE_GPIO_PIN | RS485_DE_GPIO_PIN, GPIO_PIN_RESET); \
                      }while(0)


#define RS485_REC_LEN               64          	/* 定义最大接收字节数 64 */
#define RS485_EN_RX                 1           	/* 使能(1)/禁止(0)RS485接收 */


extern uint8_t g_RS485_rx_buf[RS485_REC_LEN];  	 	/* 接收缓冲,最大RS485_REC_LEN个字节 */
extern uint8_t g_RS485_rx_cnt;                  	/* 接收数据长度 */


void rs485_init( uint32_t baudrate);  				/* RS485初始化 */
void rs485_send_data(uint8_t *buf, uint8_t len);    /* RS485发送数据 */
void rs485_receive_data(uint8_t *buf, uint8_t *len);/* RS485接收数据 */


#endif /* APPLICATIONS_RS485_H_ */
  • main.c测试
#include <rtthread.h>
#include <drv_common.h>
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>
#include <string.h>
#include "rs485.h"

int main(void)
{
    uint8_t rs485buf[64];
    uint8_t key;

    rs485_init(9600);              	/* 初始化RS485 */

    for (int i = 0; i < 5; i++)
    {
        rs485buf[i] = i+5;      	/* 填充发送缓冲区 */
    }
    rs485_send_data(rs485buf, 5);   /* 发送5个字节 */

    while (1)
    {
        rs485_receive_data(rs485buf, &key);
        if (key)
        {
            rs485_send_data(rs485buf, key);   /* 发送5个字节 */
        }

        rt_thread_mdelay(50);
    }

    return RT_EOK;
}

五、RS485通信现象

1、通过串口进行测试,选择通信的串口、波特率为9600,选择CRC校验数据。
2、通过发送读取电源状态、输出电压、输出电流、温度的Modbus数据进行通信测试,可以看到发送的数据会被返回,当进行Modbus数据通信时,发送通信指令,会返回响应的数据,也是包含设备地址、功能码、数据、和CRC校验。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值