跳到内容

🥧 PI.C.1 — 上手完成第一台遥控车

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

← 返回


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

📦 物资清单

类别物品数量备注
第 1 章Raspberry Pi Pico 开发板× 1主控板
USB 数据线(USB-C 转 USB-A)× 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 章新增Raspberry Pi Pico 开发板× 1遥控器端主控
MPU6050 六轴陀螺仪/加速度计× 1姿态检测
SSD1306 OLED 128×64 显示屏× 1遥控器显示
双轴模拟摇杆(含按键)× 1遥控器控制
HC-05 蓝牙模块× 1与第 1 章组成主从对
轻触按键× 4遥控器功能按键
SWD 调试器(如 Picoprobe)(可选)第 3.7 节调试用
PCB 打样(可选)第 3.6 节制作遥控器 PCB

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

本节我们将做:

  • 了解如何将程序编译链接烧录到板子上并运行,选择最官方最现代化最AI化的开发方案
  • 下载并安装 VS Code
  • 添加 Raspberry Pi Pico 开发插件
  • 下载 .PICO_SDK 文件,并测试编译
  • 通过 Zadig 安装 USB 串口驱动,并测试烧录
  • 完成 Deepseek API Key 接入 Chat聊天 工具

核心知识点:

  • SDK 编译流程:编写代码 → CMake 构建 → 生成 .uf2 → 拖入烧录

相关资源:

提示:当前及后续新的工具版本中,Pico板在正常连接电脑的情况下可直接烧录,无需再按住 BOOTSEL 重新上电。


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

本节我们将做:

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

核心知识点:

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

技能点:

  • 函数调用、while循环
c
//******** PI.C.1.1 *********//
#include <stdio.h>
#include "pico/stdlib.h"

void init_led()
{
    // 初始化 GPIO 25 引脚为输出模式
    gpio_init(25);              // 初始化 GPIO 25 引脚
    gpio_set_dir(25, GPIO_OUT); // 设置 GPIO 25 引脚为输出模式
    gpio_put(25, 0);            // 将 GPIO 25 引脚设置为低电平(LED 关闭)
    return;
}

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

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

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

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

本节我们将做:

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

核心知识点:

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

连接图

参考:引脚定义图

参考:电路原理图

技能点:

  • 宏定义、变量、flag标志位、作用域、条件语句、static变量

相关资源: PID Web Sim

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

void init_button()
{
    gpio_init(BUTTON_PIN);             // 初始化 BUTTON_PIN 引脚
    gpio_set_dir(BUTTON_PIN, GPIO_IN); // 设置 BUTTON_PIN 引脚为输入模式
    gpio_pull_up(BUTTON_PIN);          // 启用 BUTTON_PIN 引脚的上拉电阻
    return;
}

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

    // 初始化 GPIO 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 = gpio_get(BUTTON_PIN); // 读取当前按钮状态

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

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

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

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

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

本节我们将做:

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

核心知识点:

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

连接图:

技能点:

  • include头文件、PWM配置结构体、if-else条件语句、const常量、全局变量、边界条件控制
c
//******** PI.C.1.3 *********//
#include "hardware/pwm.h"

#define PWM_LED_PIN 0

void init_pwm_led()
{
    // 初始化 PWM 功能
    gpio_init(PWM_LED_PIN);                        // 初始化 PWM_LED_PIN 引脚
    gpio_set_function(PWM_LED_PIN, GPIO_FUNC_PWM); // 将 PWM_LED_PIN 引脚设置为 PWM 功能

    // 申请表 LED的申请表 = 申请表默认模板;
    pwm_config led_config = pwm_get_default_config();                // 获取默认的 PWM 配置
    pwm_config_set_clkdiv(&led_config, 125.0f);                      // 设置 PWM 时钟分频为 125.0(125Mhz/125.0=1Mhz)
    pwm_config_set_wrap(&led_config, 1000);                          // 设置 PWM 的周期为 1000(1Mhz/1000=1Khz)
    pwm_init(pwm_gpio_to_slice_num(PWM_LED_PIN), &led_config, true); // 初始化 PWM_LED_PIN 的 PWM 功能,并启用
    pwm_set_gpio_level(PWM_LED_PIN, 0);                              // 初始占空比为 0(LED 关闭)
}

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

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

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

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

    // 初始化 PWM LED
    /*
    PWM配置结构体:通过 pwm_config 结构体配置 PWM 的频率、分频等参数,再初始化到对应的 PWM 切片中。
    */
    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 = gpio_get(BUTTON_PIN); // 读取当前按钮状态

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

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

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

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

        pwm_set_gpio_level(PWM_LED_PIN, brightness); // 设置 PWM 占空比以调整 LED 亮度

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

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

