跳到内容

🔴 ARD.C.1 — 上手完成第一台遥控车

🔴 从点亮第一颗 LED 到通过蓝牙遥控小车,共 9 节内容。

← 返回


在本章结束时,你将得到一辆可以遥控的灵活电动小车,或许可以开始你们的碰碰车大赛。

📦 物资清单

类别物品数量备注
第 1 章Arduino Uno R3 开发板× 1主控板
USB 数据线(USB-A 转 USB-B)× 1烧录与供电
面包板× 1搭建电路
公对公杜邦线若干电路连接
LED(任意颜色)× 1GPIO 输出实验
电阻(220Ω~510Ω)× 1LED 限流
轻触按键× 1GPIO 输入实验
TB6612FNG 电机驱动模块× 1驱动直流电机
带编码器的 TT 马达× 2左右轮动力 + 测速
两节 18650 电池 + 电池盒× 1小车供电
5V DC-DC 降压模块(MP1584 等)× 1降压稳压
HC-05 蓝牙模块× 1无线遥控通信
公对母杜邦线若干模块连接
第 2 章新增HC-SR04 超声波测距模块× 1避障测距
SG90(或类似)舵机× 1雷达扫描
第 3 章新增Arduino Uno R3 开发板× 1遥控器端主控
MPU6050 六轴陀螺仪/加速度计× 1姿态检测
SSD1306 OLED 128×64 显示屏× 1遥控器显示
双轴模拟摇杆(含按键)× 1遥控器控制
HC-05 蓝牙模块× 1与第 1 章组成主从对
轻触按键× 4遥控器功能按键
PCB 打样(可选)第 3.6 节制作遥控器 PCB

ARD.C.1.0 — 万事开头难:项目介绍与环境搭建

本节我们将做:

  • 了解如何将程序编译链接烧录到板子上并运行,选择最官方最现代化最AI化的开发方案
  • 下载并安装 VS Code
  • 安装 PlatformIO 插件
  • 测试编译与烧录
  • 完成 Deepseek API Key 接入 Chat聊天 工具

核心知识点:

  • 开发流程:编写代码 → 编译 → 烧录 → 运行

相关资源:


ARD.C.1.1 — 从一束光开始:GPIO 推挽输出

本节我们将做:

  • 编写第一个 Arduino 程序,点亮板载 LED(D13)
  • 控制 LED 以 1 秒间隔闪烁

核心知识点:

  • 硬件外设、寄存器:CPU执行程序时,会与硬件外设的寄存器交流。
  • GPIO通用输入输出-推挽输出:控制该引脚连接到GND或VCC。

技能点:

  • 函数调用、while循环
cpp
//******** ARD.C.1.1 *********//

#include <Arduino.h>

void init_led()
{
    // 初始化 GPIO 13 引脚为输出模式
    pinMode(13, OUTPUT);              // 初始化 GPIO 13 引脚
    
    digitalWrite(13, LOW);            // 将 GPIO 13 引脚设置为低电平(LED 关闭)
    return;
}

int main()
{
    // 初始化整块板子
    init();

    /*
    - 函数调用:通过调用函数来执行特定的任务,这里我们定义了一个函数来初始化 GPIO 13 引脚。
    */
    init_led();

    /*
    - while 循环:无限循环,程序将一直在此运行。若想退出循环并向下运行,需要break;。
    */
    while (true)
    {
        digitalWrite(13, HIGH); // 将 GPIO 13 引脚设置为高电平(LED 点亮)
        delay(1000);            // 亮灯持续 1000 毫秒(1 秒)
        digitalWrite(13, LOW);  // 将 GPIO 13 引脚设置为低电平(LED 关闭)
        delay(1000);            // 灭灯持续 1000 毫秒(1 秒)
    }
}

ARD.C.1.2 — 相信光,成为光:GPIO 上拉输入

本节我们将做:

  • 连接按键到 D4,通过按键控制 LED 的开关
  • 每次按下按键切换 LED 状态

核心知识点:

  • 引脚复用:一个引脚可以连接至不同的硬件外设,通过切换实现不同的功能。
  • GPIO通用输入输出-上拉输入:可用检测该引脚是否通过按键连接到了 GND。
  • 上升沿和下降沿:当电平出现从低到高的变化或从高到低的变化时,执行相应的操作。
  • 面包板与杜邦线:快速搭建电路的工具,便于实验和调试。

