【正点原子STM32】RS485串行通信标准(串口基础协议 和 MODBUS协议、总线连接、通信电路、通信波形图、RS485相关HAL库驱动、RS485配置步骤、)

一、RS485介绍
二、RS485相关HAL库驱动介绍
三、RS485配置步骤
四、编程实战
五、总结


串口、 UART、TTL、RS232、RS422、RS485关系

串口、UART、TTL、RS232、RS422和RS485之间的关系可以如此理解:

  • 串口:是一个广义术语,通常指的是采用串行通信协议的接口,它可以包括多种具体的物理接口标准和逻辑电平标准。

  • 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标准的信号,以便在不同的物理环境中进行可靠的串行通信。

串口基础协议 和 MODBUS协议

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

  1. 通信时序:启动位、数据位(一般为5到8位)、奇偶校验位(可选)和停止位(1到2位)。
  2. 波特率:每秒传输的位数,常见的有9600、19200、38400、115200等。
  3. 通信方向:可以是全双工(同时发送和接收数据)、半双工(同一时间只能进行发送或接收)或单工(只能发送或只能接收)。

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

  1. 主从结构:网络中有一个主设备(主控制器)向多个从设备发起请求,从设备响应请求。
  2. 功能码:MODBUS协议定义了一系列功能码,每个功能码对应一种操作,如读取线圈状态、寄存器值,写入线圈状态、寄存器值等。
  3. 数据组织:MODBUS协议中的数据传输包括设备地址、功能码、数据区(数据长度根据功能码定义)和校验码(如RTU模式下的CRC校验)。

在实际应用中,串口基础协议提供的是物理层和链路层的通信基础,而MODBUS协议则是更高层次的应用层协议,它规定了如何在串口通信的基础上构造有意义的消息结构,从而实现设备间复杂的控制和数据交换。

MODBUS协议

MODBUS协议是一种广泛应用于工业控制领域的串行通信协议,最初由Modicon公司于1979年发布,现已成为一种通用的工业标准协议。MODBUS允许不同厂商的设备通过串行线路或以太网进行通信,从而实现了不同设备之间的互操作性。

MODBUS协议主要特点:

  1. 主从架构:网络中只有一个主设备(如PLC或HMI),可以向多个从设备(如传感器、执行器、其他控制器等)发送请求并接收响应。
  2. 功能码:MODBUS协议定义了一系列功能码,用于执行读写操作,如读取线圈状态、寄存器值、输入状态,写入线圈状态、寄存器值等。
  3. 数据传输模式:MODBUS支持ASCII、RTU(Remote Terminal Unit)和TCP/IP三种传输模式。其中,ASCII模式适合于低速通信和易于调试,RTU模式适用于高速通信且效率较高,TCP/IP模式则支持在网络环境中传输MODBUS协议。

MODBUS协议的数据帧结构:

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

MODBUS协议在实际应用中,主设备可以向从设备发出读写请求,从设备在接收到请求后根据功能码执行相应操作,并将结果返回给主设备。由于其简洁、通用和开放的特性,MODBUS在工业自动化、楼宇自动化、能源管理系统等多个领域得到广泛应用。


一、RS485介绍

RS485是一种串行通信标准,它利用差分信号对(通常是一对非绝缘的导线A和B)来传输数据,由于其差分信号的性质,使得它具有很强的抗共模干扰能力,特别适用于工业控制环境,能够确保在长距离、复杂电磁环境下稳定通信。

RS485接口允许构建大型分布式系统,支持多达32个节点(在某些条件下甚至更多)挂接在同一总线上,并且每个节点既可以作为主设备也可以作为从设备,非常适合构建多点对多点的网络架构。

串口基础协议是指最底层的数据传输规则,包括但不限于数据位的定义(例如8位或9位数据位)、波特率设置、停止位数量以及奇偶校验等参数,这些参数共同决定了数据的基本包格式。

