Arduino 定时器中断实现高精度软件串口:边界条件与工程实践
👁 3 阅读 · 2026-08-27 · 嵌入式
在资源受限的嵌入式系统中,软件串口(bit-banging)常被用于扩展通信接口。本文深入探讨基于Arduino定时器中断的软件串口实现,重点分析其边界条件:定时精度、中断延迟、波特率误差、CPU占用率及多实例冲突。通过原理剖析、配置步骤和完整代码示例,揭示非库实现的核心约束,帮助开发者规避常见陷阱,在极端场景下权衡性能与可靠性。
# 引言
软件串口(Software Serial)是嵌入式开发中常见的折衷方案,尤其在Arduino平台上,当硬件UART数量不足时,利用GPIO模拟时序成为刚需。然而,基于`delayMicroseconds()`的阻塞式实现无法满足高波特率或并发任务需求。本文聚焦于**定时器中断驱动**的非库实现,并系统阐述其边界条件——这些条件决定了方案的可行性与稳定性。
# 原理:定时器中断与位时序
软件串口的核心是精确控制每个位的电平持续时间。硬件UART依赖专用时钟,而软件实现需借助定时器。基本思路:
- **发送**:将字节拆分为起始位(低)、8数据位(LSB first)、停止位(高),在定时器中断服务程序(ISR)中逐位翻转GPIO。
- **接收**:检测起始位下降沿,启动定时器,在每个位中点采样电平,重组字节。
关键参数:波特率(bps)决定位周期`T_bit = 1/baud`。例如9600bps时,T_bit≈104.17μs;115200bps时≈8.68μs。定时器中断频率需为波特率的整数倍(通常8倍或16倍),以便在每位内多次采样,提高抗干扰性。
# 边界条件分析
## 1. 定时器精度与时钟源
Arduino Uno(ATmega328P)的定时器1为16位,时钟源可选内部RC或外部晶振。晶振(16MHz)精度±20ppm,但内部RC误差可达±3%,导致波特率偏差。**边界条件**:波特率误差需<2%(UART规范),否则接收端采样错位。
- 计算:实际波特率 = 定时器溢出率 / 分频系数。若配置不当,误差累积。
- 建议:使用16位定时器(如Timer1),避免8位定时器因溢出频繁而增加中断负载。
## 2. 中断延迟与抖动
ISR执行时间直接影响位时序。若ISR内包含复杂运算或函数调用,将导致位周期延长。**边界条件**:ISR总执行时间必须远小于T_bit(通常<10%)。
- 例如:115200bps时T_bit=8.68μs,ISR需在0.87μs内完成,而Arduino数字写入(`digitalWrite`)耗时约4μs,因此必须直接操作端口寄存器(如`PORTB`)。
- 中断嵌套:若其他中断(如定时器0的毫秒计时)抢占,会造成抖动。需关闭全局中断或调整优先级(AVR支持`cli()`/`sei()`,但会阻塞其他功能)。
## 3. 波特率上限与CPU占用
定时器中断频率 = 波特率 × 采样倍数。以16倍采样为例:
- 9600bps → 153.6kHz中断,每次ISR约20个周期(直接端口操作),CPU占用≈20/(1/153.6k)≈3%。
- 115200bps → 1.843MHz中断,CPU占用≈37%,且ISR需极简,否则溢出。
- **边界条件**:CPU占用率不能超过50%,否则主循环几乎无法运行。实际中,115200bps已是极限,且需优化代码。
## 4. 多实例与资源冲突
Arduino Uno仅有一个16位定时器(Timer1),若需多个软件串口,必须共享定时器。**边界条件**:
- 多通道复用:在单个ISR中轮询多个通道,但需维护各自的状态机,增加ISR复杂度。
- 冲突:若同时使用Timer1作为PWM输出,则无法用于软件串口。
- 替代:使用Timer2(8位)但精度较低,或采用外部定时器芯片。
## 5. 接收起始位检测
接收时,需在空闲高电平中检测下降沿。**边界条件**:
- 若使用外部中断(如`attachInterrupt`),中断触发延迟可能错过起始位中点。
- 更可靠:使用引脚电平变化中断(PCINT),但需在ISR中立即启动定时器,且定时器需支持单次触发模式。
# 配置步骤(以Arduino Uno + Timer1为例)
## 硬件连接
- 发送引脚:D2(作为输出)
- 接收引脚:D3(作为输入,启用内部上拉)
## 代码实现
```c
// 定时器中断软件串口 - 发送与接收(115200bps,16倍采样)
#include
#include
#define TX_PIN 2
#define RX_PIN 3
#define BAUD 115200
#define SAMPLE_RATE (BAUD * 16) // 1.8432MHz
#define TIMER_TOP (16000000 / SAMPLE_RATE - 1) // 8.68-1≈7
volatile uint8_t tx_byte, tx_bit, tx_state;
volatile uint8_t rx_byte, rx_bit, rx_state;
volatile uint8_t rx_sample;
void setup_soft_serial() {
pinMode(TX_PIN, OUTPUT);
digitalWrite(TX_PIN, HIGH); // 空闲高
pinMode(RX_PIN, INPUT_PULLUP);
// 配置Timer1为CTC模式,中断频率=SAMPLE_RATE
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS10); // CTC, 无分频
OCR1A = TIMER_TOP;
TIMSK1 = (1 << OCIE1A);
sei();
}
ISR(TIMER1_COMPA_vect) {
// 发送状态机
if (tx_state) {
if (tx_bit == 0) {
// 起始位
PORTD &= ~(1 << TX_PIN);
tx_bit = 1;
} else if (tx_bit <= 8) {
// 数据位
if (tx_byte & (1 << (tx_bit - 1))) PORTD |= (1 << TX_PIN);
else PORTD &= ~(1 << TX_PIN);
tx_bit++;
} else {
// 停止位
PORTD |= (1 << TX_PIN);
tx_state = 0;
}
}
// 接收状态机(简化:仅采样)
if (rx_state) {
rx_sample = (PIND >> RX_PIN) & 1;
if (rx_bit == 0) {
// 起始位中点
if (rx_sample == 0) {
rx_bit = 1;
} else {
rx_state = 0; // 误触发
}
} else if (rx_bit <= 8) {
rx_byte |= (rx_sample << (rx_bit - 1));
rx_bit++;
} else {
// 停止位
if (rx_sample == 1) {
// 接收完成,处理rx_byte
}
rx_state = 0;
}
}
}
void soft_serial_send(uint8_t data) {
while (tx_state); // 等待空闲
cli();
tx_byte = data;
tx_bit = 0;
tx_state = 1;
sei();
}
void setup() {
setup_soft_serial();
}
void loop() {
soft_serial_send('A');
delay(1000);
}
```
**注意**:接收起始位检测需在外部中断中启动定时器,上述代码仅为示意,实际需结合PCINT。
# 注意事项与优化建议
- **直接端口操作**:避免`digitalWrite`,使用`PORTD`等寄存器,可减少ISR时间。
- **禁用中断嵌套**:在ISR中避免调用`delay`或`Serial.print`。
- **缓冲区管理**:接收数据应存入环形缓冲区,避免丢失。
- **波特率误差测试**:用示波器测量实际波形,调整`TIMER_TOP`微调。
- **多实例设计**:若需多个串口,考虑使用状态机数组,但ISR复杂度增加,建议使用硬件UART扩展芯片。
# 结论
基于定时器中断的软件串口在Arduino上可行,但受限于定时器精度、中断延迟和CPU占用。边界条件决定了其适用场景:9600bps以下可稳定运行,115200bps需极致优化且占用大量CPU。对于要求高可靠性的项目,建议优先考虑硬件UART或外部芯片。理解这些边界,开发者方能做出合理架构决策。