连接图

参考:引脚定义图

参考:电路原理图

技能点:

  • 宏定义、变量、flag标志位、作用域、条件语句、static变量
cpp
//******** ARD.C.1.2 *********//

/*
宏定义:使用 #define 定义一个常量或宏,以便在代码中使用更具描述性的名称来代替具体的值。
*/
#define BUTTON_PIN 4
#define BOARD_LED_PIN 13

void init_button()
{
    pinMode(BUTTON_PIN, INPUT_PULLUP);      // 初始化 BUTTON_PIN 引脚 启用 BUTTON_PIN 引脚的上拉电阻

    return;
}

int main()
{
    // 初始化整块板子
    init();

    // 初始化 BOARD_LED_PIN 引脚为输出模式
    init_led();

    // 初始化 BUTTON_PIN 引脚为输入模式,并启用上拉电阻
    init_button();

    /*
    变量:在程序中用于存储数据的容器,这里使用 bool 类型的变量 last_button_state 来记录上一次按钮的状态。
    */
    /*
    flag 标志位: 用于记录系统的某项状态
    */
    bool last_button_state = true; // 记录上一次按钮的状态,初始为未按下(高电平)

    while (true)
    {

        /*
        作用域:
        在 while 循环内定义的变量 current_button_state 只在循环体内有效,每次循环都会重新定义和赋值。
        而 last_button_state 定义在循环外部,因此它的值在整个程序运行期间都保持不变,直到被更新为当前按钮状态。
        */
        bool current_button_state = digitalRead(BUTTON_PIN); // 读取当前按钮状态

        //

        /*
        if() 条件语句:根据括号内的条件判断是否执行大括号内的代码块,经常包含&&(逻辑与)或||(逻辑或)等逻辑运算符来组合多个条件。
        */

        if (last_button_state && !current_button_state)
        {
            // 检测到按钮从未按下变为按下的状态变化(下降沿)

            /*
            static 变量:在函数内定义的 static 变量在程序周期内只初始化一次、不被销毁。但作用域仍然限制。
            */
            static bool led_on = false;      // 记录 LED 当前状态
            led_on = !led_on;                // 切换 LED 状态
            digitalWrite(BOARD_LED_PIN, led_on); // 根据 led_on 的值设置 BOARD_LED_PIN 引脚
        }

        last_button_state = current_button_state; // 将当前按钮状态保存为上一次状态,以便下一次循环比较
        delay(10);                             // 延迟
    }
}

ARD.C.1.3 — 呼吸的光才有灵魂:PWM 与 TIM 定时器

本节我们将做:

  • 使用 PWM 实现 LED 呼吸灯效果(亮度从暗到亮再到暗循环)
  • 通过按键切换呼吸 / 常亮模式

核心知识点:

  • PWM脉宽调制:通过快速改变高低电平,用高低电平的比例模拟输出的强度。

连接图:

技能点:

  • include头文件、PWM配置、if-else条件语句、const常量、全局变量、边界条件控制
cpp
//******** ARD.C.1.3 *********//

#define PWM_LED_PIN 10   // pin11 与 MsTimer2 共用 Timer2 冲突,改到 pin10(OC1B/Timer1)

void init_pwm_led()
{
    // 初始化 PWM 功能
    pinMode(PWM_LED_PIN, OUTPUT);                        // 初始化 PWM_LED_PIN 引脚
    // Arduino analogWrite 使用内置 PWM
    // PWM_LED_PIN=10 使用 Timer1/OC1B,与 Servo(pin9/OC1A) 共用 Timer1
    // Timer1 默认配置:prescaler=64, phase-correct PWM, TOP=255
    // Phase-correct PWM:计数器 0→255→0 为一个周期,共 512 tick
    // PWM 频率 = 16MHz / 64 / 512 ≈ 488Hz
    
    analogWrite(PWM_LED_PIN, 0);                          // 初始占空比为 0(LED 关闭)
}

/*
const变量:在函数内定义的 const 变量在程序周期内值不可修改,且只初始化一次。适用于常量值的定义。
全局变量:在函数外部定义的变量,在整个程序中都可以访问和修改。
*/
const int brightness_step = 2; // 亮度变化步长(8位 PWM 0~255)