而MODBUS协议是在串口基础协议之上的应用层协议,它定义了更高级别的数据打包、寻址、错误检测以及数据交换的具体过程。MODBUS协议利用串口基础协议提供的传输机制,将数据按照特定的帧格式进行组织,从而实现在多个设备间的报文交换和设备控制。MODBUS协议因其开放性和通用性,在工业自动化领域得到了广泛应用。

总结RS485接口的关键特性如下:

  • 通信方式:半双工或全双工(取决于具体应用配置)
  • 信号线:一对差分信号线(A和B或+和-)
  • 电平标准:逻辑"1"时,A相对于B为正,逻辑"0"时相反,典型差分电压范围为±200mV至±2V
  • 拓扑结构:总线型或星型(通过有源或无源转换器)
  • 通信距离:长达1200米(取决于布线质量和波特率)
  • 通讯速率:最高可达10Mbps,但常用速率通常在9600bps至115200bps之间
  • 抗干扰能力:强,因为采用差分信号传输
  • 组网功能:强大,可支持多节点网络
  • 接口安全性:接口电平低,相对其他标准(如RS232)更不容易损坏芯片

通过上述特性,RS485不仅适合长距离、高速率传输,而且具备良好的噪声抑制能力和大规模网络部署能力。

在RS485总线连接中,组件之间的连接和作用如下:

  1. CPU:中央处理器,负责控制整个系统的运行和数据处理。

  2. UART控制器:集成在CPU内部或外部的通用异步收发传输器,用于生成和解析串行通信数据流。

  3. TXD/RXD连接

    • TXD(Transmit Data):UART控制器的发送数据线,通常连接到485收发器的接收端(如DE/RE引脚禁能时的接收端或RO引脚)。
    • RXD(Receive Data):UART控制器的接收数据线,通常连接到485收发器的发送端(如DI引脚)。
  4. 485收发器

    • 如SP3485、TP8485E、MAX485等,这类芯片用于实现TTL电平与RS485差分信号电平之间的转换,并具备收发控制功能。
    • DE(Driver Enable)/RE(Receiver Enable):控制485收发器工作在发送或接收模式,防止在同一时刻既发送又接收导致冲突。
    • DI(Data Input):接收来自UART的TTL电平信号,并转换为RS485差分信号。
    • RO(Receiver Output):将接收到的RS485差分信号转换为TTL电平,发送给UART。
  5. 485_A/485_B

    • 这是RS485总线的差分信号线,485_A和485_B两根线通过双绞线连接到所有的RS485设备,确保信号质量良好。
  6. 匹配电阻

    • 在485_A和485_B的两端(或靠近设备端)通常会并联一个120欧姆左右的终端电阻,目的是吸收信号反射,确保RS485总线的稳定性,抑制噪声,增强信号质量。

总之,通过上述部件的连接和协同工作,CPU得以通过UART控制器与RS485总线上的其它设备进行可靠的数据交换。

RS485通信电路中各引脚的功能以及信号传输规则如下:

  • RO (Receiver Output) :

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

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

    RE是接收器使能端,低电平有效。当RE为低电平时,允许接收器工作,可以正常接收总线上的数据;反之,当RE为高电平时,接收器被禁止接收数据。

  • DE (Driver Enable) :

    DE是驱动器使能端,高电平有效。当DE为高电平时,允许驱动器工作,可以向总线发送数据;反之,当DE为低电平时,驱动器停止发送数据,进入高阻态,不影响总线上的其他设备通信。

  • DI (Driver Input) :

    DI是驱动器输入端,与微控制器的TTL/CMOS电平输出相连,用于决定驱动器要发送的数据:

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

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

关于R19和R22这两个偏置电阻的作用:

在某些设计中,为了避免总线在没有数据传输时处于不确定状态,会在A线和B线之间设置偏置电阻(如R19和R22),使得在总线空闲时,A线与B线之间有一个固定的正向偏移电压(大于0.2V)。这样做的目的是确保即使在没有明确数据信号的情况下,也不会因为噪声或其他原因误判为有效的逻辑信号,增强了通信的可靠性。在实际应用中,这些偏置电阻的选择和是否需要取决于具体的硬件设计需求和所使用的RS485收发器芯片的特性。

