【正点原子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)通常指的是用于串行通信的基本规则,它定义了数据在串行链路上如何进行传输,包括但不限于以下几个关键要素:
- 通信时序:启动位、数据位(一般为5到8位)、奇偶校验位(可选)和停止位(1到2位)。
- 波特率:每秒传输的位数,常见的有9600、19200、38400、115200等。
- 通信方向:可以是全双工(同时发送和接收数据)、半双工(同一时间只能进行发送或接收)或单工(只能发送或只能接收)。
而MODBUS协议则建立在串口基础协议之上,是一种应用层协议,用于在不同设备间进行数据交换,特别是工业控制系统中的现场设备如PLC、智能仪表、传感器和执行器等。MODBUS协议的特点包括:
- 主从结构:网络中有一个主设备(主控制器)向多个从设备发起请求,从设备响应请求。
- 功能码:MODBUS协议定义了一系列功能码,每个功能码对应一种操作,如读取线圈状态、寄存器值,写入线圈状态、寄存器值等。
- 数据组织:MODBUS协议中的数据传输包括设备地址、功能码、数据区(数据长度根据功能码定义)和校验码(如RTU模式下的CRC校验)。
在实际应用中,串口基础协议提供的是物理层和链路层的通信基础,而MODBUS协议则是更高层次的应用层协议,它规定了如何在串口通信的基础上构造有意义的消息结构,从而实现设备间复杂的控制和数据交换。
MODBUS协议
MODBUS协议是一种广泛应用于工业控制领域的串行通信协议,最初由Modicon公司于1979年发布,现已成为一种通用的工业标准协议。MODBUS允许不同厂商的设备通过串行线路或以太网进行通信,从而实现了不同设备之间的互操作性。
MODBUS协议主要特点:
- 主从架构:网络中只有一个主设备(如PLC或HMI),可以向多个从设备(如传感器、执行器、其他控制器等)发送请求并接收响应。
- 功能码:MODBUS协议定义了一系列功能码,用于执行读写操作,如读取线圈状态、寄存器值、输入状态,写入线圈状态、寄存器值等。
- 数据传输模式: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总线连接中,组件之间的连接和作用如下:
-
CPU:中央处理器,负责控制整个系统的运行和数据处理。
-
UART控制器:集成在CPU内部或外部的通用异步收发传输器,用于生成和解析串行通信数据流。
-
TXD/RXD连接:
- TXD(Transmit Data):UART控制器的发送数据线,通常连接到485收发器的接收端(如DE/RE引脚禁能时的接收端或RO引脚)。
- RXD(Receive Data):UART控制器的接收数据线,通常连接到485收发器的发送端(如DI引脚)。
-
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。
-
485_A/485_B:
- 这是RS485总线的差分信号线,485_A和485_B两根线通过双绞线连接到所有的RS485设备,确保信号质量良好。
-
匹配电阻:
- 在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库进行,可以细化为以下步骤:
-
配置串口工作参数
- 使用
HAL_UART_Init()
函数初始化串口。在这个函数中,你需要提供一个指向UART_HandleTypeDef
结构体的指针,并在结构体内填入串口的工作参数,如波特率、数据位数、停止位数、奇偶校验等。
- 使用
-
串口底层初始化
- 配置与串口相关的GPIO引脚,设置它们为复用功能模式,并配置为AF(Alternate Function)对应的串口功能。
- 配置NVIC(Nested Vectored Interrupt Controller),为串口的中断请求分配优先级,并关联中断服务函数。
- 使能串口对应的时钟,通过
__HAL_RCC_USARTx_CLK_ENABLE()
函数启用相应USART的时钟。
-
开启串口异步接收中断
- 通过
__HAL_UART_ENABLE_IT()
函数启用串口的接收中断,例如启用UART_IT_RXNE
,这样当接收数据寄存器非空时,会产生中断请求。
- 通过
-
设置中断优先级并使能中断
- 使用
HAL_NVIC_SetPriority()
函数设置串口中断的服务优先级。 - 通过
HAL_NVIC_EnableIRQ()
函数使能串口对应的中断请求,例如USART1_IRQn
。
- 使用
-
编写中断服务函数
- 编写串口中断服务函数,如
USARTx_IRQHandler()
(其中x
代表具体的串口号,如USART1、USART2等)。 - 在中断服务函数内部,调用
HAL_UART_IRQHandler()
函数来处理中断,特别是如果有数据接收,HAL_UART_Receive_IT()
或HAL_UART_Receive_DMA()
可用于异步接收数据。
- 编写串口中断服务函数,如
-
串口数据发送
- 发送数据时,通过写入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
#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 */
#endifRS485_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
#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
#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_RXHAL_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();
#endifHAL_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
#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
#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); /* 显示数据 */}}
}
五、总结
相关文章:

【正点原子STM32】RS485串行通信标准(串口基础协议 和 MODBUS协议、总线连接、通信电路、通信波形图、RS485相关HAL库驱动、RS485配置步骤、)
一、RS485介绍 二、RS485相关HAL库驱动介绍 三、RS485配置步骤 四、编程实战 五、总结 串口、 UART、TTL、RS232、RS422、RS485关系 串口、UART、TTL、RS232、RS422和RS485之间的关系可以如此理解: 串口:是一个广义术语,通常指的是采用串行通…...
从SPDY到HTTP/2:网络协议的革新与未来
从SPDY到HTTP/2:网络协议的革新与未来 在互联网的发展史上,协议的演进始终是推动用户体验提升的关键。从早期的HTTP/1.1到如今的HTTP/2,再到即将全面普及的HTTP/3,每一次变革都伴随着性能、安全性和效率的突破。今天,…...

在力扣刷题中触摸算法的温度
在代码的世界里,每一道力扣题目都是一扇通往未知的门。当我推开这些门,与内置求和函数、二进制位运算、辗转相减思想以及链表结构相遇时,才真正触摸到算法的温度 —— 那是一种理性与智慧交织的炽热,也是思维不断淬炼的滚烫。 最…...

外部访问可视化监控 Grafana (Windows版本)
Grafana 是一款通用,美观的,强大的可视化监控指标的展示工具。可以将不同的数据源数据以图形化的方式展示。它支持多种数据源,如 Prometheus 等,可以满足不同的需求。也可以通过插件和 API 进行扩展满足各种需求,…...

通用的防御框架,用于抵御(多模态)大型语言模型的越狱攻击
大家读完觉得有帮助记得关注!!! 摘要 尽管(多模态)大型语言模型(LLMs)因其卓越的能力而受到广泛关注,但它们仍然容易受到越狱攻击。已经提出了各种防御方法来防御越狱攻击ÿ…...
聊聊JVM怎么调优?(实战总结)
JVM 核心配置与调优指南 一、堆内存与年轻代配置(影响最大) 堆内存大小: 在资源允许的前提下,堆内存应尽可能设置得更大。关键点: 必须将堆内存的最大值 (-Xmx) 和最小值 (-Xms) 设置为相同值。动态扩容会触发 Full G…...

新能源汽车电控系统的精准守护者PKDV5355高压差分探头
在新能源汽车的"心脏"——电控系统中,每一次电流的精准切换都关乎车辆的性能与安全。PRBTEK PKDV5355高压差分探头就像一位经验丰富的"汽车医生",帮助工程师们精准捕捉IGBT模块的每一次"心跳",确保电驱系统健康…...

C# 导出word 插入公式问题
最近遇到了一个问题,下载一个文档时需要下载word可编辑的公式。找了很久终于找到了一种解决办法。下面是以C#代码来实现在Word中插入公式的功能。 目录 一、引入dll程序集文件1、通过 NuGet 引入dll(2种方法)的方法:2、手动添加d…...