本节我们将做:

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

核心知识点:

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

相关链接:

技能点:

  • 函数返回值、函数传入参数、printf调试输出
c
//******** PI.C.1.4 *********//
/*
函数返回值:
函数可以返回一个值,表示函数执行的结果或状态。
check_button_pressed 函数用于检测按钮是否被按下,并返回一个布尔值表示按钮状态。
*/
bool check_button_pressed()
{
    static bool last_button_state = true;             // 记录上一次按钮的状态,初始为未按下(高电平)
    bool current_button_state = gpio_get(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)
    {
        pwm_set_gpio_level(PWM_LED_PIN, 0); // 关闭 LED
        return -1;
    }
    else
    {
        static int brightness = 0;    // 记录当前亮度
        static bool direction = true; // 记录亮度变化方向

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

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

    // 初始化 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 状态
            gpio_put(BOARD_LED_PIN, led_on); // 根据 led_on 的值设置 BOARD_LED_PIN 引脚

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

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

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

        sleep_ms(10); // 延迟
    }
}

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

本节我们将做:

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

核心知识点:

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

连接图:

补充:对于PWM的资源冲突问题,可参考第二个Pico引脚图(PI.C.1.2)和第二个Pico引脚图中对于TIM的标注,PWM后面的同一个数字表示同一个定时器资源,同一个定时器的预分频系数和自动重载值只能一样(频率相同),A与B可设置不同的比较值(占空比不同)。

技能点:

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

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

    gpio_set_dir(MOTOR_A_IN1, GPIO_OUT);
    gpio_set_dir(MOTOR_A_IN2, GPIO_OUT);
    gpio_set_dir(MOTOR_B_IN1, GPIO_OUT);
    gpio_set_dir(MOTOR_B_IN2, GPIO_OUT);

    // 初始化 PWM 功能
    gpio_init(MOTOR_PWM_A);
    gpio_init(MOTOR_PWM_B);
    gpio_set_function(MOTOR_PWM_A, GPIO_FUNC_PWM);
    gpio_set_function(MOTOR_PWM_B, GPIO_FUNC_PWM);

    pwm_config motor_pwm_config = pwm_get_default_config();
    pwm_config_set_clkdiv(&motor_pwm_config, 12.5f); // 125Mhz/12.5=10Mhz
    pwm_config_set_wrap(&motor_pwm_config, 1000);    // 10Mhz/1000=10Khz

    pwm_init(pwm_gpio_to_slice_num(MOTOR_PWM_A), &motor_pwm_config, true);
    pwm_init(pwm_gpio_to_slice_num(MOTOR_PWM_B), &motor_pwm_config, true);

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

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

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

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

    // 初始化 PWM LED
    init_pwm_led();

    // 初始化电机控制
    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)
        {
            // 前进
            gpio_put(MOTOR_A_IN1, 1); // 10:A 正转
            gpio_put(MOTOR_A_IN2, 0);
            gpio_put(MOTOR_B_IN1, 0); // 01:B 反转。注意电机 B 的正转和反转控制引脚与电机 A 相反
            gpio_put(MOTOR_B_IN2, 1);
            pwm_set_gpio_level(MOTOR_PWM_A, 500); // 50% 占空比
            pwm_set_gpio_level(MOTOR_PWM_B, 500); // 50% 占空比

            // 呼吸 + 常亮
            update_breathing_led(true); // 启用呼吸灯
            gpio_put(BOARD_LED_PIN, 1); // 启用常亮
        }
        else if (motor_state_step == 1)
        {
            // 后退
            gpio_put(MOTOR_A_IN1, 0); // 01:A 反转
            gpio_put(MOTOR_A_IN2, 1);
            gpio_put(MOTOR_B_IN1, 1); // 10:B反转。注意电机 B 的正转和反转控制引脚与电机 A 相反
            gpio_put(MOTOR_B_IN2, 0);
            pwm_set_gpio_level(MOTOR_PWM_A, 500); // 50% 占空比
            pwm_set_gpio_level(MOTOR_PWM_B, 500); // 50% 占空比

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

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

        sleep_ms(10); // 延迟
    }
}

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

本节我们将做:

  • 封装底层电机控制逻辑,提供 set_motor_speed()drive() 高级 API
  • 实现差速转向:通过组合前进速度和转向速度控制两轮