485通信波形图示例:

在485通信中,发送端和接收端的信号波形如下:

发送端波形图示:

  • 发送逻辑1时

    • A线的波形显示为高电平(接近电源电压)。
    • B线的波形显示为低电平(接近接地电平)。
  • 发送逻辑0时

    • A线的波形显示为低电平。
    • B线的波形显示为高电平。

接收端波形图示:

  • 判断逻辑1时

    • A线的电压高于B线电压至少0.2V以上,例如A线为3.5V,B线为1V,则(A-B)=2.5V≥+0.2V。
    • 在这种情况下,接收器输出RO会被置为高电平,表示接收到的是逻辑"1"。
  • 判断逻辑0时

    • B线的电压高于A线电压至少0.2V以上,例如B线为3.5V,A线为1V,则(B-A)=-2.5V≤-0.2V。
    • 在这种情况下,接收器输出RO会被置为低电平,表示接收到的是逻辑"0"。

485通信采用差分信号传输,这种设计有效地提高了抗干扰能力,允许多个设备共享相同的通信总线,并且能够在长距离和恶劣环境下保持稳定的通信。在实际的波形图中,可以看到A线和B线的波形互补,并且根据他们的电压差来确定传输的逻辑值。

二、RS485相关HAL库驱动介绍

实际上,对于RS485通信,STM32的HAL库并没有直接提供名为HAL_RS485_xxx的特定函数,而是通过配置通用的UART接口来实现RS485通信。在STM32 HAL库中,使用通用的UART接口函数来驱动RS485,同时需要搭配额外的RS485收发器(如SP3485、MAX485等)进行电平转换。以下是与RS485通信密切相关的HAL库函数及其功能描述:

  • __HAL_RCC_USARTx_CLK_ENABLE(...)

    • 关联寄存器:RCC_APBxPeriphClockCmd()函数间接影响USARTx的时钟使能寄存器(如APB1ENR/APB2ENR)。
    • 功能描述:使能指定USART(例如USART1、USART2等)的时钟,这是使用任何USART功能之前的必要步骤。
  • HAL_UART_Init(...)

    • 关联寄存器:USART_CR1、USART_CR2、USART_CR3等。
    • 功能描述:初始化指定的USART外设,配置诸如波特率、数据位数、停止位、奇偶校验、模式(如异步模式)、硬件流控制等各种参数。
  • __HAL_UART_ENABLE_IT(...)

    • 关联寄存器:USART_CR1(以及USART_CR2和USART_CR3,视中断类型而定)。
    • 功能描述:使能USART的相关中断,如接收数据寄存器非空中断(RXNE)、发送数据寄存器为空中断(TXE)等,这对RS485通信中的数据收发中断处理非常重要。
  • HAL_UART_Receive(...)

    • 关联寄存器:USART_DR(数据寄存器)。
    • 功能描述:通过DMA或中断方式从USART接收数据,适用于RS485接收数据阶段。
  • HAL_UART_Transmit(...)

    • 关联寄存器:USART_DR。
    • 功能描述:通过DMA或中断方式向USART发送数据,适用于RS485发送数据阶段。
  • __HAL_UART_GET_FLAG(...)

    • 关联寄存器:USART_SR(状态寄存器)。
    • 功能描述:查询USART当前的状态标志位,例如判断是否已完成数据发送(TC=Transmission Complete)、是否接收到新的数据(RXNE=Received Data Not Empty)等。

针对RS485通信,还需配置RS485收发器的控制引脚(如DE/RE)来切换RS485模式(发送或接收)。这部分通常不在HAL库提供的UART函数内,需要在应用层手动控制或者通过GPIO中断等方式进行管理。此外,为了正确进行RS485通信,还要考虑总线的信号线连接、终端电阻匹配等问题。