Mac安装配置InfluxDB,InfluxDB快速入门,Java集成InfluxDB
1. 与MySQL的比较 InfluxDBMySQL解释BucketDatabase数据库MeasurementTable表TagIndexed Column索引列FieldColumn普通列PointRow每行数据 2. 安装FluxDB brew update默认安装 2.x的版本 brew install influxdb查看influxdb版本 influxd version # InfluxDB 2.7.11 (git: …...

手撕Java+硅基流动实现MCP服务器教程
手撕Java硅基流动实现MCP服务器教程 一、MCP协议核心概念 MCP是什么 MCP 是 Anthropic (Claude) 主导发布的一个开放的、通用的、有共识的协议标准。 ● MCP 是一个标准协议,就像给 AI 大模型装了一个 “万能接口”,让 AI 模型能够与不同的数据源和工…...

EasyRTC嵌入式音视频通信SDK助力1v1实时音视频通话全场景应用
一、方案概述 在数字化通信需求日益增长的今天,EasyRTC作为一款全平台互通的实时视频通话方案,实现了设备与平台间的跨端连接。它支持微信小程序、APP、PC客户端等多端协同,开发者通过该方案可快速搭建1v1实时音视频通信系统,适…...

Prometheus学习之pushgateway和altermanager组件
[rootnode-exporter41 /usr/local/alertmanager-0.28.1.linux-amd64]# pwd /usr/local/alertmanager-0.28.1.linux-amd64[rootnode-exporter41 /usr/local/alertmanager-0.28.1.linux-amd64]# cat alertmanager.yml # 通用配置 global:resolve_timeout: 5msmtp_from: 914XXXXX…...

01 redis 的环境搭建
前言 这一系列文章主要包含的内容主要是 各种常用软件的调试环境的搭建 主要的目的是 搭建一个可打断点的一个调试环境 c 系列 主要是基于 clion 调试, java 系列主要是基于 idea 调试, js 系列主要是基于 webstorm 调试 需要有一定的 c, c, java, js 相关基础 基于的…...

《操作系统真相还原》——加载器
显存 将上一章的中断输出,变为显存输出 加载器 使用mbr引导程序从磁盘中加载loader程序。 MBR %include "boot.inc" SECTION MBR vstart0x7c00 mov ax,cs mov ds,axmov es,axmov ss,axmov fs,axmov sp,0x7c00mov ax,0xb800mov gs,ax;cl…...
电网即插即用介绍
一、统一设备信息模型与标准接口 实现即插即用功能的基础在于建立统一的设备信息模型。不同厂家生产的各类电网设备,其内部结构、通信协议、数据格式等往往千差万别。通过制定统一的设备信息模型,能够对设备的各种属性、功能以及接口进行标准化定义&…...

HJ25 数据分类处理【牛客网】
文章目录 零、原题链接一、题目描述二、测试用例三、解题思路四、参考代码 零、原题链接 HJ25 数据分类处理 一、题目描述 二、测试用例 三、解题思路 基本思路: 首先理解题目,题目要求对规则集先进行排序,然后去重,这一步我…...
spring-boot redis lua脚本实现滑动窗口限流
因为项目中没有集成redisson,但是又需要用到限流,所以简单的将redisson中限流的核心lua代码移植过来,并进行改造,因为公司版本的redis支持lua版本为5.1,针对于长字符串的数字,使用tonumber转换的时候会得到…...
USB MSC
主机(如电脑)识别USB MSC(Mass Storage Class)设备中的文件,本质上是通过多层协议协作实现的,涉及USB枚举、SCSI命令传输和文件系统解析三个核心环节。以下是详细机制: 🔍 一、USB…...
css实现文字渐变
在前端开发中,给文字设置渐变色是完全可以实现的,常用的方式是结合 CSS 的 background、-webkit-background-clip 和 -webkit-text-fill-color 属性。下面是一个常见的实现方法: <!DOCTYPE html> <html lang"zh-CN"> …...

