# 引言 Arduino 的硬件串口(UART)通常只有 1-2 个,当需要连接多个串行设备(如 GPS、蓝牙、传感器)时,软件串口成为必然选择。传统软件串口(如 SoftwareSerial)依赖外部中断和位敲打(bit-banging),在高速率或高负载下容易丢失数据,且与其它中断冲突。本文介绍一种基于定时器输入捕获(Input Capture)和 DMA(Direct Memory Access)的替代方案,无需中断频繁介入,显著提升可靠性和效率。 # 原理剖析 ## 传统软件串口的痛点 - 每个字节的每一位都触发中断,CPU 频繁响应,占用率高。 - 中断延迟导致波特率误差累积,尤其在 115200bps 以上时。 - 与其它中断(如定时器、外部中断)竞争,造成数据丢失。 ## 定时器捕获与 DMA 的优势 - **输入捕获**:定时器硬件检测引脚边沿,记录精确时间戳,无需 CPU 干预。 - **DMA**:自动将捕获的时间戳搬运到内存,批量处理,减少中断次数。 - **无中断**:仅在缓冲区满或空闲时触发一次中断,CPU 负载极低。 ## 实现思路 - **接收**:利用定时器捕获引脚(如 ICP1)检测起始位下降沿,记录每个边沿时刻,通过时间间隔解码数据位。 - **发送**:使用定时器比较匹配(Output Compare)和 DMA 生成精确的位时序,无需逐位中断。 - **关键**:定时器时钟频率需远高于波特率(如 16MHz 时钟,波特率 9600 时,每个位约 1667 个时钟周期)。 # 配置步骤(以 Arduino Mega 2560 为例) ## 硬件准备 - Arduino Mega 2560(ATmega2560,16MHz) - 一个定时器(如 Timer1)用于捕获和比较 - 两个引脚:一个输入捕获引脚(Pin 48,ICP1),一个输出比较引脚(Pin 11,OC1A) ## 定时器配置 1. **设置定时器1**:选择 CTC 模式(Clear Timer on Compare Match),用于发送;同时启用输入捕获噪声抑制。 2. **配置捕获**:设置捕获边沿为下降沿(起始位),启用捕获中断(但仅在捕获完成时触发一次)。 3. **配置 DMA**:在 AVR 上,DMA 通常由外设(如 USART)支持,但 ATmega2560 无通用 DMA。因此,我们使用定时器捕获的“批量捕获”模式(通过捕获缓冲),或利用“输入捕获”的连续采样。 > 注意:ATmega2560 没有通用 DMA 控制器,但我们可以模拟 DMA 行为:使用定时器捕获中断,但每次中断只处理一个字节,且通过乒乓缓冲减少中断频率。对于有 DMA 的 STM32 等平台,可直接使用硬件 DMA。 ## 代码实现 以下代码实现一个基于 Timer1 的软件串口接收(仅接收部分,发送类似)。 ```c // 软件串口接收 - 基于定时器捕获 #include #include #define RX_PIN 48 // ICP1 (Pin 48 on Mega) #define BAUD 9600 #define F_CPU 16000000UL volatile uint8_t rx_buffer[64]; volatile uint8_t rx_head = 0, rx_tail = 0; volatile uint16_t last_capture = 0; volatile uint8_t bit_index = 0; volatile uint8_t current_byte = 0; volatile uint8_t receiving = 0; void setup_soft_uart_rx() { // 设置引脚为输入 DDRL &= ~(1 << PL0); // Pin 48 is PL0 PORTL |= (1 << PL0); // 使能上拉 // Timer1 配置 TCCR1A = 0; TCCR1B = (1 << ICNC1) | (1 << ICES1) | (1 << CS10); // 噪声抑制,上升沿捕获,无分频 TIMSK1 = (1 << ICIE1); // 使能输入捕获中断 sei(); } ISR(TIMER1_CAPT_vect) { uint16_t now = ICR1; uint16_t diff = now - last_capture; last_capture = now; if (!receiving) { // 检测到起始位(下降沿) if (diff > (F_CPU/BAUD)*0.5) { // 确保是起始位 receiving = 1; bit_index = 0; current_byte = 0; TCCR1B &= ~(1 << ICES1); // 改为下降沿捕获 } } else { // 采样数据位 bit_index++; if (bit_index >= 1 && bit_index <= 8) { // 在位的中间采样,这里简化:使用边沿间隔判断 if (diff > (F_CPU/BAUD)*0.5) { current_byte |= (1 << (bit_index-1)); } } else if (bit_index == 9) { // 停止位,忽略 receiving = 0; // 存入缓冲区 rx_buffer[rx_head] = current_byte; rx_head = (rx_head + 1) % 64; TCCR1B |= (1 << ICES1); // 恢复上升沿捕获 } } } uint8_t soft_uart_available() { return (rx_head != rx_tail); } uint8_t soft_uart_read() { while (rx_head == rx_tail); uint8_t data = rx_buffer[rx_tail]; rx_tail = (rx_tail + 1) % 64; return data; } void setup() { Serial.begin(9600); setup_soft_uart_rx(); } void loop() { if (soft_uart_available()) { Serial.write(soft_uart_read()); } } ``` ## 发送实现(简述) 发送部分使用定时器比较匹配和 DMA(或模拟): - 设置 OC1A 引脚为输出,配置 CTC 模式。 - 将待发送字节的位序列预先计算为比较值数组。 - 使用 DMA 或循环逐个设置比较值,定时器自动翻转引脚,无需中断。 # 注意事项 - **波特率误差**:定时器捕获的精度受时钟频率影响,建议使用 16MHz 晶振,并校准。 - **噪声抑制**:启用 ICNC1 可滤除短脉冲,但会增加延迟,需权衡。 - **缓冲区溢出**:如果接收数据过快,缓冲区可能溢出,需适当增大或使用 DMA 直接存储。 - **平台差异**:Arduino Uno(ATmega328P)与 Mega 的引脚和定时器不同,需调整。 - **DMA 限制**:AVR 系列无硬件 DMA,本文方法实际仍依赖中断,但频率低;若使用 STM32 等,可真正实现无中断。 # 总结 本文提出的基于定时器捕获的软件串口方案,显著减少了中断频率,提高了稳定性。虽然 AVR 上无法完全摆脱中断,但思路可迁移至 STM32 等带 DMA 的平台,实现真正的无中断串口。对于需要多串口的嵌入式项目,这是一种值得尝试的替代方案。