三、RS485配置步骤

RS485配置步骤,基于STM32 HAL库进行,可以细化为以下步骤:

  1. 配置串口工作参数

    • 使用HAL_UART_Init()函数初始化串口。在这个函数中,你需要提供一个指向UART_HandleTypeDef结构体的指针,并在结构体内填入串口的工作参数,如波特率、数据位数、停止位数、奇偶校验等。
  2. 串口底层初始化

    • 配置与串口相关的GPIO引脚,设置它们为复用功能模式,并配置为AF(Alternate Function)对应的串口功能。
    • 配置NVIC(Nested Vectored Interrupt Controller),为串口的中断请求分配优先级,并关联中断服务函数。
    • 使能串口对应的时钟,通过__HAL_RCC_USARTx_CLK_ENABLE()函数启用相应USART的时钟。
  3. 开启串口异步接收中断

    • 通过__HAL_UART_ENABLE_IT()函数启用串口的接收中断,例如启用UART_IT_RXNE,这样当接收数据寄存器非空时,会产生中断请求。
  4. 设置中断优先级并使能中断

    • 使用HAL_NVIC_SetPriority()函数设置串口中断的服务优先级。
    • 通过HAL_NVIC_EnableIRQ()函数使能串口对应的中断请求,例如USART1_IRQn
  5. 编写中断服务函数

    • 编写串口中断服务函数,如USARTx_IRQHandler()(其中x代表具体的串口号,如USART1、USART2等)。
    • 在中断服务函数内部,调用HAL_UART_IRQHandler()函数来处理中断,特别是如果有数据接收,HAL_UART_Receive_IT()HAL_UART_Receive_DMA()可用于异步接收数据。
  6. 串口数据发送

    • 发送数据时,通过写入USART的数据寄存器(USART_DR)来发送数据。
    • 使用HAL_UART_Transmit()函数发送数据,该函数在数据发送完毕后会返回成功标志,适合在非中断模式下使用;若采用中断模式发送数据,可使用HAL_UART_Transmit_IT()HAL_UART_Transmit_DMA()函数。

对于RS485通信,除了上述常规的UART配置之外,还需额外控制DE(Driver Enable)或RE(Receiver Enable)引脚,以切换RS485收发器的工作模式。在发送数据时,使能DE引脚以便驱动总线;在接收数据时,关闭DE引脚并开启RE引脚。这部分控制通常通过GPIO进行操作,并非直接在HAL库的UART接口函数内完成。

四、编程实战


RS485源码

rs485.c

c 复制代码
#include "./BSP/RS485/rs485.h"
#include "./SYSTEM/delay/delay.h"

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

#ifdef RS485_EN_RX /* 如果使能了接收 */

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

void RS485_UX_IRQHandler(void)
{
    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 */
        }
    }
}

#endif

/**
 * @brief       RS485初始化函数
 *   @note      该函数主要是初始化串口
 * @param       baudrate: 波特率, 根据自己需要设置波特率值
 * @retval      无
 */