int main()
{
    // 初始化整块板子
    init();

    // 初始化 BOARD_LED_PIN 引脚为输出模式
    init_led();

    // 初始化 BUTTON_PIN 引脚为输入模式,并启用上拉电阻
    init_button();

    // 初始化 PWM LED
    /*
    PWM:通过 analogWrite 输出 PWM 信号控制 LED 亮度。
    */
    init_pwm_led();

    bool last_button_state = true; // 记录上一次按钮的状态,初始为未按下(高电平)

    while (true)
    {
        static bool breathing = true; // 新增:记录当前呼吸状态
        static int brightness = 0;    // 记录当前亮度
        static bool direction = true; // 记录亮度变化方向

        bool current_button_state = digitalRead(BUTTON_PIN); // 读取当前按钮状态

        if (last_button_state && !current_button_state)
        {
            // 检测到按钮从未按下变为按下的状态变化(下降沿)

            static bool led_on = false;      // 记录 LED 当前状态
            led_on = !led_on;                // 切换 LED 状态
            digitalWrite(BOARD_LED_PIN, led_on); // 根据 led_on 的值设置 BOARD_LED_PIN 引脚

            breathing = !breathing; // 新增:切换呼吸模式状态
        }

        /*
        if-else 条件语句:根据括号内的条件判断执行不同的代码块。
        */
        if (breathing)
        {
            if (direction)
            {
                brightness += brightness_step; // 增加亮度
                /*
                边界条件控制:当亮度值达到上限(255)时停止增加并翻转方向,实现亮度从暗到亮再到暗的循环变化。
                */
                if (brightness >= 255)
                { // 达到最大亮度
                    brightness = 255;
                    direction = false; // 切换为减少亮度
                }
            }
            else
            {
                brightness -= brightness_step; // 减少亮度
                /*
                边界条件控制:当亮度值达到下限(0)时停止减少并翻转方向,形成完整的呼吸周期。
                */
               if (brightness <= 0)
                { // 达到最小亮度
                    brightness = 0;
                    direction = true; // 切换为增加亮度
                }
            }
        }
        else
        {
            brightness = 0; // 关闭 LED
        }

        analogWrite(PWM_LED_PIN, brightness); // 设置 PWM 占空比以调整 LED 亮度(Arduino 范围 0-255)

        last_button_state = current_button_state; // 将当前按钮状态保存为上一次状态,以便下一次循环比较
        delay(10);                             // 延迟
    }
}

ARD.C.1.4 — 才第四节就这么复杂了?:参数传入返回与 USB 通信

本节我们将做:

  • 将之前的逻辑封装成函数,提高代码复用性和可读性
  • 实现 check_button_pressed()update_breathing_led() 两个新函数

核心知识点:

  • 函数定义与调用:将代码封装成函数,提高代码的复用性和可读性。
  • Serial调试输出:通过在代码中添加打印语句,将变量值和程序状态输出到串口进行调试和分析。
  • USB通信:通过 USB 将数据发送到电脑进行调试和分析。

提示:淘宝上大多数 17~30 块的 Arduino Uno R3 克隆板使用的是 CH340 或 CP2102 作为 USB 转串口芯片,Windows 不自带这两者的驱动,需要额外安装。如果连接板子后设备管理器中没有出现 COM 口,请根据芯片型号下载对应驱动:CH340 驱动下载 / CP210x 驱动下载

相关链接:

技能点:

  • 函数返回值、函数传入参数、Serial调试输出
cpp
//******** ARD.C.1.4 *********//
/*
函数返回值:
函数可以返回一个值,表示函数执行的结果或状态。
check_button_pressed 函数用于检测按钮是否被按下,并返回一个布尔值表示按钮状态。
*/
bool check_button_pressed()
{
    static bool last_button_state = true;             // 记录上一次按钮的状态,初始为未按下(高电平)
    bool current_button_state = digitalRead(BUTTON_PIN); // 读取当前按钮状态

    if (last_button_state && !current_button_state)
    {
        last_button_state = current_button_state; // 更新 last_button_state
        return true;                              // 检测到按钮按下,返回 true
    }

    last_button_state = current_button_state; // 更新 last_button_state
    return false;                             // 没有检测到按钮按下,返回 false
}