FART 自动化脱壳框架一些 bug 修复记录
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/ open() 判断不严谨 https://github.com/CYRUS-STUDIO/FART/blob/master/fart10/art/runtime/art_method.cc 比如: int dexfilefp open(dex_pat…...

基于Flask实现豆瓣Top250电影可视化
项目截图 概述 该项目旨在对豆瓣Top 250电影进行全面的数据分析,使用了Python爬虫、Flask框架进行开发,并采用了Echarts进行数据可视化以及WordCloud进行词云分析。应用展示了多个功能,如电影列表、评分分布、词频统计和团队信息。 主要功能…...

More SQL(Focus Subqueries、Join)
目录 Subqueries Subqueries That Return One Tuple Subqueries and Self Connection The IN Operator The Exists Operator The Operator ANY The Operator ALL Union, Intersection, and Difference(交并差) Bag Semantics Controlling Dupl…...
项目部署react经历
简单的说: 1. 编译打包并压缩为压缩包 2. 将压缩包上传到服务器(这里以宝塔面板为例:www/wwwroot/目录下) 3. 将文件解压生成比如:www/wwwroot/ttms/build/* 多文件 4. php 项目建站,选择静态ÿ…...

从图像处理到深度学习:直播美颜SDK的人脸美型算法详解
在直播的镜头前,每一位主播都希望自己“光彩照人”。但在高清摄像头无死角的审视下,哪怕是天生丽质,也难免需要一点技术加持。于是,美颜SDK应运而生,成为直播平台提升用户粘性和视觉体验的重要工具。 尤其是在“人脸美…...
智能教育个性化学习路径规划系统实战指南
引言 在数字化教育革命中,如何利用AI技术实现"因材施教"的千年教育理想?本文将通过构建一个完整的智能教育系统,演示如何基于Python生态(Django机器学习)实现从数据采集到个性化推荐的全流程。系统将通过分…...
spark- ResultStage 和 ShuffleMapStage介绍
目录 1. ShuffleMapStage(中间阶段)1.1 作用1.2 核心特性1.3 示例2. ResultStage(最终结果阶段)2.1 作用2.2 核心特性2.3 示例3. 对比总结4. 执行流程示例5. 常见问题Q1:为什么需要区分两种 Stage?**Q2:如何手动观察 Stage 划分?Q3:ShuffleMapStage 的数据一定会落盘吗…...

zTasker一款Windows自动化软件,提升效率:大小仅有10MB,免费无广告
一、zTasker是什么? zTasker是一款发布于2023年9月的免费无广告工具,专为Windows用户打造。它以仅8MB的轻量体积、极低资源占用(内存消耗不足10MB)和秒级启动速度脱颖而出,堪称“任务计划程序的终极强化版”。无论是定…...
人工智能100问☞第34问:什么是语音识别与合成?
目录 一、通俗解释 二、专业解析 三、权威参考 在人工智能的世界里,“看、听、说、写”早已不是人类的专属技能。语音识别,让机器有了耳朵;语音合成,让机器长了嘴巴;合在一起,机器就开始“说人话、听人言”了。 一、通俗解释 1、语音识别:让机器听懂人说话 你有没…...

最大流-Ford-Fulkerson增广路径算法py/cpp/Java三语言实现
最大流-Ford-Fulkerson增广路径算法py/cpp/Java三语言实现 一、网络流问题与相关概念1.1 网络流问题定义1.2 关键概念 二、Ford-Fulkerson算法原理2.1 核心思想2.2 算法步骤 三、Ford-Fulkerson算法的代码实现3.1 Python实现3.2 C实现3.3 Java实现 四、Ford-Fulkerson算法的时间…...
怎么从一台电脑拷贝已安装的所有python第三方库到另一台
要将Python库从一台电脑拷贝到另一台,可以采用以下方法: 方法一:使用pip命令导出和安装依赖 如果目标电脑在线,且python与pip命令可以正常使用 在源电脑上,打开命令行,执行以下命令导出所有依赖库到requ…...