void rs485_init(uint32_t baudrate)
{
    /* IO 及 时钟配置 */
    RS485_RE_GPIO_CLK_ENABLE(); /* 使能 RS485_RE 脚时钟 */
    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;
    HAL_GPIO_Init(RS485_TX_GPIO_PORT, &gpio_initure); /* 串口TX 脚 模式设置 */

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

    gpio_initure.Pin = RS485_RE_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(RS485_RE_GPIO_PORT, &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()会使能UART2 */

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

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

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

/**
 * @brief       RS485查询接收到的数据
 * @param       buf     : 接收缓冲区首地址
 * @param       len     : 接收到的数据长度
 *   @arg               0   , 表示没有接收到任何数据
 *   @arg               其他, 表示接收到的数据长度
 * @retval      无
 */
void rs485_receive_data(uint8_t *buf, uint8_t *len)
{
    uint8_t rxlen = g_RS485_rx_cnt;
    uint8_t i = 0;
    *len = 0;     /* 默认为0 */
    delay_ms(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; /* 记录本次数据长度 */
        g_RS485_rx_cnt = 0;    /* 清零 */
    }
}

rs485.h

c 复制代码
#ifndef __RS485_H
#define __RS485_H

#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* RS485 引脚 和 串口 定义 
 * 默认是针对RS485的.
 */
#define RS485_RE_GPIO_PORT                  GPIOD
#define RS485_RE_GPIO_PIN                   GPIO_PIN_7
#define RS485_RE_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)   /* PD口时钟使能 */

#define RS485_TX_GPIO_PORT                  GPIOA
#define RS485_TX_GPIO_PIN                   GPIO_PIN_2
#define RS485_TX_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */

#define RS485_RX_GPIO_PORT                  GPIOA
#define RS485_RX_GPIO_PIN                   GPIO_PIN_3
#define RS485_RX_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */

#define RS485_UX                            USART2
#define RS485_UX_IRQn                       USART2_IRQn
#define RS485_UX_IRQHandler                 USART2_IRQHandler
#define RS485_UX_CLK_ENABLE()               do{ __HAL_RCC_USART2_CLK_ENABLE(); }while(0)  /* USART2 时钟使能 */

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

/* 控制RS485_RE脚, 控制RS485发送/接收状态
 * RS485_RE = 0, 进入接收模式
 * RS485_RE = 1, 进入发送模式
 */
#define RS485_RE(x)   do{ x ? \
                          HAL_GPIO_WritePin(RS485_RE_GPIO_PORT, RS485_RE_GPIO_PIN, GPIO_PIN_SET) : \
                          HAL_GPIO_WritePin(RS485_RE_GPIO_PORT, RS485_RE_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

usart.c

c 复制代码
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"


/* 如果使用os,则包括下面的头文件即可. */
#if SYS_SUPPORT_OS
#include "includes.h" /* os 使用 */
#endif

/******************************************************************************************/
/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */

#if 1

#if (__ARMCC_VERSION >= 6010050)            /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t");  /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t");    /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */

#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
};

#endif

/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}


/* FILE 在 stdio.h里面定义. */
FILE __stdout;

/* MDK下需要重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f)
{
    while ((USART_UX->SR & 0X40) == 0);     /* 等待上一个字符发送完成 */

    USART_UX->DR = (uint8_t)ch;             /* 将要发送的字符 ch 写入到DR寄存器 */
    return ch;
}
#endif
/******************************************************************************************/

#if USART_EN_RX /*如果使能了接收*/

/* 接收缓冲, 最大USART_REC_LEN个字节. */
uint8_t g_usart_rx_buf[USART_REC_LEN];

/*  接收状态
 *  bit15,      接收完成标志
 *  bit14,      接收到0x0d
 *  bit13~0,    接收到的有效字节数目
*/
uint16_t g_usart_rx_sta = 0;

uint8_t g_rx_buffer[RXBUFFERSIZE];  /* HAL库使用的串口接收缓冲 */

UART_HandleTypeDef g_uart1_handle;  /* UART句柄 */

/**
 * @brief       串口X初始化函数
 * @param       baudrate: 波特率, 根据自己需要设置波特率值
 * @note        注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
 *              这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
 * @retval      无
 */
void usart_init(uint32_t baudrate)
{
    /* UART 初始化设置*/
    g_uart1_handle.Instance = USART_UX;                                       /* USART_UX */
    g_uart1_handle.Init.BaudRate = baudrate;                                  /* 波特率 */
    g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;                      /* 字长为8位数据格式 */
    g_uart1_handle.Init.StopBits = UART_STOPBITS_1;                           /* 一个停止位 */
    g_uart1_handle.Init.Parity = UART_PARITY_NONE;                            /* 无奇偶校验位 */
    g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;                      /* 无硬件流控 */
    g_uart1_handle.Init.Mode = UART_MODE_TX_RX;                               /* 收发模式 */
    HAL_UART_Init(&g_uart1_handle);                                           /* HAL_UART_Init()会使能UART1 */

    /* 该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量 */
    HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE); 
}

