在嵌入式系统开发中,串口通信是最基础也最常用的外设接口之一。STM32系列微控制器提供了强大的USART/UART外设,支持多种通信模式和配置选项。作为一位有十年嵌入式开发经验的工程师,我经常使用串口进行设备调试、数据传输和固件升级等工作。
串口通信的本质是通过单根数据线(或差分线对)按位顺序传输数据。与并行通信相比,串行通信虽然传输速率较低,但具有线路简单、成本低廉、传输距离远等优势。在STM32中,USART(Universal Synchronous/Asynchronous Receiver/Transmitter)模块既支持异步模式(UART),也支持同步模式(USART),为开发者提供了灵活的通信解决方案。
提示:USART和UART的主要区别在于同步通信能力。USART可以配置为同步模式,此时需要额外的时钟信号线;而UART仅支持异步通信,依靠双方约定的波特率进行数据传输。
配置串口通信时,以下几个关键参数必须正确设置:
波特率(Baud Rate):表示每秒传输的符号数,常见的波特率有9600、115200等。波特率误差应控制在2%以内,否则可能导致通信失败。
数据位(Data Bits):每个数据帧包含的有效数据位数,通常为8位(一个字节),但也可以配置为5-9位。
停止位(Stop Bits):标志数据帧结束的位,可以是1、1.5或2位。大多数应用使用1位停止位。
校验位(Parity Bit):用于简单的错误检测,可选无校验(None)、奇校验(Odd)或偶校验(Even)。
流控制(Flow Control):管理数据传输的节奏,防止接收端缓冲区溢出,包括硬件流控(RTS/CTS)和软件流控(XON/XOFF)。
在STM32CubeMX中配置这些参数时,我通常会参考以下经验值:
在设计STM32串口硬件电路时,有几个关键点需要注意:
电平匹配:STM32的USART接口使用TTL电平(0V表示逻辑0,3.3V表示逻辑1),如果连接RS232设备(如老式计算机串口),需要使用MAX232等电平转换芯片;连接RS485设备则需要使用MAX485等差分收发器。
引脚配置:
抗干扰设计:
以下是一个典型的STM32F1系列USART1引脚配置代码示例:
c复制GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA和USART1时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
// 配置PA9为USART1_TX
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PA10为USART1_RX
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
串口通信的数据传输以帧为单位,每帧数据包含以下几个部分:
起始位(Start Bit):一个逻辑低电平,标志数据帧的开始。接收端检测到这个下降沿后,会在中间时刻采样后续数据位。
数据位(Data Bits):有效数据内容,传输顺序通常是最低有效位(LSB)在前。STM32支持5-9位数据长度,但8位最为常用。
校验位(Parity Bit):可选的错误检测位。奇校验确保数据位和校验位中"1"的总数为奇数;偶校验则确保为偶数。
停止位(Stop Bit):逻辑高电平,标志数据帧结束。停止位长度可以是1、1.5或2个位时间。
下图展示了一个典型的8N1(8位数据,无校验,1位停止位)数据帧时序:
code复制[起始位] [D0] [D1] [D2] [D3] [D4] [D5] [D6] [D7] [停止位]
0 1 0 1 1 0 0 1 1 1
在STM32中,数据帧格式通过USART_CR1和USART_CR2寄存器配置。例如,设置M位为1选择9位数据长度,PCE位为1启用校验,PS位选择奇偶校验类型。
波特率发生器是USART模块的核心部件,它根据系统时钟生成所需的波特率时钟。STM32的波特率计算公式为:
code复制波特率 = fCK / (16 * USARTDIV)
其中:
USART_BRR寄存器分为两部分:
例如,当fCK=72MHz,目标波特率=115200时:
code复制USARTDIV = 72000000 / (16 * 115200) = 39.0625
DIV_Mantissa = 39 = 0x27
DIV_Fraction = 0.0625 * 16 = 1 = 0x1
USART_BRR = (0x27 << 4) | 0x1 = 0x271
在实际项目中,我通常会使用STM32CubeMX自动计算这些值,或者使用HAL库提供的初始化函数。但理解背后的原理对于调试通信问题非常有帮助。
硬件流控通过RTS(Request To Send)和CTS(Clear To Send)信号线管理数据传输,防止接收端缓冲区溢出。其工作流程如下:
在STM32中配置硬件流控的步骤:
c复制huart1.Init.HwFlowCtl = UART_HWCONTROL_RTS_CTS;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
// 还需要配置RTS和CTS对应的GPIO引脚
GPIO_InitStruct.Pin = GPIO_PIN_12; // CTS引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_11; // RTS引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
注意:硬件流控需要通信双方都支持,并且正确连接。如果只连接TX/RX而忽略RTS/CTS,可能导致通信失败。
最基本的串口功能是单个字符的发送和接收。使用HAL库实现非常简单:
发送数据:
c复制uint8_t data = 'A';
HAL_UART_Transmit(&huart1, &data, 1, HAL_MAX_DELAY);
接收数据(轮询方式):
c复制uint8_t received;
HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, &received, 1, 1000);
if(status == HAL_OK) {
// 处理接收到的数据
}
中断方式接收:
c复制// 初始化时启用接收中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
// 中断服务函数
void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
uint8_t data = huart1.Instance->DR;
// 处理接收到的数据
}
}
在实际项目中,我建议使用中断方式而非轮询,因为轮询会阻塞CPU,影响系统实时性。下面是一个更完整的中断接收示例:
c复制#define RX_BUFFER_SIZE 128
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint16_t rx_index = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART1) {
if(rx_index < RX_BUFFER_SIZE) {
// 处理接收到的数据
process_received_data(rx_buffer[rx_index]);
rx_index++;
} else {
// 缓冲区溢出处理
handle_buffer_overflow();
rx_index = 0;
}
// 重新启用接收中断
HAL_UART_Receive_IT(&huart1, &rx_buffer[rx_index], 1);
}
}
在实际应用中,我们经常需要接收不定长度的数据帧。以下是两种常用的实现方法:
设置一个定时器,当收到第一个字符时启动定时器,如果在指定时间内没有收到新字符,则认为一帧数据接收完成。
c复制#define TIMEOUT_MS 50
uint8_t rx_data[256];
uint16_t rx_len = 0;
uint32_t last_rx_time = 0;
void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
uint8_t data = huart1.Instance->DR;
rx_data[rx_len++] = data;
last_rx_time = HAL_GetTick();
}
}
void check_rx_timeout(void) {
if(rx_len > 0 && (HAL_GetTick() - last_rx_time) > TIMEOUT_MS) {
// 处理完整帧
process_complete_frame(rx_data, rx_len);
rx_len = 0;
}
}
利用STM32 USART的空闲中断(IDLE)检测数据接收完成,这是更高效的方法。
c复制void uart1_init(void) {
// ...其他初始化代码...
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
// 正常接收数据
rx_buffer[rx_index++] = huart1.Instance->DR;
}
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 处理完整帧
process_complete_frame(rx_buffer, rx_index);
rx_index = 0;
}
}
提示:空闲中断是指总线在至少一个帧时间(10位)内没有检测到新的起始位。这种方法不需要额外的定时器,响应更快,资源占用更少。
对于高速或大数据量传输,使用DMA可以大幅降低CPU负载。STM32的USART支持TX和RX DMA请求。
DMA发送配置:
c复制// 初始化DMA
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_usart1_tx.Instance = DMA1_Channel4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
HAL_DMA_Init(&hdma_usart1_tx);
// 关联DMA到USART
__HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
// 发送数据
uint8_t data[] = "Hello, DMA!";
HAL_UART_Transmit_DMA(&huart1, data, sizeof(data));
DMA接收配置(循环模式):
c复制// 初始化DMA接收
hdma_usart1_rx.Instance = DMA1_Channel5;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart1_rx);
// 关联DMA到USART
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
// 启动DMA接收
#define DMA_RX_BUFFER_SIZE 256
uint8_t dma_rx_buffer[DMA_RX_BUFFER_SIZE];
HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, DMA_RX_BUFFER_SIZE);
// 结合空闲中断处理数据
void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 计算接收到的数据长度
uint16_t remaining = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
uint16_t received = DMA_RX_BUFFER_SIZE - remaining;
// 处理数据
process_dma_received_data(dma_rx_buffer, received);
// 重新启动DMA接收(循环模式会自动处理)
}
}
这种DMA+空闲中断的方式是我在高速数据采集项目中常用的方案,可以实现几乎零CPU开销的高速数据接收。
RS232是传统的串行通信标准,虽然在新设备中逐渐被USB取代,但在工业控制、仪器仪表等领域仍有广泛应用。
电气特性:
典型连接方式:
code复制STM32 TX ----> MAX232 ----> DB9 Pin2 (RX)
STM32 RX <---- MAX232 <---- DB9 Pin3 (TX)
STM32 GND ----------------- DB9 Pin5 (GND)
硬件设计注意事项:
RS485是工业环境中广泛使用的差分串行通信标准,具有抗干扰能力强、传输距离远、支持多点总线等优点。
电气特性:
典型半双工连接电路:
code复制STM32 USART TX ------> MAX485 DI
STM32 USART RX <----- MAX485 RO
STM32 GPIO ------> MAX485 DE/RE (发送使能)
MAX485 A <------> RS485总线A线
MAX485 B <------> RS485总线B线
硬件设计关键点:
软件实现要点:
c复制// 发送前使能发送器
void rs485_send(uint8_t *data, uint16_t len) {
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);
HAL_UART_Transmit(&huart2, data, len, 100);
while(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TC) == RESET);
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);
}
// 接收配置(常态为接收模式)
void rs485_init(void) {
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);
HAL_UART_Receive_IT(&huart2, &rx_data, 1);
}
经验分享:在RS485多节点系统中,我曾遇到因终端电阻配置不当导致的通信不稳定问题。正确的做法是只在总线最远两端安装120Ω终端电阻,中间节点不应安装。使用示波器观察总线波形是诊断此类问题的有效方法。
症状: 数据偶尔丢失或出现乱码
排查步骤:
典型案例:
在一次工业现场调试中,RS485通信在白天不稳定但夜间正常,最终发现是附近变频器干扰导致。解决方案包括:
症状: 长数据包被截断或分多次接收
解决方案:
c复制#define BUF_SIZE 256
typedef struct {
uint8_t data[BUF_SIZE];
uint16_t head;
uint16_t tail;
} ring_buffer_t;
void buf_push(ring_buffer_t *buf, uint8_t byte) {
buf->data[buf->head++] = byte;
if(buf->head >= BUF_SIZE) buf->head = 0;
}
uint8_t buf_pop(ring_buffer_t *buf) {
uint8_t byte = buf->data[buf->tail++];
if(buf->tail >= BUF_SIZE) buf->tail = 0;
return byte;
}
bool buf_is_empty(ring_buffer_t *buf) {
return buf->head == buf->tail;
}
场景: RS485总线多个从机同时响应导致冲突
解决方案:
需求: 电池供电设备需要最小化串口功耗
优化措施:
__HAL_RCC_USART1_CLK_DISABLE()c复制// 配置串口唤醒
HAL_UARTEx_EnableClockStop(&huart1);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_WUF);
常用调试工具:
调试技巧:
c复制HAL_GPIO_WritePin(DEBUG_GPIO_Port, DEBUG_Pin, GPIO_PIN_SET);
// 要测试的代码段
HAL_GPIO_WritePin(DEBUG_GPIO_Port, DEBUG_Pin, GPIO_PIN_RESET);
通过以上这些实战经验和技巧,希望能帮助开发者更高效地实现稳定可靠的STM32串口通信。在实际项目中,根据具体需求选择合适的通信方案和优化方法,是保证系统性能的关键。