Arduino 软件串口波特率自适应:基于脉冲宽度测量的动态校准法

一、为什么需要波特率自适应?

在嵌入式系统中,软件串口(如 Arduino 的 SoftwareSerial)依赖定时器精确计时来采样 RX 引脚。然而,实际应用中常遇到以下问题:

  • 外部晶振精度不足(如 ±1% 误差)导致波特率偏差;
  • 温度变化引起时钟漂移;
  • 使用内部 RC 振荡器时误差更大(可达 ±5%)。

传统固定波特率配置下,若实际波特率与配置值偏差超过 2%,通信将出现误码。动态校准法通过测量接收信号的脉冲宽度,实时估算实际波特率,并调整采样时刻,从而显著提高通信可靠性。

二、原理:脉冲宽度测量与波特率估算

1. 串口帧结构

UART 帧包含:起始位(低电平)、数据位(8 位)、可选校验位、停止位(高电平)。起始位和停止位提供了明确的电平跳变,可用于测量位时间。

2. 校准思路

  • 当 RX 引脚检测到下降沿(起始位开始),启动定时器;
  • 测量起始位低电平持续时间(即一个位时间 T_bit);
  • 实际波特率 = 1 / T_bit;
  • 根据 T_bit 计算每个数据位的采样点(通常取位中间,即 0.5 * T_bit);
  • 后续数据位按此采样点进行读取。

3. 动态更新

为避免单次测量误差,可对多个帧的 T_bit 进行滑动平均滤波,并持续更新采样点。

三、硬件与软件配置

1. 硬件连接

  • Arduino Uno(ATmega328P)
  • 外部设备(如 GPS 模块)连接至 D2(RX)和 D3(TX)
  • 注意:软件串口仅支持 5V 逻辑电平,若外部设备为 3.3V,需电平转换。

2. 软件配置

  • 使用 Arduino IDE,开发板选择 "Arduino/Genuino Uno"
  • 无需额外库,直接使用寄存器操作或标准库函数

四、完整代码示例

以下代码实现基于脉冲宽度测量的动态波特率校准,并回显接收到的数据。

// 软件串口自适应波特率示例
// 引脚定义
#define RX_PIN 2
#define TX_PIN 3

// 全局变量
volatile unsigned long start_time = 0;
volatile unsigned long bit_time = 0; // 单位:微秒
volatile bool frame_started = false;

// 软件串口对象(使用标准库)
#include <SoftwareSerial.h>
SoftwareSerial mySerial(RX_PIN, TX_PIN); // 初始波特率任意,后续校准

// 中断服务函数:检测下降沿并测量起始位宽度
void rxInterrupt() {
  if (!frame_started) {
    // 下降沿触发,记录起始时间
    start_time = micros();
    frame_started = true;
  } else {
    // 上升沿触发,计算位时间
    unsigned long end_time = micros();
    bit_time = end_time - start_time;
    frame_started = false;
  }
}

// 校准函数:根据测量的位时间设置采样参数
void calibrateBaudRate() {
  if (bit_time > 0) {
    // 计算实际波特率
    long actual_baud = 1000000L / bit_time;
    Serial.print("校准波特率: ");
    Serial.println(actual_baud);
    
    // 重新配置软件串口(注意:SoftwareSerial 不支持动态改波特率,需重新初始化)
    // 这里采用手动采样方式,因此仅需记录 bit_time
  }
}

// 手动采样函数:根据 bit_time 读取一个字节
uint8_t readByteManual() {
  uint8_t data = 0;
  // 等待起始位(低电平)
  while (digitalRead(RX_PIN) == HIGH);
  // 跳过起始位(半个位时间)
  delayMicroseconds(bit_time / 2);
  
  // 读取8个数据位
  for (int i = 0; i < 8; i++) {
    delayMicroseconds(bit_time); // 等待到下一个位中心
    data |= (digitalRead(RX_PIN) << i);
  }
  // 跳过停止位(可选)
  delayMicroseconds(bit_time);
  return data;
}

void setup() {
  Serial.begin(9600); // 调试串口
  pinMode(RX_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(RX_PIN), rxInterrupt, CHANGE);
  
  // 等待第一个帧进行校准
  Serial.println("等待数据帧校准...");
  while (bit_time == 0) {
    // 空转等待
  }
  calibrateBaudRate();
  
  // 校准完成后,关闭中断,使用手动采样
  detachInterrupt(digitalPinToInterrupt(RX_PIN));
  Serial.println("校准完成,开始接收数据");
}

void loop() {
  if (bit_time > 0) {
    // 检查是否有数据(通过起始位检测)
    if (digitalRead(RX_PIN) == LOW) {
      uint8_t received = readByteManual();
      Serial.print("收到: 0x");
      Serial.println(received, HEX);
    }
  }
}

五、代码说明

  • 中断测量:使用 attachInterrupt 检测 RX 引脚的电平变化,测量起始位低电平持续时间。
  • 手动采样:校准后,通过 readByteManual 函数按位采样,采样点位于位中心,避免边沿抖动。
  • 动态更新:可在每次接收后重新测量位时间,并更新 bit_time,实现持续校准。

六、注意事项

  1. 中断优先级:测量起始位时,中断服务函数应尽量简短,避免影响其他中断。
  2. 定时器精度micros() 精度为 4 微秒(16MHz 主频),对于高波特率(如 115200)位时间约 8.68 微秒,误差较大,建议用于 9600 及以下波特率。
  3. 噪声抑制:可在 RX 引脚加 RC 滤波或施密特触发器,减少毛刺干扰。
  4. 多帧校准:建议连续测量多个帧取平均值,提高稳定性。
  5. 兼容性:此方法适用于半双工通信,若需全双工,需考虑 TX 和 RX 同时工作时的资源冲突。

七、总结

本文提出的脉冲宽度测量动态校准法,无需额外硬件,仅通过软件测量起始位宽度即可实现波特率自适应,有效解决了时钟漂移导致的通信失败问题。该方法适用于对成本敏感、波特率较低(≤57600)的嵌入式应用,如传感器数据采集、GPS 模块通信等。开发者可根据实际需求扩展滑动平均滤波、自动重校准等机制,进一步提升可靠性。