/**
 * @brief       UART底层初始化函数
 * @param       huart: UART句柄类型指针
 * @note        此函数会被HAL_UART_Init()调用
 *              完成时钟使能,引脚配置,中断配置
 * @retval      无
 */
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef gpio_init_struct;

    if (huart->Instance == USART_UX)                            /* 如果是串口1,进行串口1 MSP初始化 */
    {
        USART_TX_GPIO_CLK_ENABLE();                             /* 使能串口TX脚时钟 */
        USART_RX_GPIO_CLK_ENABLE();                             /* 使能串口RX脚时钟 */
        USART_UX_CLK_ENABLE();                                  /* 使能串口时钟 */

        gpio_init_struct.Pin = USART_TX_GPIO_PIN;               /* 串口发送引脚号 */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;                /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* IO速度设置为高速 */
        HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct);
                
        gpio_init_struct.Pin = USART_RX_GPIO_PIN;               /* 串口RX脚 模式设置 */
        gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;    
        HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct);   /* 串口RX脚 必须设置成输入模式 */
        
#if USART_EN_RX
        HAL_NVIC_EnableIRQ(USART_UX_IRQn);                      /* 使能USART1中断通道 */
        HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3);              /* 组2,最低优先级:抢占优先级3,子优先级3 */
#endif
    }
}

/**
 * @brief       串口数据接收回调函数
                数据处理在这里进行
 * @param       huart:串口句柄
 * @retval      无
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART_UX)                    /* 如果是串口1 */
    {
        if ((g_usart_rx_sta & 0x8000) == 0)             /* 接收未完成 */
        {
            if (g_usart_rx_sta & 0x4000)                /* 接收到了0x0d(即回车键) */
            {
                if (g_rx_buffer[0] != 0x0a)             /* 接收到的不是0x0a(即不是换行键) */
                {
                    g_usart_rx_sta = 0;                 /* 接收错误,重新开始 */
                }
                else                                    /* 接收到的是0x0a(即换行键) */
                {
                    g_usart_rx_sta |= 0x8000;           /* 接收完成了 */
                }
            }
            else                                        /* 还没收到0X0d(即回车键) */
            {
                if (g_rx_buffer[0] == 0x0d)
                    g_usart_rx_sta |= 0x4000;
                else
                {
                    g_usart_rx_buf[g_usart_rx_sta & 0X3FFF] = g_rx_buffer[0];
                    g_usart_rx_sta++;

                    if (g_usart_rx_sta > (USART_REC_LEN - 1))
                    {
                        g_usart_rx_sta = 0;             /* 接收数据错误,重新开始接收 */
                    }
                }
            }
        }
    }
}

/**
 * @brief       串口X中断服务函数
                注意,读取USARTx->SR能避免莫名其妙的错误
 * @param       无
 * @retval      无
 */
void USART_UX_IRQHandler(void)
{
#if SYSTEM_SUPPORT_OS                                                   /* 使用OS */
    OSIntEnter();
#endif
    HAL_UART_IRQHandler(&g_uart1_handle);                               /* 调用HAL库中断处理公用函数 */

    while (HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE) != HAL_OK)     /* 重新开启中断并接收数据 */
    {
        /* 如果出错会卡死在这里 */
    }

#if SYSTEM_SUPPORT_OS                                                   /* 使用OS */
    OSIntExit();
#endif
}
#endif

usart.h

c 复制代码
#ifndef __USART_H
#define __USART_H

#include "stdio.h"
#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* 引脚 和 串口 定义 
 * 默认是针对USART1的.
 * 注意: 通过修改这几个宏定义,可以支持USART1~UART5任意一个串口.
 */
#define USART_TX_GPIO_PORT                  GPIOA
#define USART_TX_GPIO_PIN                   GPIO_PIN_9
#define USART_TX_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */

