RS ==Recommend Standard ==推荐标准;
232==标识号,第232号;
时间:1962年
地点:美国
人物:美国电子工业协会 == Electronic Industries Association ==(美国)电子工业协会
事件:发布了一个串行通信的物理接口结合逻辑电平的规范文件,就是这个232号文件。
现在关于串口通讯的叫法太多,什么RS232通讯、串口通讯、DB9通讯、UART通讯等等,其实这些称呼都跟这个额232号文件有些关系,随便怎么叫吧,理解就行。
串行通讯,从字面意思理解就行,就是把数据串成一串发出去,比如一个字节8位,高位先发出去,那么发送顺序就是bit7–bit6–bit5–bit4–bit3–bit2–bit1–bit0。
就某种单片机来说,比如STM32F103,它有好几个UART口,俗称串口,但是它的引脚高电平是3.3V,低电平是0V,正好满足TTL的电平标准,2.4V–5V表示逻辑1,0V–0.4V表示逻辑0。
但是电压低了传输距离就比较短,为了传输远点,就把TTL电平转换成RS232电平,通过某种电平转换芯片,比如MAX232。RS232电平逻辑是-3V到-15V表示逻辑1,3V到15V表示逻辑0。这相当于把电平扩大,但是也只能传输十几米。
人们为了传输更远,就用上了差分传输,比如RS422和RS485,用两根线的电压差来表示逻辑1和逻辑0,两根线的压差为2V至6V表示逻辑1,两线的压差为-2V至-6V表示逻辑0,这样就能传输1000米以上。
数据在一根线上传输,那什么时候是开始,什么时候是结束,每位数据的宽度是多少、数据有没有传输错误。那就需要约定一下,不是谁都像孙悟空一样有觉悟,头上敲三下就是让他凌晨3点过来,还是要讲清楚点好。
线顶一个空闲的状态,就是没传输数据的时候,传输线的电平逻辑是1,用单片机TTL的电平标准就是3.3V,高电平;
开始:发出1位逻辑0电平
结束:发出1位逻辑1电平
数据宽度:要定义每一位的宽度是多少,不然你发两位1我却认为是1位1,怎么办,发送和接收的双发要统一度量衡,才不会有误解。这个数据宽度就是用波特率的约定。
数据有没有传错:那就把收到的数据大家数一数,算一算,我给你发100块钱,我还告诉你是100张一块的,那你收到之后,要数一数,是不是100张,是不是100块,都对了,那就表示是我给你的。
传输一个字节数据的示意图

STM32F103单片机USART内部结构图