/*
函数传入参数:
函数可以接受参数,这些参数在函数内部使用来执行特定的操作。
update_breathing_led 函数接受一个布尔参数 breathing,表示当前是否处于呼吸模式,根据该参数来更新 LED 的状态。
*/
int update_breathing_led(bool breathing)
{
    if (!breathing)
    {
        analogWrite(PWM_LED_PIN, 0); // 关闭 LED
        return -1;
    }
    else
    {
        static int brightness = 0;    // 记录当前亮度
        static bool direction = true; // 记录亮度变化方向

        if (direction)
        {
            brightness += brightness_step; // 增加亮度
            if (brightness >= 255)
            { // 达到最大亮度
                brightness = 255;
                direction = false; // 切换为减少亮度
            }
        }
        else
        {
            brightness -= brightness_step; // 减少亮度
            if (brightness <= 0)
            { // 达到最小亮度
                brightness = 0;
                direction = true; // 切换为增加亮度
            }
        }
        analogWrite(PWM_LED_PIN, brightness); // 设置 PWM 占空比以调整 LED 亮度(Arduino 范围 0-255)
        return brightness; // 返回当前亮度值
    }
}

int main()
{
    // 初始化整块板子
    init();

    // 初始化串口通信,设置波特率为 115200
    Serial.begin(115200);

    // 初始化 BOARD_LED_PIN 引脚为输出模式
    init_led();

    // 初始化 BUTTON_PIN 引脚为输入模式,并启用上拉电阻
    init_button();

    // 初始化 PWM LED
    init_pwm_led();

    bool breathing = true; // 记录当前呼吸状态

    while (true)
    {

        if (check_button_pressed())
        {
            static bool led_on = false;      // 记录 LED 当前状态
            led_on = !led_on;                // 切换 LED 状态
            digitalWrite(BOARD_LED_PIN, led_on); // 根据 led_on 的值设置 BOARD_LED_PIN 引脚

            breathing = !breathing; // 切换呼吸模式状态
        }

        int now_brightness = update_breathing_led(breathing); // 更新呼吸灯状态

        /*
        printf() 函数:用于将信息输出到串口进行调试和分析,这里我们可以输出当前的亮度值来观察呼吸灯的变化。
        */
        Serial.print("Brightness: "); Serial.println(now_brightness); // 打印当前亮度值到串口进行调试

        delay(10); // 延迟
    }
}

ARD.C.1.5 — 我要用 GPIO 和 PWM 干票大的:TB6612 与电机控制

本节我们将做:

  • 使用 TB6612FNG 电机驱动芯片驱动两个直流电机
  • 通过按键循环切换前进 → 后退 → 停止

核心知识点:

  • 直流电机原理:通过改变电机两端的电压极性来控制转动方向。
  • 电机驱动芯片:TB6612FNG 是一个常用的电机驱动芯片,可以控制两个直流电机的转动方向和速度。
  • 电压与共地:务必!务必!务必!共地连接以保证电路的正常工作。

连接图:

技能点:

  • 引脚资源分配、累加取模
cpp
//******** ARD.C.1.5 *********//
//引脚资源分配:单片机的引脚支持不同的功能,因此要合理分配引脚资源,避免冲突。这里我们为两个电机的控制引脚和 PWM 引脚进行了定义。
#define MOTOR_A_IN1 7
#define MOTOR_A_IN2 8
#define MOTOR_B_IN1 A0
#define MOTOR_B_IN2 A1
#define MOTOR_PWM_A 5
#define MOTOR_PWM_B 6

void init_motor()
{
    // 初始化电机控制引脚
    pinMode(MOTOR_A_IN1, OUTPUT);
    pinMode(MOTOR_A_IN2, OUTPUT);
    pinMode(MOTOR_B_IN1, OUTPUT);
    pinMode(MOTOR_B_IN2, OUTPUT);

    // 初始化 PWM 功能(Arduino analogWrite 自动配置 PWM 引脚)
    pinMode(MOTOR_PWM_A, OUTPUT);
    pinMode(MOTOR_PWM_B, OUTPUT);

    // Arduino analogWrite 使用内置 PWM,无需手动配置分频和周期

    analogWrite(MOTOR_PWM_A, 0); // 初始占空比为 0(电机停止)
    analogWrite(MOTOR_PWM_B, 0); // 初始占空比为 0(电机停止)
}

