一.常用的串口通讯

1.串口通信核心分类(同步/异步)

串口通信从时序上分为两种模式,是所有串口协议的基础:

  1. 同步通信:英文 Synchronous,缩写 Sync,收发双方依靠统一时钟信号对齐数据
  2. 异步通信:英文 Asynchronous,缩写 Async,无统一时钟,依靠起始位、停止位帧结构对齐数据

注:日常单片机 TTL、RS232、RS485 串口,全部默认是异步通信。

2.波特率核心原理

  1. 定义:波特率表示1秒钟发送的二进制bit位数,作用是让收发双方数据严格对应、精准解析。
  2. 异步串口帧结构:单片机发送 1 字节有效数据时,必须额外携带1位起始位 + 1位停止位,单次传输实际占用 10 bit。
  3. 举例计算(115200 波特率):

每秒比特数:115200 bit/s

每秒字节数 = 115200 ÷ 10 = 11520 byte/s

3.TTL 串口电平与特性(单片机原生电平)

  1. 标准 TTL 电平定义:
  1. 逻辑 0(低电平):0V ~ 0.4V
  2. 逻辑 1(高电平):2.4V ~ 5V
  1. 单片机硬件适配:常规 3.3V 单片机硬件限制,实际以 2.4V ~ 3.3V 判定为高电平逻辑1。
  2. 硬件本质:单片机直接输出的串口电平,默认就是 TTL 电平。
  3. 通信帧规则:和所有异步串口一致,发送1字节数据需拼接起始位、停止位,单次传输10个bit。
  4. 优缺点:电平电压范围极小,极易受外界电磁干扰,传输距离极短,常规有效传输距离 1m 以内。

4.RS232 串口电平与特性

  1. 电平标准(与 TTL 完全相反、电压范围更广):
  1. 逻辑 0:-3V ~ -15V(负电压)
  2. 逻辑 1:+3V ~ +15V(正电压)
  1. 硬件要求:单片机只能输出 TTL 电平,无法直接输出 RS232 电平,必须外接 MAX232 电平转换芯片完成电压和逻辑翻转。
  2. 硬件接线:标准三线制通信 TX / RX / GND。
  3. 通信模式:和 TTL 串口一致,属于异步单端通信,依赖公共 GND 做参考。
  4. 优缺点:相比 TTL 电平电压范围宽,抗干扰能力大幅提升,标准最大传输距离可达 15m。

5.RS485 差分通信原理与特性

  1. 传输方式:采用差分平衡传输,依靠 A、B 两根信号线的电压差值判断逻辑,不依赖单一 GND 参考,彻底解决地电位偏移问题 但只能半双工通讯。
  2. 共模干扰抵抗核心机制:工业环境中的外界干扰噪声,会同时叠加在 A、B 两根信号线上,形成共模电压;RS485 电路只识别 A、B 之间的电压差值,会自动抵消两路相同的干扰电压,完美过滤共模干扰,适配强干扰工业场景。
  3. 逻辑判定标准:
  1. 驱动器输出(实际测量电压):Va-Vb = +2V ~ +6V → 逻辑 1
  2. 驱动器输出(实际测量电压):Va-Vb = -6V ~ -2V → 逻辑 0
  3. 补充关键考点(必考):接收端识别阈值更宽松,只要差分电压绝对值 ≥ 200mV 就能正确识别逻辑,这也是485抗干扰、远距离传输的容错余量来源。
  1. 核心优势:抗干扰能力极强、传输距离远,最大有效传输距离可达 1200m;支持一主多从总线架构,可实现多设备组网通信但是连接单片机需要485转换芯片。
  2. 传输速度大概能达到10Mbps 10 000000 bit/s 即每秒能传送1M字节的数据随距离增加。

6.三种串口核心总结

  1. TTL:单片机原生电平、距离短、易受干扰、板内短距离调试使用
  2. RS232:电平翻转、电压范围大、抗干扰更强、点对点短距离外接设备通信(15m)
  3. RS485:差分传输、抗共模干扰、远距离、支持多机组网,工业通信首选

二.串口通讯硬件基本原理

