主题
🥧 PI.C.1 — 上手完成第一台遥控车
🥧 从点亮第一颗 LED 到通过蓝牙遥控小车,共 9 节内容。
在本章结束时,你将得到一辆可以遥控的灵活电动小车,或许可以开始你们的碰碰车大赛。
📦 物资清单
| 类别 | 物品 | 数量 | 备注 |
|---|---|---|---|
| 第 1 章 | Raspberry Pi Pico 开发板 | × 1 | 主控板 |
| USB 数据线(USB-C 转 USB-A) | × 1 | 烧录与供电 | |
| 面包板 | × 1 | 搭建电路 | |
| 公对公杜邦线 | 若干 | 电路连接 | |
| LED(任意颜色) | × 1 | GPIO 输出实验 | |
| 电阻(220Ω~510Ω) | × 1 | LED 限流 | |
| 轻触按键 | × 1 | GPIO 输入实验 | |
| 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 课程的所有内容!驾驶你的小遥控车,能忍住不来一场碰碰车比赛吗?