int main()
{
    // 初始化整块板子
    init();

    // 初始化 BOARD_LED_PIN 引脚为输出模式
    init_led();

    // 初始化 BUTTON_PIN 引脚为输入模式,并启用上拉电阻
    init_button();

    // 初始化 PWM LED
    init_pwm_led();

    // 初始化电机控制
    /*
    引脚资源分配:单片机的引脚支持不同的功能,因此要合理分配引脚资源,避免冲突。
    Arduino Uno 的 PWM 引脚为 3,5,6,9,10,11,我们将电机 PWM 分配在 5,6。
    */
    init_motor();

    int motor_state_step = 0; // 0: 前进, 1: 后退, 2: 停止

    while (true)
    {

        if (check_button_pressed())
        {
            /*
            累加去模:通过对 motor_state_step 进行累加并取模来循环切换电机状态,实现前进、后退和停止的功能。
            */
            motor_state_step = (motor_state_step + 1) % 3; // 循环切换状态
        }

        if (motor_state_step == 0)
        {
            // 前进
            digitalWrite(MOTOR_A_IN1, HIGH);
            digitalWrite(MOTOR_A_IN2, LOW);
            digitalWrite(MOTOR_B_IN1, LOW);
            digitalWrite(MOTOR_B_IN2, HIGH);
            analogWrite(MOTOR_PWM_A, 125); // ~50% 占空比(Arduino 范围 0-255)
            analogWrite(MOTOR_PWM_B, 125); // ~50% 占空比

            // 呼吸 + 常亮
            update_breathing_led(true); // 启用呼吸灯
            digitalWrite(BOARD_LED_PIN, HIGH); // 启用常亮
        }
        else if (motor_state_step == 1)
        {
            // 后退
            digitalWrite(MOTOR_A_IN1, LOW);
            digitalWrite(MOTOR_A_IN2, HIGH);
            digitalWrite(MOTOR_B_IN1, HIGH);
            digitalWrite(MOTOR_B_IN2, LOW);
            analogWrite(MOTOR_PWM_A, 125); // ~50% 占空比
            analogWrite(MOTOR_PWM_B, 125); // ~50% 占空比

            // 呼吸 + 关闭
            update_breathing_led(true); // 启用呼吸灯
            digitalWrite(BOARD_LED_PIN, LOW); // 关闭常亮
        }
        else
        {
            // 停止
            digitalWrite(MOTOR_A_IN1, HIGH);
            digitalWrite(MOTOR_A_IN2, HIGH);
            digitalWrite(MOTOR_B_IN1, HIGH);
            digitalWrite(MOTOR_B_IN2, HIGH);
            analogWrite(MOTOR_PWM_A, 0); // 停止电机 A
            analogWrite(MOTOR_PWM_B, 0); // 停止电机 B

            // 关闭呼吸灯和常亮
            update_breathing_led(false); // 关闭呼吸灯
            digitalWrite(BOARD_LED_PIN, LOW);  // 关闭常亮
        }

        delay(10); // 延迟
    }
}

ARD.C.1.6 — 恭喜你,你已经会做多级 API 了:多层 API 设计

本节我们将做:

  • 封装底层电机控制逻辑,提供 set_motor_speed()drive() 高级 API
  • 实现差速转向

核心知识点:

  • 多层API设计:通过分层封装实现代码的模块化和复用。
  • 小车运动控制:差速转向 = 前进/后退速度 + 左转/右转速度。

技能点:

  • 差速转向叠加原理、速度限幅
cpp
//******** ARD.C.1.6 *********//
#define MAX_SPEED 255

// 第一层 API,传入电机ID与速度,直接控制电机转动
void set_motor_speed(int motor_id, int speed)
{
    // motor_id: 0 - 左轮, 1 - 右轮
    int in1, in2, pwm_pin;
    if (motor_id == 0)
    {
        // 左轮 ← 物理 Motor A 的引脚
        in1 = MOTOR_A_IN1;
        in2 = MOTOR_A_IN2;
        pwm_pin = MOTOR_PWM_A;
    }
    else
    {
        // 右轮 ← 物理 Motor B 的引脚
        in1 = MOTOR_B_IN2;
        in2 = MOTOR_B_IN1;
        pwm_pin = MOTOR_PWM_B;
    }

    if (speed > 0)
    {
        digitalWrite(in1, HIGH);  // 接线反相,IN1/IN2 对调
        digitalWrite(in2, LOW);
        analogWrite(pwm_pin, speed);
    }
    else if (speed < 0)
    {
        digitalWrite(in1, LOW);
        digitalWrite(in2, HIGH);
        analogWrite(pwm_pin, -speed);
    }
    else
    {
        // 停止
        digitalWrite(in1, HIGH);
        digitalWrite(in2, HIGH);
        analogWrite(pwm_pin, 0);
    }
}