三.串口通讯标准库学习

  1. 先开 USART1 和 GPIOA 的时钟 
  2. 再把 PA9/PA10 配成 AF 
  3. GPIO_PinAFConfig() 一定用 GPIO_PinSource9/10 
  4. 然后 USART_Init() 
  5. 最后 USART_Cmd(ENABLE)

Q:void USART_SendData(USART_TypeDef* USARTx, uint16_t Data) 传入的数据是uint16,但是我怎么可以写入uint8照样可以发出去?数据发送寄存器和接收寄存器公用一个DR寄存器吗

A:uint16_t Data 不是说你必须发 16 位,而是因为这个 API 要兼容不同的数据长度配置,尤其是 9-bit 模式。函数内部实际上做了掩码:

USARTx->DR = (Data & 0x01FF);

 Data 用 uint16_t 是为了兼容 8/9 位发送

四.串口通讯HAL库学习

1、串口通讯常用的函数,基于USART_IT中断

HAL_UART_Receive_IT(&huart1,buffer_uart_data,5);

用于开启串口接收中断,每来一个字节会触发

void USART1_IRQHandler(void)

由于是定长中断触发,所以HAL_UART_Receive_IT函数对应的5字节触发回调函数

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

串口的数据是每来一个字节就会进行一次数据拷贝:

HAL 库的中断接收HAL_UART_Receive_IT是一次性的,因此在中断函数中还要进行下次使能。

2.串口基于DMA中断接收数据

关于USART-DMA的配置

原理:

使用DMA需要注意所设置的数据宽度是多少,如果是字,那么接收一个字节就会放进4byte的内存中 最低位。

修改数据宽度为byte如下

因为 DMA 接收完成后,最终也会调用同一个回调函数:HAL_UART_RxCpltCallback

不管你用的是:

  1. HAL_UART_Receive_IT
  2. HAL_UART_Receive_DMA

完成后,进的都是同一个回调:HAL_UART_RxCpltCallback

HAL针对于USART单独封装了一个DMA的APIHAL_UART_Receive_DMA

在UART_Start_Receive_DMA函数中完成了DMA函数指针的绑定(DMA中断的注册)

3.CPU的SRAM的搬运速度和AHB总线速度相关

AHB = 16 MHz → 一次搬运循环(50 次)耗时 33 us

AHB = 100 MHz → 一次搬运循环(50 次)耗时 4 us

CPU 内核 通过 AHB 总线 访问 SRAM

SRAM 挂在 AHB 总线 上

你写的 (uint32_t)addr = data 是 AHB 上的读写操作

三个中断的配合例子:​ 

中断一(DMA 半满中断),中断二(DMA 全满中断),中断三(串口空闲中断)之间的关系:​     

当使用 HAL_UARTEx_ReceiveToIdle_DMA 函数时,假设参数三 Size 是 32 字节(半满 16 字节)。​ 

当发送 17 个字节时,会先进入中断一(DMA 半满中断),然后再进入中断三(串口空闲中断)。​  

当发送 15 个字节时,会直接进入中断三(串口空闲中断)。​

当发送 33 个字节时,会先进入中断一(DMA 半满中断),然后进入中断二(DMA 全满中断),然后进入中断三(串口空闲中断)。并且,由于是 33 个字节,如果这个时候 DMA 的配置为 DMA_NORMAL 模式,则第 33 个字节会被丢掉。如果,DMA 被配置为 DMA_CIRCULAR,则第 33 个字节会覆盖最开始的第一个字节。

3.1半满全满中断介绍

每次数据达到半满或者全满会触发一次中断。

HAL_UART_Receive_DMA(&huart1, recv_data, 20);

NDTR 会被配置为20,每当写入一个数据NDTR就会-1;但是要注意这里是写入一个数据,而不是接收一个数据。

如果打开了FIFO,Threshold设置为Full 那么由于DMA FIFO深度是4*word =4*4字节=16字节

那么就会接收数据一直存在FIFO里,且Threshold配置为FULL,那当FIFO满了会触发 突发传输 一次性把16字节数据全写进SRAM。

那么此时NDTR的值直接从20变为了20-16=4;直接触发半满中断,再发4个字节数据触发全满中断。