STM32单片机的RS232串口通讯可以通过轮询、中断和DMA三种方式实现,以下是每种方式的工作流程详解:
发送流程
1、初始化
配置USART时钟、TX和RX的GPIO引脚、波特率、数据位、停止位等,现在可以使用STM32CubeMX配置后直接生成配置代码。初始化代码如下所示:
使能USART模块;
// 在main()中调用初始化函数
MX_USART1_UART_Init(); // 由CubeMX生成
2、发送数据
检查状态寄存器USART_SR中的TXE(发送缓冲器空)标志位;
若TXE=1,向USART_DR寄存器写入数据;
重复上述步骤直到所有数据发送完成;
使用HAL库可以通过下面的代码实现:关键函数HAL_UART_Transmit()
uint8_t tx_data[] = "Hello World!";
HAL_UART_Transmit(&huart1, tx_data, sizeof(tx_data), HAL_MAX_DELAY); // 阻塞发送
参数说明:
&huart1:UART句柄(如huart1,huart2等);
tx_data:发送缓冲区指针;
sizeof_(tx_data):数据长度;
HAL_MAX_DELAY:无限阻塞等待发送完成,当然这个阻塞时间也可以自行设置一个短一点的时间。
接收流程
1、初始化:同发送
2、接收数据
检查状态寄存器USART_SR中的RXNE(接收缓冲区非空)标志位;
若RXNE=1,从USART_DR寄存器读取数据;
使用HAL库可以通过下面的代码实现:关键函数HAL_UART_Receive()
uint8_t rx_data[10];
HAL_UART_Receive(&huart1, rx_data, 10, HAL_MAX_DELAY); // 阻塞接收10字节
轮询方式的特点
轮询方式需要CPU主动检查状态标志位,效率低。
优点:实现简单
缺点:CPU 长时间阻塞,无法处理其他任务;
轮询方式缺点太明显,工程上要谨慎使用,最好别用!
发送流程
1、初始化
配置USART(与轮询方式差不多);
使能发送中断(USART_CR1中的TXEIE或者TCIE);
在NVIC中配置USART中断优先级;
2、发送数据
写入第一个数据到USART_DR;
后续数据在中断服务程序(ISR)中处理:
TXE中断:自动触发当发送缓冲区空,在ISR中写入下一个数据;
TC中断:发送完成时触发,用于关闭中断或通知主程序;
使用HAL库可以通过下面的代码实现:关键函数HAL_UART_Transmit_IT()
uint8_t tx_data[] = "Interrupt Mode";
HAL_UART_Transmit_IT(&huart1, tx_data, sizeof(tx_data)); // 非阻塞发送
函数触发后,HAL库自动管理中断,数据逐个发送
3、中断回调处理:数据发送完之后触发HAL_UART_TxCpltCallback回调函数:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
}
接收流程
1、初始化
配置USART(与轮询方式差不多);
使能接收中断(USART_CR1中的RXNEIE)
2、接收数据
当数据到达是触发RXNE中断,在ISR中读取USART_DR
使用HAL库可以通过下面的代码实现:关键函数HAL_UART_Receive_IT()
uint8_t rx_buffer[100];
HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 启动中断接收
3、中断回调处理:每接收一个字节触发HAL_UART_RxCpltCallback(需要再回调函数中重新开机接收中断,否则是接受多个字节之后才触发回调函数)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
}
HAL库中断接收的底层机制
1、中断服务函数 USARTx_IRQHandler
HAL库通过USARTx_IRQHandler处理中断;
每次中断会检查RXNE标志,读取数据并存入缓冲区,同时更新接收状态;
2、接收计数器管理
当调用HAL_UART_Receive_IT时,记录目标缓冲区地址和剩余接收长度;
每次中断将剩余长度减1,直到为0时触发回调;
重难点理解
中断接收函数默认是可以接受多个字节的,字节个数是Size个,函数原型如下
/**
* @brief Receives an amount of data in non blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the received data is handled as a set of u16. In this case, Size must indicate the number
* of u16 available through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be received.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
/* Process Locked */
__HAL_LOCK(huart);
/* Set Reception type to Standard reception */
huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;
return(UART_Start_Receive_IT(huart, pData, Size));
}
else
{
return HAL_BUSY;
}
}
我们以接收10个字节为例
1、初始化代码,以中断的方式开始接收10个字节的数据
uint8_t rx_buffer[10];
HAL_UART_Receive_IT(&huart1, rx_buffer, 10); // 启动接收10字节
2、中断触发逻辑
当USART接收到1个字节数据时,硬件触发接收缓冲区非空RXNE中断;
进入HAL库的USART中断服务函数USARTx_IRQHandler;
HAL库自动读取USART->DR寄存器,将数据存入rx_buffer,并减少剩余接收计数器;
每接收1个字节触发一次中断,HAL库的默认行为是在所有字节接收完成之后触发1次完成回调。
所以上面的代码来接收数据,硬件中断10次,但是回调函数只执行一次;
3、回调函数的执行
当10个字节全部接收完成之后,HAL库才调用HAL_UART_RxCpltCallback回调函数;
用户可以在回调函数中处理完成的数据包;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
}
一次性接收多个字节,等数据全部接收之后才执行一次回调函数,虽然用起来很方便但是会产生一个风险,比如由于某些原因,10个数据只接受了9个,那就会一直等第十个10数据的出现,如果不使用超时管理,软件系统也会出错一直死等。
在需要实时处理或者协议解析的时候,需要每次中断都进行一次回调函数,判断命令协议,此时需要使用单字节接收,如下所示:
1、初始代码
uint8_t rx_byte;
HAL_UART_Receive_IT(&huart1, &rx_byte, 1); // 启动单字节接收
2、回调函数中处理数据并重启接收
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
}
对比多字节数据接收和单字节数据接收

发送流程
1、初始化
驱启动USART模块和GPIO;
启动DMA通道,配置方向;
发送方向:Memory → Peripheral(USART->DR)
接收方向:Peripheral → Memory(USART->DR)
2、启动发送
uint8_t tx_data[] = "DMA Mode";
HAL_UART_Transmit_DMA(&huart1, tx_data, sizeof(tx_data)); // 启动DMA发送
3、回调处理
发送完成时触发HAL_UART_TxCpltCallback
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
}
接收流程
1、初始化
2、启动接收,核心函数 HAL_UART_Receive_DMA
uint8_t rx_buffer[200];
HAL_UART_Receive_DMA(&huart1, rx_buffer, 200); // 启动DMA接收
3、回调处理
接收完成时触发HAL_UART_RxCpltCallback
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
}