// 第二层 API,传入正向速度和转向速度,控制两轮速度实现差速转向
void drive(int forward_speed, int turn_speed)
{
    // forward_speed: -255 到 255,负数表示后退,正数表示前进
    // turn_speed: -255 到 255,负数表示左转,正数表示右转

    int left_motor_speed = forward_speed + turn_speed;  // 左轮速度 = 前进速度 + 转向速度
    int right_motor_speed = forward_speed - turn_speed; // 右轮速度 = 前进速度 - 转向速度

    /*
    速度限幅:通过检查计算后的速度值是否超过 MAX_SPEED 阈值,
    将左右轮速度限制在 [-255, 255] 范围内,防止电机驱动芯片过载。
    */
    if (left_motor_speed > MAX_SPEED)
        left_motor_speed = MAX_SPEED;
    if (left_motor_speed < -MAX_SPEED)
        left_motor_speed = -MAX_SPEED;
    if (right_motor_speed > MAX_SPEED)
        right_motor_speed = MAX_SPEED;
    if (right_motor_speed < -MAX_SPEED)
        right_motor_speed = -MAX_SPEED;

    set_motor_speed(0, left_motor_speed);  // 控制左轮
    set_motor_speed(1, right_motor_speed); // 控制右轮
}

int main()
{
    // 初始化整块板子
    init();

    // 初始化 BOARD_LED_PIN 引脚为输出模式
    init_led();

    // 初始化 BUTTON_PIN 引脚为输入模式,并启用上拉电阻
    init_button();

    // 初始化电机控制
    /*
    引脚资源分配:单片机的引脚支持不同的功能,因此要合理分配引脚资源,避免冲突。
    Arduino Uno 的 PWM 引脚为 3,5,6,9,10,11,我们将电机 PWM 分配在 5,6。
    */
    init_motor();

    int drive_state_step = 0; // 0: 前进, 1: 后退, 2: 左转, 3: 右转, 4: 停止

    while (true)
    {

        if (check_button_pressed())
        {
            drive_state_step = (drive_state_step + 1) % 5; // 循环切换状态
        }

        if (drive_state_step == 0)
        {

            drive(150, 0); // 前进

            digitalWrite(BOARD_LED_PIN, HIGH);
        }
        else if (drive_state_step == 1)
        {

            drive(-150, 0); // 后退

            digitalWrite(BOARD_LED_PIN, LOW);
        }
        else if (drive_state_step == 2)
        {
            drive(0, -150); // 左转

            digitalWrite(BOARD_LED_PIN, HIGH);
        }
        else if (drive_state_step == 3)
        {

            drive(0, 150); // 右转

            digitalWrite(BOARD_LED_PIN, LOW);
        }
        else
        {

            drive(0, 0); // 停止

            digitalWrite(BOARD_LED_PIN, LOW);
        }

        update_breathing_led(true); // 呼吸灯

        delay(10); // 延迟
    }
}

ARD.C.1.7 — 追着车跑一定很累吧:UART 串口与蓝牙模块

本节我们将做:

  • 配置 HC-05 蓝牙模块的 UART 通信
  • 通过蓝牙遥控器发送指令控制小车

核心知识点:

  • UART串口通信:一种异步串行通信协议,通过 TX/RX 两根数据线实现双向通信。
  • HC-05蓝牙模块:将 UART 有线信号转换为蓝牙无线信号。

连接图:

技能点:

  • Serial.available 可读判断、Serial.write/Seria.read、指针遍历字符串

相关资源:

使用外部 USB 转串口模块(如 CH340 / CP2102)时,Windows 不自带驱动,需要额外安装。请根据模块芯片型号下载对应驱动:

cpp
//******** ARD.C.1.7 *********//