核心知识点:

  • 多层API设计:对不同的数据抽象层提供不同的接口,使得代码更模块化、更易于维护和扩展。
  • 小车运动控制:通过组合前进速度和转向速度来实现小车的运动控制。

技能点:

  • 差速转向叠加原理、速度限幅
c
//******** PI.C.1.6 *********//
#define MAX_SPEED 1000

// 第一层 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)
    {
        gpio_put(in1, 1);  // 接线反相,IN1/IN2 对调
        gpio_put(in2, 0);
        pwm_set_gpio_level(pwm_pin, speed);
    }
    else if (speed < 0)
    {
        gpio_put(in1, 0);
        gpio_put(in2, 1);
        pwm_set_gpio_level(pwm_pin, -speed);
    }
    else
    {
        // 停止
        gpio_put(in1, 1);
        gpio_put(in2, 1);
        pwm_set_gpio_level(pwm_pin, 0);
    }
}

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

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

    /*
    速度限幅:通过检查计算后的速度值是否超过 MAX_SPEED 阈值,
    将左右轮速度限制在 [-1000, 1000] 范围内,防止电机驱动芯片过载。
    */
    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()
{
    // 初始化整块板子
    stdio_init_all();

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

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

    // 初始化电机控制
    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(500, 0); // 前进

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

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

            gpio_put(BOARD_LED_PIN, 0);
        }
        else if (drive_state_step == 2)
        {
            drive(0, -500); // 左转

            gpio_put(BOARD_LED_PIN, 1);
        }
        else if (drive_state_step == 3)
        {

            drive(0, 500); // 右转

            gpio_put(BOARD_LED_PIN, 0);
        }
        else
        {

            drive(0, 0); // 停止

            gpio_put(BOARD_LED_PIN, 0);
        }

        update_breathing_led(true); // 呼吸灯

        sleep_ms(10); // 延迟
    }
}

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

本节我们将做:

  • 配置 HC-05 蓝牙模块的 UART 通信
  • 实现 UART 回显:蓝牙发送的字符通过串口返回
  • 按键发送 "Hello, world!" 字符串

核心知识点:

  • 串口通信:通过 UART 协议进行数据传输。
  • FIFO缓冲区:UART 使用环形缓冲区来管理数据的接收和发送。
  • HC-05 蓝牙模块:常用的蓝牙串口模块,可以通过 UART 与单片机通信。

连接图:

技能点:

  • uart_is_readable/writable可读/可写判断、指针遍历字符串

相关资源:

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

c
//******** PI.C.1.7 *********//
#define UART_ID uart0
#define BAUD_RATE 115200
#define UART_TX_PIN 12
#define UART_RX_PIN 13

void init_uart()
{
    uart_init(UART_ID, BAUD_RATE);                  // 初始化 UART0,设置波特率为 115200
    gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART); // 将 GPIO 12 设置为 UART 功能(TX)
    gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART); // 将 GPIO 13 设置为 UART 功能(RX)
}

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

    // 初始化 UART 串口通信
    init_uart();

	init_button();

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

            /*
            uart_is_writable可写判断:当 UART 发送缓冲区有空间时,返回 true,否则返回 false。
            */
            if (uart_is_writable(UART_ID))
            {
                uart_putc(UART_ID, c); // 将接收到的字符通过 UART 发送回去
            }
        }

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

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

本节我们将做:

  • 整合所有功能,通过蓝牙接收控制指令控制小车
  • 使用状态机解析 5 字节通信协议

核心知识点:

  • 状态机:通过定义不同的状态来管理程序的流程和逻辑,使得程序更清晰、更易于维护。
  • 通信协议设计:设计一个简单的协议来解析从蓝牙模块接收到的控制指令。

技能点:

  • 状态机设计、缓冲区轮询、switch条件语句、校验和验证
c
//******** PI.C.1.8 *********//
static enum {
    IDLE,
    GET_FORWARD,
    GET_TURN,
    GET_CHECKSUM,
    GET_TAIL
} uart_state = IDLE;

// 解析字符

static int16_t forward_speed = 0, turn_speed = 0;

void uart_handler()
{

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

        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 * 4; // 前进速度
                turn_speed = byte2 * 4;    // 转向速度
            }
            uart_state = IDLE;
            break;
        }
    }
}

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

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

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

    // 初始化 PWM LED
    init_pwm_led();

    // 初始化电机控制
    init_motor();

    // 初始化 UART 串口通信
    init_uart();

    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);

        sleep_ms(10);
    }
}

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