1、硬件配置:需要正确配置GPIO、时钟、波特率、校验位等;
2、错误处理:检查USART_SR中的错误标志(如溢出错误ORE);
3、回调函数:中断和DMA方式都依赖回调函数
4、错误处理:通过HAL_UART_ErrorCallback处理通信错误
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
}
5、DMA优化:合理设置DMA缓冲区大小和中断优先级。
可以使用双缓冲区HAL_UARTEx_ReceiveToIdle_DMA处理不定长数据;
避免在DMA传输中修改缓冲数据。
处理不定长串口数据是实际项目中常见的需求,如Modbus、自定义协议等。
不定长数据接收的核心挑战
1、数据长度位置:无法预先设定;
2、数据完整性判断:需明确数据包结束标志(如空闲时间、特定字符、超时等);
3、高效性要求:避免CPU轮询,优先使用硬件特性,如IDLE中断、DMA;
常用的解决方案有:
原理 :USART在数据中线空闲时触发中断,配合DMA自动板运数据。
实现步骤
Step1:CubeMX配置
1、启用USART和DMA,接收方向:Peripheral→Memory
2、启用USART的空闲中断(IDLE Interrupt)
在USARTx的NVIC设置中勾选USARTx global interrupt;
代码中手动开启IDLE中断:
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 在初始化后添加
Step2:启动DMA接收
// 启动DMA循环接收(缓冲区需足够大)
uint8_t rx_buffer[256];
HAL_UART_Receive_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
Step3:处理空闲中断
// 在USART中断服务函数中检测IDLE标志
void USART1_IRQHandler(void)
HAL_UART_IRQHandler(&huart1); // 调用HAL库默认处理
}
原理:通过定时器检测两次接受数据的间隔时间,超过阈值则认为数据包结束。
实现步骤
Step1:启动中断接收
uint8_t rx_buffer[256];
uint16_t rx_index = 0;
HAL_UART_Receive_IT(&huart1, &rx_buffer[rx_index], 1); // 单字节接收
Step2:接收中断中重置定时器
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
// 重置超时定时器(如TIM6)
__HAL_TIM_SET_COUNTER(&htim6, 0);
HAL_TIM_Base_Start_IT(&htim6);
// 继续接收下一字节
HAL_UART_Receive_IT(&huart1, &rx_buffer[rx_index], 1);
}
}
Step3:定时器超时中断处理
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
}
原理:每接收1个字节触发中断,手动管理缓冲区
实现步骤
Step1:启动单字节接收
uint8_t rx_byte;
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
Step2:在回调中填充缓冲区
#define MAX_BUF_SIZE 256
uint8_t rx_buffer[MAX_BUF_SIZE];
uint16_t rx_index = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
} else {
rx_index = 0; // 缓冲区溢出处理
}
// 重启接收
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
HAL库提供HAL_UARTEx_ReceiveToIdle_XXX系列函数,简化不定长接收:
使用 HAL_UARTEx_ReceiveToIdle_DMA
可以自动处理IDLE中断和DMA传输;
支持超时和IDLE双触发条件;
// 启动接收,直到IDLE或超时
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
// 接收完成回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
}
前面的代码还存在问题与对应的解决方案

代码优化(IDLE + DMA双缓冲区)
// 定义双缓冲区
uint8_t rx_buf1[256], rx_buf2[256];
volatile bool buf1_ready = false, buf2_ready = false;
uint16_t data_length = 0;
// 启动首次接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf1, sizeof(rx_buf1));
// 接收事件回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) else {
data_length = Size;
buf2_ready = true;
// 切换回缓冲区1
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf1, sizeof(rx_buf1));
}
}
}
// 主循环中处理数据
while (1)
if (buf2_ready) {
process_data(rx_buf2, data_length);
buf2_ready = false;
}
}

1、清除中断标志:在IDLE中断中必须调用__HAL_UART_CLEAR_IDLEFLAG,否则会持续触发;
2、DMA循环模式:若使用循环DMA,需确保缓冲区足够大以避免数据覆盖;
3、线程安全:在中断和主程序间传递数据时,需使用标志位或临界区保护;
4、协议解析:在process_data中需实现数据校验(如CRC)和协议解析。