所以在使用FIFO时候需要注意半满全满。

如果设置为Threshold=Full,DMA FIFO满了,UART 内部 RX FIFO 满了,外部还在发数据,并且此时CPU在访问DMA的目的地-SRAM导致DMA无法写入,那么此时会丢UART数据。

所以Threshold=Full 会让 “抗阻塞余量” 变小,高速 / 高负载下建议用 1/2 或 1/4 阈值更安全。

开 FIFO 后是批量搬运,NDTR 是跳变,但 HT 逻辑仍然按 “累计量过半” 来算

关于DMA的IRQHandler函数:触发条件:

void DMA2_Stream2_IRQHandler(void)
{
  /* USER CODE BEGIN DMA2_Stream2_IRQn 0 */

  /* USER CODE END DMA2_Stream2_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_usart1_rx);
  /* USER CODE BEGIN DMA2_Stream2_IRQn 1 */

  /* USER CODE END DMA2_Stream2_IRQn 1 */
}

只要下面 任意一种中断发生,就会进入:

  1. HT 中断(Half Transfer)半传输→ 你设置 20 字节 → 收到 ≥10 字节就进
  2. TC 中断(Transfer Complete)传输完成→ 20 字节收完 → 进
  3. TE 中断(Transfer Error)传输错误
  4. FE 中断(FIFO Error)FIFO 上溢 / 下溢
  5. DME 中断(Direct Mode Error)

3.2全满半满中断函数

void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
	if(&huart1 == huart)
	{
		printf("half_interrupt\r\n");
	}

}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(&huart1 == huart)
	{
		HAL_UART_Receive_DMA(&huart1, recv_data, 20);
		printf("full_interrupt\r\n");
	}

}

3.3Burst Size 配置介绍

如果DMA正在向SRAM写入数据,而此时CPU需要访问SRAM,那么AHB总线仲裁就会让DMA停止写入,转而给CPU权限。如果DMA写入的数据正是CPU要拿的数据,那么会导致出现问题。因此只有两种方式防止DMA被打断:1、互斥锁 2、Burst

USART的Burst Size 意味着每接受一个字节触发多少次的DMA请求。

memory的Burst Size意味着每次触发多少次突发传输。并且此突发传输不会被高优先级的任务打断。即:将AHB总线锁死  只会锁死当前正在使用的那一条 AHB 总线。

对于USART 接收一个字节就触发一次DMA请求,如Buest Size 设置为4 那么就会连续触发4次的DMA请求,发生如下情况

USART接收到 0x12

此时DMA的FIFO里存入  0x12 0x12 0x12 0x12 (连着存四个)

---所以一般设置USART的buestSize =single

Burst Size 的设置和FIFO的阈值字节数以及数据宽度有关(根据Threshold 所配置的)

FIFO 阈值字节数 = N × (BURST × SIZE),N=1,2,3,4;

就是说BURST的设置就是分几拍写入到内存中,可以是

MSIZE = 1B ;那么MBURST = 4;

分四拍,由于阈值容量选的FULL 那么FIFO满了才写入。

就是每当FIFO满了就开始写入,一次写入MSIZE = 1B (N) 就是4;

4拍就是 16 字节完成写入,以确保原子操作。

3.4接收不定长数据-串口空闲中断介绍

HAL_UART_Receive_DMA() 是标准定长 DMA 接收 API,工作模式标记为 HAL_UART_RECEPTION_STANDARD;

而 HAL_UARTEx_RxEventCallback 是扩展事件回调,仅当接收模式为 HAL_UART_RECEPTION_TOIDLE(空闲帧模式)时才会被 HAL 调度执行,二者底层完全隔离,天然不会联动。

1.HAL_UART_Receive_DMA(标准 DMA 接收)

内部行为

仅开启 DMA TC(传输完成)、HT(半满)中断;不会开启串口 IDLE 空闲中断;

huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;

中断触发 & 对应回调

DMA 收满Size字节(TC 全满) → 进入 HAL_UART_RxCpltCallback

DMA 收到一半数据(HT 半满) → 进入 HAL_UART_RxHalfCpltCallback

