1. 嵌入式开发中的串口通信基础认知
第一次接触嵌入式开发时,我被UART这个看似简单的通信方式深深吸引。记得当时用STM32F103C8T6开发板调试串口,发送"Hello World"到PC端串口助手的那一刻,那种成就感至今难忘。串口通信作为嵌入式系统中最基础也最常用的通信方式,其重要性怎么强调都不为过。
UART(Universal Asynchronous Receiver/Transmitter)是一种异步串行通信协议,它不需要时钟信号线,仅通过TX(发送)和RX(接收)两根数据线就能实现全双工通信。与I2C、SPI等同步通信协议相比,UART的连接更简单,但需要通信双方事先约定好相同的波特率。常见的波特率有9600、115200等,数值越大传输速度越快,但对硬件时序的要求也越高。
在嵌入式系统中,UART的应用场景非常广泛:
- 设备调试:通过串口打印调试信息
- 固件升级:通过串口烧录程序
- 模块通信:与GPS、蓝牙、WiFi等模块交互
- 设备控制:与传感器、执行器等外设通信
注意:虽然UART和USART经常被混用,但它们有本质区别。USART(Universal Synchronous/Asynchronous Receiver/Transmitter)既支持异步模式也支持同步模式,而UART仅支持异步模式。STM32等现代MCU通常配备的是USART外设。
2. 硬件连接与电平标准解析
2.1 常见硬件连接方案
实际项目中,嵌入式设备与PC或其他设备的UART连接通常需要电平转换芯片。最常用的方案包括:
- CH340G:低成本USB转TTL方案,广泛用于国产开发板
- CP2102:Silicon Labs出品,稳定性好,驱动兼容性强
- FT232RL:FTDI公司产品,性能稳定但价格较高
以CP2102为例,其典型连接电路如下:
code复制MCU TXD ----| |---- USB D+
MCU RXD ----| CP2102 |---- USB D-
GND ----| |---- USB GND
2.2 电平标准详解
UART通信涉及多种电平标准,混用会导致通信失败甚至损坏设备:
| 电平标准 | 电压范围 | 典型应用场景 |
|---|---|---|
| TTL | 0V-3.3V/5V | MCU之间直接通信 |
| RS232 | ±3V-±15V | 工业设备长距离通信 |
| RS485 | ±1.5V-±5V | 多设备差分通信 |
在STM32等3.3V MCU与5V设备通信时,必须注意电平兼容性问题。我曾在项目中因忽略这点导致通信不稳定,后来通过添加电平转换电路(如TXB0108)解决了问题。
3. 嵌入式UART驱动开发实战
3.1 STM32CubeMX配置指南
以STM32F103C8T6为例,使用STM32CubeMX配置UART的步骤如下:
- 在Pinout界面启用USART1,自动分配PA9(TX)和PA10(RX)
- 在Configuration选项卡设置参数:
- Baud Rate: 115200
- Word Length: 8bit
- Parity: None
- Stop Bits: 1
- Hardware Flow Control: Disable
- 生成代码时勾选生成中断和DMA选项
关键配置代码示例:
c复制/* USART1 init function */
void MX_USART1_UART_Init(void) {
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK) {
Error_Handler();
}
}
3.2 三种通信方式对比与实现
嵌入式开发中UART通信主要有三种实现方式:
- 轮询方式:
c复制HAL_UART_Transmit(&huart1, (uint8_t *)"Hello", 5, 100);
uint8_t rxData;
HAL_UART_Receive(&huart1, &rxData, 1, 100);
优点:实现简单;缺点:阻塞CPU效率低
- 中断方式:
c复制// 启动接收中断
HAL_UART_Receive_IT(&huart1, &rxBuffer, 1);
// 回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart == &huart1) {
// 处理接收数据
HAL_UART_Transmit(&huart1, &rxBuffer, 1, 100);
HAL_UART_Receive_IT(&huart1, &rxBuffer, 1); // 重新启用中断
}
}
优点:非阻塞;缺点:频繁中断影响系统性能
- DMA方式:
c复制// 配置DMA
__HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
// 启动DMA接收
HAL_UART_Receive_DMA(&huart1, rxBuffer, BUFFER_SIZE);
优点:CPU占用最低;缺点:实现复杂度高
实际项目中,我推荐对发送使用DMA,对接收使用中断,这样既能保证效率又不会丢失数据。特别是在115200及以上波特率时,纯中断方式可能导致数据丢失。
4. 常见问题排查与性能优化
4.1 典型故障排查指南
根据多年经验,UART通信常见问题包括:
-
无通信信号:
- 检查硬件连接:TX-RX交叉连接是否正确
- 测量信号线电平:确认电平标准匹配
- 验证波特率:双方设置必须完全一致
-
数据乱码:
- 检查时钟配置:HSE_VALUE是否与硬件匹配
- 验证停止位和校验位设置
- 测试不同波特率下的稳定性
-
通信不稳定:
- 添加适当的电容滤波(通常0.1μF)
- 缩短通信线长度(TTL电平建议<30cm)
- 在高速通信时考虑使用屏蔽线
4.2 性能优化技巧
- 环形缓冲区实现:
c复制#define BUF_SIZE 256
typedef struct {
uint8_t buffer[BUF_SIZE];
volatile uint32_t head;
volatile uint32_t tail;
} RingBuffer;
void RingBuffer_Put(RingBuffer *rb, uint8_t data) {
rb->buffer[rb->head++] = data;
if(rb->head >= BUF_SIZE) rb->head = 0;
}
uint8_t RingBuffer_Get(RingBuffer *rb) {
uint8_t data = rb->buffer[rb->tail++];
if(rb->tail >= BUF_SIZE) rb->tail = 0;
return data;
}
-
波特率自动检测:
通过测量起始位持续时间动态计算波特率,特别适合需要兼容多种设备的应用。 -
数据分包处理:
定义简单的通信协议解决粘包问题,例如:
code复制[HEADER][LEN][DATA][CRC]
0xAA 1 ... 1
5. 高级应用与协议设计
5.1 Modbus RTU协议实现
Modbus RTU是工业领域广泛使用的串行通信协议,基于UART实现:
c复制// Modbus RTU帧校验
uint16_t ModRTU_CRC(uint8_t *buf, int len) {
uint16_t crc = 0xFFFF;
for(int pos = 0; pos < len; pos++) {
crc ^= (uint16_t)buf[pos];
for(int i = 8; i != 0; i--) {
if((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
5.2 自定义通信协议设计要点
设计稳定可靠的UART通信协议需要考虑:
-
帧结构设计:
- 起始标志(如0xAA)
- 长度字段
- 序列号(用于重传机制)
- 数据域
- 校验字段(CRC16或CRC32)
-
流量控制:
- 硬件流控(RTS/CTS)
- 软件流控(XON/XOFF)
- 窗口机制(类似TCP)
-
超时重传:
实现简单的ACK/NACK机制,配合定时器实现超时重传。
在实际项目中,我开发过一个基于UART的固件升级协议,包含分块传输、CRC校验、断点续传等功能,稳定支持了数万台设备的远程升级。关键是要在协议简单性和可靠性之间找到平衡点。
6. 多平台UART开发对比
6.1 RT-Thread中的UART开发
RT-Thread提供了完善的串口框架,设备注册示例:
c复制static struct rt_serial_device serial;
static struct rt_uart_ops uart_ops = {
.configure = uart_configure,
.control = uart_control,
.putc = uart_putc,
.getc = uart_getc,
};
rt_err_t uart_register(void) {
serial.ops = &uart_ops;
return rt_hw_serial_register(&serial, "uart1",
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX, NULL);
}
6.2 Linux下的UART开发
Linux下通过termios库配置串口:
c复制int setup_uart(int fd, int speed) {
struct termios options;
tcgetattr(fd, &options);
cfsetispeed(&options, speed);
cfsetospeed(&options, speed);
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_oflag &= ~OPOST;
tcsetattr(fd, TCSANOW, &options);
return 0;
}
6.3 Arduino平台串口开发
Arduino简化了串口操作,但灵活性较低:
cpp复制void setup() {
Serial.begin(115200);
while(!Serial); // 等待串口就绪
}
void loop() {
if(Serial.available()) {
char c = Serial.read();
Serial.print("Received: ");
Serial.println(c);
}
}
在不同平台间移植UART代码时,最需要注意的是字节序和硬件抽象层的差异。我曾将一个Linux下的串口程序移植到RT-Thread,发现其termios配置方式完全不同,最终通过抽象出统一的配置接口解决了问题。
