1. UART通信基础与ARM裸机开发价值
在嵌入式系统开发中,UART(Universal Asynchronous Receiver/Transmitter)堪称最古老却永不落伍的通信接口。我十年前第一次在51单片机上调试串口时,完全没想到这个技术会在ARM Cortex-M系列芯片上继续陪伴我的职业生涯。裸机环境下操作UART,就像直接与硬件对话,能让我们真正理解数据是如何一位一位地从芯片引脚传输出去的。
为什么在ARM Cortex-M这样的现代处理器上还要学习裸机UART?原因有三:首先,UART是嵌入式系统最基础的调试手段,在没有JTAG调试器的情况下,printf调试全靠它;其次,许多工业传感器、模块仍采用UART协议通信;最重要的是,理解UART的裸机驱动原理,是掌握DMA、中断等高级特性的必经之路。以STM32F103为例,其USART外设的寄存器配置流程就体现了典型ARM外设的控制逻辑。
2. ARM芯片UART硬件架构解析
2.1 UART外设组成结构
以STM32F1系列为例,其USART(Universal Synchronous Asynchronous Receiver Transmitter)外设包含几个关键部件:
- 波特率发生器(由时钟分频得到)
- 发送移位寄存器(TDR)
- 接收移位寄存器(RDR)
- 状态寄存器(ISR)
- 数据寄存器(DR)
硬件连接上,UART通常只需要TX(发送)、RX(接收)和GND三根线。但实际开发中我发现,很多工程师容易忽略信号电平匹配问题——比如3.3V的STM32与5V的Arduino直接连接可能导致通信异常。稳妥的做法是添加电平转换芯片如MAX3232,或者至少使用电阻分压。
2.2 关键寄存器详解
配置STM32的USART1需要操作以下核心寄存器:
c复制typedef struct {
__IO uint32_t SR; // 状态寄存器
__IO uint32_t DR; // 数据寄存器
__IO uint32_t BRR; // 波特率寄存器
__IO uint32_t CR1; // 控制寄存器1
__IO uint32_t CR2; // 控制寄存器2
__IO uint32_t CR3; // 控制寄存器3
__IO uint32_t GTPR; // 保护时间和预分频寄存器
} USART_TypeDef;
其中BRR寄存器的设置最为关键,它决定了通信波特率。计算公式为:
code复制BRR = (APB时钟频率) / (16 * 期望波特率)
例如APB2时钟为72MHz,要求波特率115200时:
code复制72000000/(16*115200) = 39.0625
整数部分39写入BRR[15:4],小数部分0.0625对应1(因BRR[3:0]步进0.0625),最终BRR值为0x0271。
3. 裸机UART驱动实现全流程
3.1 初始化配置步骤
- 时钟使能:先开启USART和对应GPIO的时钟
c复制RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 |
RCC_APB2Periph_GPIOA, ENABLE);
- GPIO配置:将PA9(USART1_TX)设为复用推挽输出,PA10(USART1_RX)设为浮空输入
c复制GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
- USART参数配置:
c复制USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
- 使能USART:
c复制USART_Cmd(USART1, ENABLE);
关键细节:USART使能后需要等待至少1个比特时间才能发送数据,否则首字节可能丢失。我通常会先读取SR寄存器清除可能存在的错误标志。
3.2 数据收发实现
阻塞式发送函数:
c复制void UART_SendByte(USART_TypeDef* USARTx, uint8_t ch) {
while(!(USARTx->SR & USART_SR_TXE)); // 等待发送缓冲区空
USARTx->DR = (ch & 0xFF);
}
中断接收示例:
c复制// 中断配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 中断服务函数
void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_RXNE)) {
uint8_t data = USART_ReceiveData(USART1);
// 处理接收数据...
}
}
4. 实战经验与性能优化
4.1 波特率误差问题排查
我曾遇到一个案例:STM32与ESP8266通信时出现随机乱码。最终发现是双方波特率误差累积导致:
- STM32实际波特率:115384 (误差+0.16%)
- ESP8266实际波特率:115200 (误差0%)
虽然单个字节能正常接收,但长数据包就会出错。解决方法有:
- 调整时钟树配置,使USART时钟为115200的整数倍
- 双方改用更低的波特率如9600
- 启用UART的过采样技术(STM32支持16/8倍过采样)
4.2 DMA传输优化
对于高速数据传输,建议使用DMA减轻CPU负担:
c复制// 配置DMA发送
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = buf_len;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
// 使能USART的DMA发送
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
重要提示:DMA发送完成后需要检查TC标志位,确保所有数据已发出。我曾因忽略这点导致最后几个字节丢失。
5. 常见问题与调试技巧
5.1 硬件连接检查清单
当UART无法工作时,建议按以下顺序排查:
- 确认TX-RX交叉连接(设备A的TX接设备B的RX)
- 检查地线是否连通
- 测量TX引脚是否有信号变化(可用示波器或逻辑分析仪)
- 确认双方波特率、数据格式一致
- 检查芯片供电电压是否稳定
5.2 软件调试方法
- 回环测试:将TX短接到RX,发送数据后应立即收到相同数据
- 寄存器检查:通过调试器查看USART_SR寄存器值:
- TXE=1表示可发送新数据
- RXNE=1表示收到数据
- ORE=1表示过载错误
- 信号质量分析:用示波器检查起始位、停止位是否正常
6. 进阶应用实例
6.1 重定向printf
通过重写fputc函数,可用printf输出到UART:
c复制int fputc(int ch, FILE *f) {
while(!(USART1->SR & USART_SR_TXE));
USART1->DR = (ch & 0xFF);
return ch;
}
使用时注意:
- 在Keil中需勾选"Use MicroLIB"
- 避免在中断中调用printf
- 长字符串输出可能阻塞系统
6.2 自定义协议设计
基于UART设计简单通信协议示例:
code复制[HEADER(0xAA)][LENGTH][DATA][CHECKSUM]
实现要点:
- 使用状态机解析数据帧
- 校验和建议采用CRC8
- 超时机制处理不完整帧
在STM32上实现时,可以结合空闲中断(IDLE)检测帧结束,大幅提升协议处理效率。具体实现需要配置CR1寄存器的IDLEIE位,并在中断服务函数中读取SR寄存器的IDLE标志。