完全不涉及任何 IDLE 空闲事件,HAL 中断服务函数里不会调用 RxEventCallback。

2. HAL_UARTEx_ReceiveToIdle_DMA(空闲帧 DMA 接收,你要的版本)

内部行为

自动同时开启 DMA HT/TC 中断 + USART IDLE 空闲中断;

huart->ReceptionType = HAL_UART_RECEPTION_TOIDLE;

中断触发 & 对应回调

总线空闲 IDLE / DMA 半满 HT / DMA 填满 TC,全部统一进入 HAL_UARTEx_RxEventCallback;

在回调内可用 HAL_UARTEx_GetRxEventType(huart) 判断是哪种事件。

3.在HAL_UART_IRQHandler中会根据具体情况进行中断事件分发

高速无缝连续数据流场景

1.IDLE 空闲中断的触发前提必须总线出现至少 1 个字符时长的空闲电平,硬件才会置 IDLE 标志、触发中断; 当多帧数据首尾紧挨着、帧之间无任何间隔,整条数据流是连续电平,IDLE 标志永远不会置位

2.此时的尴尬局面 DMA 缓冲区还没收满预设Size,不会触发 TC 全满中断;又没有空闲,IDLE 中断也不进;接收缓存持续不断写入新数据,旧数据没有机会被业务代码读取解析,最终 DMA 循环缓存覆盖旧帧、普通 DMA 缓存直接溢出丢包。

3.HT 半满中断的补偿作用DMA 接收达到缓冲区一半长度时强制触发中断,提前把前半段数据取走解析,提前释放缓存空间,不让缓存被持续填满积压。

防止串口通信中数据丢失或错误,我通常从以下几个维度出发:一是使用 DMA 结合空闲中断和环形缓冲区机制,减少 CPU 干预同时避免缓冲溢出;二是启用硬件或软件校验机制如 CRC、奇偶校验,确保数据正确性;三是从协议层设计鲁棒的帧结构、加上超时与重传机制防止数据错乱;四是提高串口中断优先级并将解包逻辑下沉至后台任务;五是硬件层面选用 RS-485 和滤波抗干扰设计。实际项目中根据使用场景做不同策略组合,以保证串口通信可靠。

五.串口环形缓冲区学习

串口缓冲区辅助函数:

注意:WB_index 始终指向已写入的数据的后一个字节。

其余任务执行数据处理:

环形缓冲区设计千变万化,但是都应该有下面几种能力设计出稳定通用的中间件。

1.面向对象的程序设计能力

2.能够熟练搭建基本的数据处理框架结合外设的使用

3.自定义协议的能力

六.需要注意的点

传输数据的时候注意大小端问题

6.1 uint16_t cmduint8_t cmd[2] 的区别

1.存储结构

uint16_t cmd:连续 2 字节,代表一个 16 位整数,有高低字节区分(大小端由芯片内核决定,Cortex-M 默认小端)

uint8_t cmd[2]:两个独立 8 字节变量,仅单纯两块内存,无 “16 位数值” 语义

2.操作语义完全不同

uint16_t a = 0x1234;
uint8_t b[2] = {0x34,0x12}; // 小端下内存布局和a一致,但类型含义不一样
a = 0x5678;    // 直接整体赋值16位数
b[0] = 0x78; b[1] = 0x56; // 必须分别操作字节

只是占用内存大小相同(都是 2 字节),不能划等号。

6.2memcpy(&data_struct.sw_ver,&temp[2],2);

假设 data_struct.sw_ver 是 uint16_t 类型:

  1. 功能:从 temp 数组下标 2 开始拷贝 2 个字节,覆盖 sw_ver 的两个字节内存
  2. 内存层面等价手动写法:

((uint8_t*)&data_struct.sw_ver)[0] = temp[2];

((uint8_t*)&data_struct.sw_ver)[1] = temp[3];

6.3关键坑:大小端问题

memcpy 只搬运原始字节,不做字节交换:

  1. 芯片小端(STM32 等 Cortex-M):低地址存低字节
  2. 如果 temp [2] 是高字节、temp [3] 是低字节,拷贝后数值会颠倒,需要手动调换两个字节。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