int main()
{
    init(); // 初始化 Arduino 定时器(millis/delay/analogWrite 需要)

    // 初始化 UART 串口通信
    Serial.begin(115200);
    init_button();

    // 接收到的数据再通过 UART 发送回去,形成回显
    while (true)
    {
        /*
        Serial.available 可读判断:当串口接收缓冲区有数据时,返回 true,否则返回 false。
        */
        if (Serial.available())
        {
            int c = Serial.read(); // 从串口接收一个字符

            /*
            Serial.write:将数据发送到串口。
            */
            Serial.write(c); // 将接收到的字符通过串口发送回去
        }

        /*
        指针遍历字符串:通过指针 msg 遍历字符串中的每个字符,
        逐个发送直到遇到字符串结束符 \0。
        */
        if (check_button_pressed())
        {
            const char *msg = "Hello, world!\r\n";
            while (*msg)
            {
                Serial.write(*msg++); // 发送字符串
            }
        }
    }
}

ARD.C.1.8 — 遥控车!启动!:状态机通信协议

本节我们将做:

  • 使用状态机解析蓝牙控制指令
  • 通过差速转向控制小车运动

核心知识点:

  • 状态机设计:通过 IDLE → GET_FORWARD → GET_TURN → GET_CHECKSUM → GET_TAIL 五个状态解析协议帧。
  • 校验和验证:通过 (byte1 + byte2) & 0xFF 计算校验和,确保数据传输的准确性。

技能点:

  • 枚举状态机、缓冲区轮询、switch条件语句、校验和验证
cpp
//******** ARD.C.1.8 *********//
typedef enum {
    IDLE,
    GET_FORWARD,
    GET_TURN,
    GET_CHECKSUM,
    GET_TAIL
} uart_state_t;

static uart_state_t uart_state = IDLE;

// 解析字符

static int16_t forward_speed = 0, turn_speed = 0;

void uart_handler()
{

    /*
    缓冲区轮询:使用 while 循环一次性处理完当前缓冲区的所有字符,防止因主循环延迟导致的数据堆积。
    */
    while (Serial.available())
    {
        uint8_t c = (uint8_t)Serial.read();

        static int8_t byte1 = 0, byte2 = 0;

        /*
        switch 条件语句:根据 uart_state 的不同值执行不同的代码块,实现状态机的逻辑。
        */
        switch (uart_state)
        {
        case IDLE:
            if (c == 0xA5)
            {
                uart_state = GET_FORWARD;
                byte1 = 0;
                byte2 = 0;
            }
            break;
        case GET_FORWARD:
            byte1 = (int8_t)c; // 转换为有符号,支持负数(后退)
            uart_state = GET_TURN;
            break;
        case GET_TURN:
            byte2 = (int8_t)c;
            uart_state = GET_CHECKSUM;
            break;
        case GET_CHECKSUM:
            /*
            校验和验证:通过 (byte1 + byte2) & 0xFF 计算校验和,
            与接收到的校验字节比较,确保数据传输的准确性。
            */
            if (((byte1 + byte2) & 0xFF) == c)
            {
                uart_state = GET_TAIL; // 校验通过,等待帧尾
            }
            else
            {
                uart_state = IDLE; // 校验失败,复位
            }
            break;
        case GET_TAIL:
            if (c == 0x5A) // 帧尾校验
            {
                forward_speed = byte1 * 2; // 前进速度
                turn_speed = byte2 * 2;    // 转向速度
            }
            uart_state = IDLE;
            break;
        }
    }
}

int main()
{
    init(); // 初始化 Arduino 定时器(millis/delay/analogWrite 需要)

    // 初始化 UART 串口通信
    Serial.begin(115200);
    
    // 初始化各模块
    init_led();
    init_button();
    init_pwm_led();
    init_motor();

    while (true)
    {
        /*
        状态机协议解析:通过 uart_handler 函数中定义的状态机,逐字节解析蓝牙接收到的控制指令,
        提取 forward_speed 和 turn_speed 两个控制量。
        */
        uart_handler(); // UART → byte1, byte2 → forward_speed, turn_speed

        /*
        控制指令应用:将解析出的 forward_speed 和 turn_speed 传入 drive 函数,
        drive 内部通过差速转向算法控制左右轮电机,实现小车运动。
        */
        drive(forward_speed, turn_speed); // 根据解析的指令控制小车运动

        update_breathing_led(true);

        delay(10);
    }
}

恭喜你,你已经完成了 ARD.C.1 课程的所有内容!驾驶你的小遥控车,能忍住不来一场碰碰车比赛吗?