一、为什么还要谈 Arduino?

对于熟悉 STM32、ESP32 或裸机开发的工程师,Arduino 的抽象层似乎过于简单。但它的价值在于极低的上手成本庞大的生态。真正进阶的关键,是撕开它那层 C++ 封装,看清底层寄存器如何工作。本文以 AVR 架构的 Arduino Uno(ATmega328P)为例,先绕过官方 API 直接操作寄存器点亮 LED,再回到串口通信,展示两种风格的平衡。

二、寄存器级点灯:揭开 HAL 的底裤

Uno 板上 D13 引脚对应 PB5(PORTB bit5)。官方 digitalWrite(13, HIGH) 内部会做映射、查表、中断保护等,最终写入 PORTB。我们直接操作:

// 寄存器版 LED 闪烁,等价于 Blink 但零封装
#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    // 配置 PB5 为输出:DDRB bit5 = 1
    DDRB |= (1 << PB5);

    while (1) {
        PORTB |= (1 << PB5);   // 置高,点亮 LED
        _delay_ms(500);
        PORTB &= ~(1 << PB5);  // 置低,熄灭 LED
        _delay_ms(500);
    }
    return 0; // 不会执行
}

要点解读:

  • DDRB 是方向寄存器,PORTB 是输出数据寄存器。
  • _delay_ms 来自 AVR 工具链,利用循环计数实现粗延时,不占用定时器。
  • 此代码不依赖 Arduino.h,可直接用 avr-gcc 编译烧录,帮助你理解底层时序。

三、工程化配置:Arduino IDE 与 PlatformIO 的选择

寄存器操作代码需要纯 AVR 工具链。若在 Arduino IDE 中,可用 setup/loop 结构,但为了更贴近嵌入式工程,推荐使用 PlatformIO + Atmel AVR 环境。配置步骤如下:

  1. 安装 VS Code 和 PlatformIO 插件。
  2. 创建新项目,Board 选择 Arduino Uno,Framework 选 ArduinoAtmel AVR
  3. 若选 Arduino 框架,你依然可以在 src/main.cpp 中混合调用寄存器函数和 Arduino API。
  4. 设置烧录器为 avrdude,默认 USBasp 或板载串口均可。

这样既能享受 Arduino 库生态,又能随时用寄存器操作进行底层调试,适合有经验的开发者。

四、串口通信:收发与协议设计

串口是嵌入式调试的“心电图”。Arduino 的 Serial 对象本质是对 USART 外设的封装。从寄存器角度看,需要配置波特率、帧格式、使能收发。但更高效的方式是直接用官方库,同时理解其底层。

以下是完整的 回环测试 代码:接收上位机字符串,并原样返回,同时打印“调试”级别信息。

// 串口收发示例:支持字符串回显和 LED 控制命令
#include <Arduino.h>

const int LED_PIN = 13;
String inputBuffer = "";   // 简单缓冲区,适合短命令

void setup() {
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);
    
    // 初始化串口,波特率 115200(需与上位机一致)
    Serial.begin(115200);
    // 等待串口稳定(部分平台需要,Uno可省略)
    while (!Serial) { ; }
    Serial.println("Arduino ready. Commands: led_on / led_off / echo <text>");
}

void loop() {
    // 检查是否有完整行数据(以换行符结尾)
    if (Serial.available() > 0) {
        char c = Serial.read();
        if (c == '\n') {
            inputBuffer.trim();  // 去除回车/空格
            processCommand(inputBuffer);
            inputBuffer = "";    // 清空缓冲区
        } else {
            inputBuffer += c;
        }
    }
}

void processCommand(String cmd) {
    if (cmd == "led_on") {
        digitalWrite(LED_PIN, HIGH);
        Serial.println("LED turned ON");
    } else if (cmd == "led_off") {
        digitalWrite(LED_PIN, LOW);
        Serial.println("LED turned OFF");
    } else if (cmd.startsWith("echo ")) {
        String msg = cmd.substring(5);
        Serial.print("Echo: ");
        Serial.println(msg);
    } else {
        Serial.println("Unknown command: " + cmd);
    }
}

代码说明:

  • 使用 String 对象简化处理,但注意在长期运行下会产生堆碎片,工程中建议用定长字符数组。
  • Serial.available() 不会阻塞,非阻塞式轮询适合裸机环境。
  • 波特率 115200 是常见调试速率,若噪声大可降到 9600,但需确保双方一致。

五、进阶:串口寄存器配置与中断接收

如果对性能有要求,可跳过 Arduino 封装直接配置 USART。以下片段初始化 USART0 为 115200,8N1:

// 寄存器配置串口,仅作参考,工程中可直接用 Serial
void uart_init(unsigned int ubrr) {
    UBRR0H = (unsigned char)(ubrr >> 8);
    UBRR0L = (unsigned char)ubrr;
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);  // 使能收发
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // 8 数据位,无校验,1 停止位
}

// 计算 UBRR:F_CPU / (16 * BAUD) - 1,例如 16MHz 下 115200 对应 8

中断接收可避免轮询占用 CPU:

volatile char rx_char;
ISR(USART_RX_vect) {
    rx_char = UDR0;
    // 放入环形缓冲区
}

这进一步说明:Arduino 库只是帮你处理了这些细节,当需要精准控制时,必须下钻到寄存器层面。

六、注意事项与调试技巧

  • 电源与电平:Uno 的 I/O 为 5V,与 3.3V 设备通信时需电平转换,避免损伤。
  • 串口乱码:检查波特率、晶振频率(Uno 为 16MHz)、USB 转串口芯片驱动。
  • 烧录占用:使用 Uno 板载串口时,引脚 0(RX)和 1(TX)会被占用,若外接串口设备需先断开再烧录。
  • 内存管理:避免频繁拼接 String,容易造成堆碎片。建议用 char 数组和 sscanf 或自建解析器。
  • 看门狗:长时间运行命令循环时,可启用看门狗定时器防止死机,但需正确处理复位原因。
  • 硬件串口 vs 软件串口:当需要多个串口时,SoftwareSerial 可能不稳定,优先使用硬件串口或升级到 Mega/ESP32。

七、总结

Arduino 入门不是终点,而是起点。从寄存器点灯到串口通信,再到中断和缓冲区设计,你能逐步脱离“调库侠”的身份,理解 MCU 外设的本来面目。建议在实际项目中混合使用:应用层用 Arduino API 提高效率,底层用寄存器或中断保证实时性。这样既兼顾开发速度,又不失对硬件的掌控力。