引言

在Arduino等嵌入式平台上,硬件串口资源通常只有1~2个,而实际项目(如多传感器采集、多设备联动)往往需要更多串口。软件模拟串口(SoftwareSerial)虽能扩展,但其默认实现是阻塞式的——接收数据时CPU必须持续等待,导致主循环卡顿,多路同时接收时数据丢失严重。

本文提出一种基于环形缓冲区(Ring Buffer)的非阻塞式接收方案,通过中断驱动+缓冲存储,让串口接收在后台自动完成,主循环只需定期读取缓冲数据,彻底解放CPU。

原理剖析

1. 环形缓冲区核心思想

环形缓冲区是一种固定大小的FIFO数据结构,通过头尾指针循环使用内存。写入时尾指针前进,读取时头指针前进,当指针到达末尾时回绕到起点。其优势在于:

  • 无动态内存分配,适合嵌入式环境
  • 读写操作O(1)复杂度,不涉及数据搬移
  • 天然支持生产者-消费者模型(中断写入,主循环读取)

2. 非阻塞式接收机制

传统SoftwareSerial在listen()后,通过available()read()轮询接收,但底层是忙等待。我们的方案改为:

  • 利用外部中断(如PCINT)检测引脚电平变化
  • 在中断服务程序(ISR)中采样位信号,将字节写入环形缓冲区
  • 主循环通过available()read()从缓冲区读取数据,无需等待

3. 多路复用策略

多路软件串口共享一个定时器中断(如Timer2),通过分时采样不同引脚。每路串口拥有独立的环形缓冲区和状态标志,ISR根据当前激活的串口索引进行采样,实现多路并发接收。

硬件与软件配置

硬件需求

  • Arduino Uno/Nano(ATmega328P)
  • 多个串口设备(如GPS、蓝牙模块)
  • 跳线连接:设备TX→Arduino数字引脚(如D8、D9)

软件配置步骤

  1. 定义环形缓冲区结构体
typedef struct {
    uint8_t buffer[BUFFER_SIZE];
    volatile uint8_t head;
    volatile uint8_t tail;
} RingBuffer;
  1. 初始化缓冲区
void ringBufferInit(RingBuffer *rb) {
    rb->head = 0;
    rb->tail = 0;
}
  1. 写入与读取操作
bool ringBufferWrite(RingBuffer *rb, uint8_t data) {
    uint8_t next = (rb->tail + 1) % BUFFER_SIZE;
    if (next == rb->head) return false; // 缓冲区满
    rb->buffer[rb->tail] = data;
    rb->tail = next;
    return true;
}

bool ringBufferRead(RingBuffer *rb, uint8_t *data) {
    if (rb->head == rb->tail) return false; // 空
    *data = rb->buffer[rb->head];
    rb->head = (rb->head + 1) % BUFFER_SIZE;
    return true;
}
  1. 配置定时器中断(以Timer2为例)
void setupTimer2() {
    TCCR2A = 0;
    TCCR2B = (1 << CS21); // 8分频,约2kHz采样率(根据波特率调整)
    TIMSK2 = (1 << OCIE2A);
    OCR2A = 249; // 设定比较值
}

ISR(TIMER2_COMPA_vect) {
    // 采样当前激活串口的引脚电平,进行位检测
    // 每bit采样多次,根据波特率计算采样点
}
  1. 实现多路软件串口类
class SoftwareSerialRing {
public:
    SoftwareSerialRing(uint8_t rxPin, uint8_t txPin);
    void begin(uint32_t baud);
    int available();
    int read();
    void process(); // 在ISR中调用
private:
    uint8_t _rxPin, _txPin;
    RingBuffer _rxBuffer;
    volatile uint8_t _state; // 接收状态机
    // ...
};

完整代码示例

以下是一个支持两路软件串口的完整实现(简化版):

#define BUFFER_SIZE 64
#define NUM_PORTS 2

// 环形缓冲区结构体
typedef struct {
    uint8_t buffer[BUFFER_SIZE];
    volatile uint8_t head;
    volatile uint8_t tail;
} RingBuffer;

// 软件串口对象
class SoftSerial {
public:
    SoftSerial(uint8_t rxPin) : _rxPin(rxPin) {
        ringBufferInit(&_rxBuffer);
    }
    void begin(uint32_t baud) {
        _bitTime = 1000000UL / baud; // 微秒
        pinMode(_rxPin, INPUT_PULLUP);
    }
    int available() {
        return (_rxBuffer.head != _rxBuffer.tail);
    }
    int read() {
        uint8_t data;
        if (ringBufferRead(&_rxBuffer, &data)) return data;
        return -1;
    }
    void process(uint8_t sample) {
        // 状态机:检测起始位,采样数据位,停止位
        // 此处省略详细实现,核心是每bit采样多次,根据采样值判断0/1
        // 完整代码见文末链接
    }
private:
    uint8_t _rxPin;
    RingBuffer _rxBuffer;
    uint32_t _bitTime;
    // 状态机变量
};

SoftSerial ss1(8);
SoftSerial ss2(9);

void setup() {
    Serial.begin(9600);
    ss1.begin(9600);
    ss2.begin(9600);
    setupTimer2();
}

void loop() {
    // 非阻塞读取第一路
    while (ss1.available()) {
        char c = ss1.read();
        Serial.print("Port1: ");
        Serial.println(c);
    }
    // 非阻塞读取第二路
    while (ss2.available()) {
        char c = ss2.read();
        Serial.print("Port2: ");
        Serial.println(c);
    }
    // 其他任务...
}

// 定时器中断服务程序
ISR(TIMER2_COMPA_vect) {
    static uint8_t portIndex = 0;
    // 轮流采样各端口
    uint8_t sample = digitalRead(ss1._rxPin);
    ss1.process(sample);
    sample = digitalRead(ss2._rxPin);
    ss2.process(sample);
    // 注意:实际中需根据波特率调整采样频率和轮询策略
}

注意事项

  • 缓冲区大小:根据数据流量和主循环处理速度合理设置,过小易溢出,过大浪费内存。建议64~256字节。
  • 中断优先级:定时器中断中执行时间必须极短,避免影响其他中断。采样和状态机操作应尽量精简。
  • 波特率与采样率:采样率需至少为波特率的8倍以上(如9600波特率,采样率需>76.8kHz),否则易误码。
  • 多路轮询:多路串口共享中断时,轮询频率会降低,需确保每路的采样率仍满足要求,必要时可提高定时器频率或使用更高效的采样算法。
  • 临界区保护:主循环读取缓冲区时,若中断同时写入,需暂时关闭中断或使用原子操作,防止数据竞争。
  • 引脚选择:优先选择支持PCINT的引脚,便于外部中断触发,但本方案使用定时器轮询,对引脚无特殊要求。

总结

通过环形缓冲区与定时器中断的结合,我们成功实现了Arduino上的非阻塞式多路软件串口接收。该方案不仅提升了系统响应速度,还保证了多路数据的同时接收不丢失。在实际项目中,可根据需求调整缓冲区大小、采样频率和轮询策略,以达到最佳性能。希望本文能为你解决多串口通信难题提供有力参考。