#define USART_RX_GPIO_PORT                  GPIOA
#define USART_RX_GPIO_PIN                   GPIO_PIN_10
#define USART_RX_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */

#define USART_UX                            USART1
#define USART_UX_IRQn                       USART1_IRQn
#define USART_UX_IRQHandler                 USART1_IRQHandler
#define USART_UX_CLK_ENABLE()               do{ __HAL_RCC_USART1_CLK_ENABLE(); }while(0)  /* USART1 时钟使能 */

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

#define USART_REC_LEN               200         /* 定义最大接收字节数 200 */
#define USART_EN_RX                 1           /* 使能(1)/禁止(0)串口1接收 */
#define RXBUFFERSIZE   1                        /* 缓存大小 */

extern UART_HandleTypeDef g_uart1_handle;       /* HAL UART句柄 */

extern uint8_t  g_usart_rx_buf[USART_REC_LEN];  /* 接收缓冲,最大USART_REC_LEN个字节.末字节为换行符 */
extern uint16_t g_usart_rx_sta;                 /* 接收状态标记 */
extern uint8_t g_rx_buffer[RXBUFFERSIZE];       /* HAL库USART接收Buffer */


void usart_init(uint32_t bound);                /* 串口初始化函数 */

#endif

main.c

c 复制代码
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/RS485/rs485.h"


int main(void)
{
    uint8_t key;
    uint8_t i = 0, t = 0;
    uint8_t cnt = 0;
    uint8_t rs485buf[5];

    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟, 72Mhz */
    delay_init(72);                             /* 延时初始化 */
    usart_init(115200);                         /* 串口初始化为115200 */
    usmart_dev.init(72);                        /* 初始化USMART */
    led_init();                                 /* 初始化LED */
    lcd_init();                                 /* 初始化LCD */
    key_init();                                 /* 初始化按键 */
    rs485_init(9600);                           /* 初始化RS485 */

    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "RS485 TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:Send", RED);    /* 显示提示信息 */

    lcd_show_string(30, 130, 200, 16, 16, "Count:", RED);       /* 显示当前计数值 */
    lcd_show_string(30, 150, 200, 16, 16, "Send Data:", RED);   /* 提示发送的数据 */
    lcd_show_string(30, 190, 200, 16, 16, "Receive Data:", RED);/* 提示接收到的数据 */

    while (1)
    {
        key = key_scan(0);

        if (key == KEY0_PRES)   /* KEY0按下,发送一次数据 */
        {
            for (i = 0; i < 5; i++)
            {
                rs485buf[i] = cnt + i;      /* 填充发送缓冲区 */
                lcd_show_xnum(30 + i * 32, 170, rs485buf[i], 3, 16, 0X80, BLUE);    /* 显示数据 */
            }

            rs485_send_data(rs485buf, 5);   /* 发送5个字节 */
        }

        rs485_receive_data(rs485buf, &key);

        if (key)    /* 接收到有数据 */
        {
            if (key > 5) key = 5;    /* 最大是5个数据. */

            for (i = 0; i < key; i++)
            {
                lcd_show_xnum(30 + i * 32, 210, rs485buf[i], 3, 16, 0X80, BLUE);    /* 显示数据 */
            }
        }

        t++;
        delay_ms(10);

        if (t == 20)
        {
            LED0_TOGGLE();  /* LED0闪烁, 提示系统正在运行 */
            t = 0;
            cnt++;
            lcd_show_xnum(30 + 48, 130, cnt, 3, 16, 0X80, BLUE);    /* 显示数据 */
        }
    }
}



五、总结


相关推荐
sakabu1 个月前
libmodbus编程应用(超详细源码讲解+移植到stm32)
笔记·学习·开源协议·modbus协议·libmodbus
兴达易控5 个月前
Modbus协议转Profibus协议模块接热传感器配置攻略
modbus转·modbus协议·转profibus协议模块·modbus协议转·profibus协议网关·profibus协议模块·profibus协议转