跳到内容

🔵 STM.C.1 — 上手完成第一台遥控车 ​

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

← 返回


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


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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

  • 了解如何将程序编译、链接、烧录到板子上并运行,选用最官方、最现代化、最 AI 化的开发方式

  • 下载并安装 VS Code

  • 安装 STM32CubeIDE for VS Code 扩展(VS Code 扩展市场搜 STM32CubeIDE)

  • 安装 STM32CubeMX(图形化配置引脚与时钟,务必启用“Serial Wire Debug”)

    下载:STM32CubeMX 6.18.1 网盘(分享文件 SetupSTM32CubeMX-6.18.1-Win-x86_64.zip)

  • 创建 STM32F103C8T6 工程并完成初步配置

    在 main.c 的 /* USER CODE BEGIN Includes */ 处加入:

    c
    #include "chapter.c"

    在 /* USER CODE BEGIN 2 */ 处加入:

    c
    chapter_main();

    新建 Core/Src/chapter.c,本章各节的代码依次粘贴到这里:

    c
    #include "main.h"
    
    void chapter_main(void)
    {
        int a = 3;
        int b = 4;
        int c;
    
        c = a + b;
        return;
    }
  • 安装 ST-Link USB 驱动

  • 安装 STM32 烧录补丁插件 STM32 One-Click Build & Flash

    在扩展市场(Ctrl+Shift+X)搜索 STM32 One-Click Build & Flash 或 烧录 或 电导实验室,找到对应插件点击安装即可。

⚠️提醒:新手首次配置 STM32 环境较为困难,具体可能遇到以下内容CubeMX 缺少 JAVA 环境、更新卡住、生成项目失败、插件识别不到工程、ST-Link 驱动/连接异常、系统用户名含中文等 7 类常见报错。

核心知识点:

  • 开发流程:CubeMX 配置引脚/时钟 → 生成工程 → VS Code 编译(CMake + Ninja + arm-none-eabi-gcc)→ ST-Link 烧录运行

相关资源:


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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动▶ 27:58 起2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

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

核心知识点:

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

技能点:

  • 函数调用、while循环
c
//******** STM.C.1.1 ********//

// ▼ CubeMX 需配置(在STM32CubeMX 中打开.ioc工程配置文件):
// ① 时钟源:左侧 Pinout & Configuration → Categories → System Core → RCC → High Speed Clock (HSE) → 选 “Crystal/Ceramic Resonator”
// ② 调试口:Categories → System Core → SYS → Debug → 选 “Serial Wire”(否则会禁用 SWD,导致之后无法烧录,如果烧录了禁用SWD的版本,需要将boot0连接到1 然后烧录新版本进行覆盖)
// ③ 时钟树:上方 Clock Configuration 标签页 → Input frequency 填 8(MHz,外部晶振)
//    → PLL Source 选 HSE → PLL Mul 选 ×9 → System Clock Mux 选 PLLCLK(HCLK 自动显示 72MHz)
//    → APB1 Prescaler 选 /2(36MHz,APB1 上限 36MHz)→ APB2 Prescaler 选 /1(72MHz)
//    (72MHz 是最常用的主频;TIM2/TIM3 因此在 72MHz 下计数)
// ④ 引脚:芯片图上点击 PC13 → 选 GPIO_Output(推挽输出;本章不填 User Label,代码用原始 PC13)
// ⑤ 点右上角 GENERATE CODE;main() 会自动调用 SystemClock_Config() 与 MX_GPIO_Init() 完成时钟与 PC13 配置

#include "main.h"      /* HAL 类型与 GPIO_PIN_x 引脚编号都来自它 */

/*
- 函数调用:通过调用函数来执行特定的任务, main() 已调用 chapter_main() 来执行我们的内容。
*/

void chapter_main(void)
{
// (PC13 已在 main() 里由 MX_GPIO_Init() 初始化成输出,无需再写初始化代码)

    /*
    - while 循环:无限循环,程序将一直在此运行。若想退出循环并向下运行,需要 break;。
    */
    while (1)
    {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);  // 拉低 → LED 点亮(板载 LED 低电平点亮)
        HAL_Delay(1000);                                           // 亮灯持续 1000 毫秒(1 秒)
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);   // 拉高 → LED 熄灭
        HAL_Delay(1000);                                           // 灭灯持续 1000 毫秒(1 秒)
    }
}

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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动▶ 42:41 起2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

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

核心知识点:

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

连接图

参考:引脚定义图 ​

参考:电路原理图 ​

技能点:

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

// ▼ CubeMX 需配置(接续第 1 节):
// ① 引脚:芯片图上点击 PA0 → 选 GPIO_Input
// ② 下方 GPIO 面板 → PA0 的 Pull-up/Pull-down → 选 “Pull-up”(上拉:平时高电平,按下接地为低)
// ③ GENERATE CODE;PA0 初始化也会进 main() 的 MX_GPIO_Init()

#include <stdbool.h>   /* 本节起使用 bool 类型,故在本节开头引入 */

/* 宏定义:使用 #define 定义一个常量或宏,以便在代码中使用更具描述性的名称来代替具体的值。 */
#define BUTTON_PORT    GPIOA
#define BUTTON_PIN     GPIO_PIN_0      /* 按键:PA0,按下接地(读到低电平) */
#define BOARD_LED_PORT GPIOC
#define BOARD_LED_PIN  GPIO_PIN_13     /* 板载 LED:PC13,低电平点亮 */

void chapter_main(void)
{
    // (PC13、PA0 已在 main() 的 MX_GPIO_Init() 初始化:LED=输出,按键=上拉输入)

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

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

        /*
        if() 条件语句:根据括号内的条件判断是否执行大括号内的代码块,经常包含 &&(逻辑与)或 ||(逻辑或)等逻辑运算符来组合多个条件。
        */
        if (last_button_state && !cur)             // 检测到按钮从未按下变为按下的状态变化(下降沿)
        {
            /*
            static 变量:在函数内定义的 static 变量在程序周期内只初始化一次、不被销毁。但作用域仍然限制。
            */
            static bool led_on = false;            // 记录 LED 当前状态
            led_on = !led_on;                      // 切换 LED 状态
            HAL_GPIO_WritePin(BOARD_LED_PORT, BOARD_LED_PIN, led_on ? GPIO_PIN_RESET : GPIO_PIN_SET);  // 低电平点亮
        }

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

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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动▶ 1:06:12 起2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

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

核心知识点:

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

连接图:

技能点:

  • include头文件、PWM配置结构体、if-else条件语句、const常量、全局变量、边界条件控制
c
//******** STM.C.1.3 ********//
// 呼吸的光才有灵魂:PWM + TIM 定时器

//▼ CubeMX 需配置(接续第 2 节):
// ① 定时器:左侧 Categories → Timers → TIM2 → 在 TIM2 面板 Channel2 下拉 → 选 “PWM Generation CH2”
//    (芯片图 PA1 会自动变绿并复用为 TIM2_CH2)
// ② 参数:TIM2 → Parameter Settings → Prescaler = 71;Counter Period = 1000(Pulse 初始占空比可填 0)
// ③ GENERATE CODE(生成 MX_TIM2_Init 与 htim2);代码中还需 HAL_TIM_PWM_Start() 真正启动 PWM 输出

/*
PWM配置结构体:通过 pwm_config 结构体配置 PWM 的频率、分频等参数,再初始化到对应的 PWM 切片中。
(STM32 中这些参数已由 CubeMX 的 MX_TIM2_Init() 配好,代码里只需启动 PWM 并改占空比)
*/
/* 本节才用到定时器句柄 → 才声明(句柄本体由 CubeMX 在 main.c 生成) */
extern TIM_HandleTypeDef htim2;

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

void init_pwm_led(void)
{
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);      // 启动 TIM2_CH2 的 PWM 输出(PA1)
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 0);   // 初始占空比为 0(LED 关闭)
}

void chapter_main(void)
{
    init_pwm_led();                                // 启动 PWM(GPIO 由 MX_GPIO_Init、PWM 参数由 MX_TIM2_Init 完成)

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

    while (1)
    {
        static bool breathing = true;              // 新增:记录当前是否呼吸模式 注意作用域的影响
        static int brightness = 0;                 // 新增:记录当前亮度
        static bool direction = true;              // 新增:记录亮度变化方向

        bool cur = (HAL_GPIO_ReadPin(BUTTON_PORT, BUTTON_PIN) == GPIO_PIN_SET);   // 读取当前按钮状态

        if (last_button_state && !cur)             // 检测到按钮从未按下变为按下的状态变化(下降沿)
        {
            static bool led_on = false;            // 记录 LED 当前状态
            led_on = !led_on;                      // 切换 LED 状态
            HAL_GPIO_WritePin(BOARD_LED_PORT, BOARD_LED_PIN, led_on ? GPIO_PIN_RESET : GPIO_PIN_SET);

            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
        }

        __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, brightness);   // 设置 PWM 占空比以调整 LED 亮度

        last_button_state = cur;                   // 将当前按钮状态保存为上一次状态
        HAL_Delay(10);                             // 延迟
    }
}

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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动▶ 1:21:16 起2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

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

核心知识点:

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

💡提醒:USB 通信的三点说明
  1. 先分清两条线的分工:ST-Link 用来烧录程序,USB 线用来供电和通信。
  2. USB 通信是直接通过板载的 USB 接口(Type-C 或 Micro-USB)连接电脑完成的,不经过 ST-Link —— 所以只做 USB 调试打印时,不插 ST-Link 也可以,一根 USB 线就能同时供电并通信。
  3. 如果电脑没有识别到设备,把 USB 线拔掉后重新插入 STM32,通常就能被正确识别。

相关链接:

技能点:

  • 函数返回值、函数传入参数、自定义print调试输出
c
//******** STM.C.1.4 ********//

// ▼ CubeMX 需配置(接续第 3 节)——本节的打印走“USB 虚拟串口”:
// ① USB:左侧 Categories → Connectivity → USB → Mode → 选 “Device (FS)”
//    (芯片图 PA11/PA12 自动复用为 USB_DM / USB_DP)
// ② 中间件:左侧列表往下 → “Middleware and Software Packs” → USB_DEVICE → 勾选 Activate
//    → Class for FS IP → 选 “Communication Device Class (Virtual Port Com)”(即 CDC 虚拟串口)
// ③ 时钟:上方 Clock Configuration 标签页 → USB → 确保 48 MHz(72MHz 主频下自动按 /1.5 分频)
// ④ GENERATE CODE;main() 会自动调用 MX_USB_DEVICE_Init()(若 chapter_main(); 被清空需补回)

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "usbd_cdc_if.h"   /* USB 虚拟串口(CDC):本节打印用它发给电脑,故在本节引入 */

/*
函数返回值:
函数可以返回一个值,表示函数执行的结果或状态。
check_button_pressed 函数用于检测按钮是否被按下,并返回一个布尔值表示按钮状态。
*/
bool check_button_pressed(void)
{
    static bool last_button_state = true;             // 记录上一次按钮的状态,初始为未按下(高电平)
    bool cur = (HAL_GPIO_ReadPin(BUTTON_PORT, BUTTON_PIN) == GPIO_PIN_SET);  // 读取当前按钮状态

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

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

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

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

// 自定义 print():把格式化文本通过 USB 虚拟串口(CDC) 发到电脑。

void print(const char *fmt, ...)
{
    char buf[64];
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, sizeof(buf), fmt, args);                // 先把格式化结果写入 buf
    va_end(args);
    CDC_Transmit_FS((uint8_t *)buf, (uint16_t)strlen(buf)); // 通过 USB 虚拟串口发给电脑
}

void chapter_main(void)
{
    init_pwm_led();                                // 启动 PWM

    bool breathing = true;                         // 记录当前是否呼吸模式

    while (1)
    {
        if (check_button_pressed())
        {
            static bool led_on = false;            // 记录 LED 当前状态
            led_on = !led_on;                      // 切换 LED 状态
            HAL_GPIO_WritePin(BOARD_LED_PORT, BOARD_LED_PIN, led_on ? GPIO_PIN_RESET : GPIO_PIN_SET);

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

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

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

        HAL_Delay(10);                             // 延迟
    }
}

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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动▶ 1:32:28 起2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

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

核心知识点:

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

连接图:

技能点:

  • 引脚资源分配、累加取模
c
//******** STM.C.1.5 ********//

// ▼ CubeMX 需配置(接续第 4 节):
// ① 方向脚:芯片图依次点击 PB12、PB13、PB14、PB15 → 各选 GPIO_Output
//    下方 GPIO 面板 User Label(建议):PB12=IN1_A、PB13=IN2_A、PB14=IN1_B、PB15=IN2_B
//    (main.h 会生成 IN1_A_Pin/IN2_A_Pin/IN1_B_Pin/IN2_B_Pin 等宏)
// ② 定时器:左侧 Categories → Timers → TIM3 → Channel1、Channel2 都选 “PWM Generation CH1/CH2”
//    (芯片图 PA6/PA7 自动复用为 TIM3_CH1/CH2)
// ③ 参数:TIM3 → Parameter Settings → Prescaler=71;Counter Period=1000(同 TIM2)
// ④ GENERATE CODE;代码中还需 HAL_TIM_PWM_Start() 启动两路 PWM

/*
引脚资源分配:单片机的引脚支持不同的功能,因此要合理分配引脚资源,避免冲突。
这里两个电机的 4 个方向脚(PB12~PB15,输出)与 2 路 PWM(PA6/PA7 = TIM3_CH1/CH2)
已在 CubeMX 里分配好,引脚宏 IN1_A_Pin/IN2_A_Pin/IN1_B_Pin/IN2_B_Pin 由 main.h 生成。
*/
/* 本节才用到电机定时器句柄 → 才声明 */
extern TIM_HandleTypeDef htim3;

void init_motor(void)
{
    // 方向脚(GPIOB)已在 MX_GPIO_Init() 配为输出;这里只需启动两路 PWM(速度由占空比决定)
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
}

void chapter_main(void)
{
    init_motor();                                  // 启动两路 PWM(GPIO 由 MX_GPIO_Init、PWM 由 MX_TIM3_Init 完成)

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

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

        if (motor_state_step == 0)
        {
            // 前进
            HAL_GPIO_WritePin(IN1_A_GPIO_Port, IN1_A_Pin, GPIO_PIN_SET);    // 10:A 正转
            HAL_GPIO_WritePin(IN2_A_GPIO_Port, IN2_A_Pin, GPIO_PIN_RESET);
            HAL_GPIO_WritePin(IN1_B_GPIO_Port, IN1_B_Pin, GPIO_PIN_RESET);  // 01:B 反转。注意电机 B 与 A 相反
            HAL_GPIO_WritePin(IN2_B_GPIO_Port, IN2_B_Pin, GPIO_PIN_SET);
            __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);   // 50% 占空比
            __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 500);   // 50% 占空比
        }
        else if (motor_state_step == 1)
        {
            // 后退
            HAL_GPIO_WritePin(IN1_A_GPIO_Port, IN1_A_Pin, GPIO_PIN_RESET);  // 01:A 反转
            HAL_GPIO_WritePin(IN2_A_GPIO_Port, IN2_A_Pin, GPIO_PIN_SET);
            HAL_GPIO_WritePin(IN1_B_GPIO_Port, IN1_B_Pin, GPIO_PIN_SET);    // 10:B 正转。注意电机 B 与 A 相反
            HAL_GPIO_WritePin(IN2_B_GPIO_Port, IN2_B_Pin, GPIO_PIN_RESET);
            __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);   // 50% 占空比
            __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 500);   // 50% 占空比
        }
        else
        {
            // 停止
            HAL_GPIO_WritePin(IN1_A_GPIO_Port, IN1_A_Pin, GPIO_PIN_SET);    // 两脚同高 → 刹车
            HAL_GPIO_WritePin(IN2_A_GPIO_Port, IN2_A_Pin, GPIO_PIN_SET);
            HAL_GPIO_WritePin(IN1_B_GPIO_Port, IN1_B_Pin, GPIO_PIN_SET);
            HAL_GPIO_WritePin(IN2_B_GPIO_Port, IN2_B_Pin, GPIO_PIN_SET);
            __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);     // 停止电机 A
            __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);     // 停止电机 B
        }

        HAL_Delay(10);                               // 延迟
    }
}

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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动▶ 1:57:06 起2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

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

核心知识点:

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

技能点:

  • 差速转向叠加原理、速度限幅
c
//******** STM.C.1.6 ********//

#define MAX_SPEED 1000

// 第一层 API,传入电机ID与速度,直接控制电机转动
void set_motor_speed(int motor_id, int speed)
{
    // motor_id: 0 - 左轮, 1 - 右轮
    uint16_t in1, in2;            // 方向引脚掩码(值是 GPIO_PIN_xx,uint16_t)
    GPIO_TypeDef *port1, *port2;  // 方向引脚所在端口(值是 GPIOB 等,是 GPIO_TypeDef* 指针)
    uint32_t ch;                  // 定时器通道
    if (motor_id == 0)
    {
        // 左轮 ← 物理 Motor A(PB12/PB13 = IN1_A/IN2_A)
        in1 = IN1_A_Pin; 
        in2 = IN2_A_Pin; 
        port1 = IN1_A_GPIO_Port;
        port2 = IN2_A_GPIO_Port;
        ch = TIM_CHANNEL_1;
    }
    else
    {
        // 右轮 ← 物理 Motor B(PB14/PB15 = IN1_B/IN2_B)
        in1 = IN2_B_Pin; 
        in2 = IN1_B_Pin; 
        port1 = IN2_B_GPIO_Port;
        port2 = IN1_B_GPIO_Port;
        ch = TIM_CHANNEL_2;
    }

    if (speed > 0)
    {
        HAL_GPIO_WritePin(port1, in1, GPIO_PIN_SET);    // IN1/IN2 对调以纠正接线反向
        HAL_GPIO_WritePin(port2, in2, GPIO_PIN_RESET);
        __HAL_TIM_SET_COMPARE(&htim3, ch, speed);            // 正转,占空比 = speed
    }
    else if (speed < 0)
    {
        HAL_GPIO_WritePin(port1, in1, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(port2, in2, GPIO_PIN_SET);
        __HAL_TIM_SET_COMPARE(&htim3, ch, -speed);           // 反转,占空比 = -speed
    }
    else
    {
        // 停止
        HAL_GPIO_WritePin(port1, in1, GPIO_PIN_SET);
        HAL_GPIO_WritePin(port2, in2, GPIO_PIN_SET);
        __HAL_TIM_SET_COMPARE(&htim3, ch, 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);  // 控制右轮
}

void chapter_main(void)
{
    init_motor();                                  // 启动两路 PWM

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

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

        if (drive_state_step == 0)      drive( 500,  0);     // 前进
        else if (drive_state_step == 1) drive(-500,  0);     // 后退
        else if (drive_state_step == 2) drive(  0, -500);    // 左转
        else if (drive_state_step == 3) drive(  0,  500);    // 右转
        else                            drive(  0,   0);     // 停止

        HAL_Delay(10);                               // 延迟
    }
}

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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动▶ 2:09:19 起2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

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

核心知识点:

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

连接图:

技能点:

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

相关资源:

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

c
//******** STM.C.1.7 ********//

// ▼ CubeMX 需配置(接续第 6 节):
// ① 串口:左侧 Categories → Connectivity → USART1 → Mode → 选 “Asynchronous”
//    (芯片图 PA9/PA10 自动复用为 USART1_TX / USART1_RX)
// ② 参数:USART1 → Parameter Settings → Baud Rate = 115200;Word Length = 8 Bits;
//    Parity = None;Stop Bits = 1(与 HC-05 蓝牙模块默认串口参数一致)
// ③ NVIC:Categories → System Core → NVIC → 勾选 “USART1 global interrupt”
//    (本节改用中断接收,每个字节到达时自动进入 USART1_IRQHandler)
// ④ GENERATE CODE;生成 MX_USART1_UART_Init() 与 huart1(若 chapter_main(); 被清空需补回)

/* huart1(USART1) 由 CubeMX 生成,供第 7/8 节蓝牙透传使用 */
extern UART_HandleTypeDef huart1;

/* ---------- USART1 环形缓冲区的接收与发送 ----------
 * F103 串口只有一个字节的接收缓冲,不会自动放到环形缓存区中,
 * 因此要手动配置中断,每次收到新的字符自动放置到环形缓冲区中。
 * 关于中断将在 2.1 中更详细介绍,这里只大概理解即可
*/
#define UART_RX_RING 32
static volatile uint8_t uart_rx_ring[UART_RX_RING];  /* 收到的字节环形缓冲 */
static volatile uint8_t uart_rx_head = 0;            /* 中断写入位置 */
static volatile uint8_t uart_rx_tail = 0;            /* 主循环读出位置 */
volatile uint8_t uart_rx_byte = 0;                   /* 中断当前收到的字节 */

/* 收完一个字节的回调:存入环形缓冲,并立即重新挂起接收(等下一个字节) */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        uint8_t next = (uint8_t)((uart_rx_head + 1) % UART_RX_RING);
        if (next != uart_rx_tail)         /* 缓冲未满才写入(满则丢弃) */
        {
            uart_rx_ring[uart_rx_head] = uart_rx_byte;
            uart_rx_head = next;
        }
        HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart_rx_byte, 1);
    }
}

/* 环形缓冲读取接口:USART1 中断负责写入;uart_handler 与
   uart_handler_control 都从这里读字节。 */
bool uart_rx_available(void)
{
    return uart_rx_tail != uart_rx_head;
}

uint8_t uart_rx_read(void)
{
    uint8_t c = uart_rx_ring[uart_rx_tail];
    uart_rx_tail = (uint8_t)((uart_rx_tail + 1) % UART_RX_RING);
    return c;
}

/* ---------- 双向环形缓冲区代码部分结束 ---------- */

void chapter_main(void)
{
    // (USART1 已在 main() 的 MX_USART1_UART_Init() 初始化;按键 PA0 已由 MX_GPIO_Init 初始化)

    /* 开启 USART1 接收中断:每个字节一到就存入环形缓冲,主循环再从容取出 */
    HAL_NVIC_SetPriority(USART1_IRQn, 6, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
    HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart_rx_byte, 1);

    // 接收到的数据再通过 UART 发送回去,形成回显
    while (1)
    {
        /*
        环形缓冲可读判断 uart_rx_available():中断负责写入,主循环只判断有没有新字节,
        */
        while (uart_rx_available())
        {
            uint8_t c = uart_rx_read();               // 从环形缓冲取一个字符(uart_rx_read)
            HAL_UART_Transmit(&huart1, &c, 1, 100);   // 原样发回去(回显)
        }

        /*
        指针遍历字符串:通过指针 msg 遍历字符串中的每个字符,
        逐个发送直到遇到字符串结束符 \0。
        */
        if (check_button_pressed())
        {
            const char *msg = "Hello, world!\r\n";
            while (*msg)                             // 还没到结尾 \0 就继续
            {
                HAL_UART_Transmit(&huart1, (uint8_t *)msg++, 1, 100);   // 一次发一个字符
            }
        }
    }
}

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

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动▶ 2:41:40 起2:58:52

一个项目速通单片机 STM32F103C8T6 第1章:上手完成第一台遥控车|GPIO PWM UART 蓝牙 电机 状态机 TIM 定时器 串口 驱动

电导不是韩导1.0万播放

本节我们将做:

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

核心知识点:

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

技能点:

  • 状态机设计、缓冲区轮询、switch条件语句、校验和验证
c
//******** STM.C.1.8 ********//

// 协议帧:0xA5 | forward(有符号) | turn(有符号) | 校验和 | 0x5A
static enum {
    IDLE,          // 等待帧头
    GET_FORWARD,   // 已收帧头,等待 forward 字节
    GET_TURN,      // 已收 forward,等待 turn 字节
    GET_CHECKSUM,  // 已收 turn,等待校验和字节
    GET_TAIL       // 已收校验和,等待帧尾
} uart_state = IDLE;

// 解析结果:两个有符号速度量(本章写入,供主循环读取)
int16_t forward_speed = 0, turn_speed = 0;


/* 主循环:把缓冲里攒下的字节逐个喂给状态机解析 */
void uart_handler(void)
{
    while (uart_rx_available())
    {
        uint8_t c = uart_rx_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 * 4;  // 前进速度(*4 放大到 -1000~1000)
                turn_speed = byte2 * 4;     // 转向速度
            }
            uart_state = IDLE;
            break;
        }
    }
}

void chapter_main(void)
{
    // 所有 GPIO/PWM/串口初始化已在 main() 里由 CubeMX 生成的
    // MX_GPIO_Init() / MX_TIM2_Init() / MX_TIM3_Init() / MX_USART1_UART_Init() 完成,
    // 这里只需要把两路 PWM 真正“开始输出”(CubeMX 不会自动 Start)。
    init_pwm_led();                                // 启动 TIM2_CH2 呼吸灯 PWM
    init_motor();                                  // 启动 TIM3 两路电机 PWM

    /* 开启 USART1 接收中断:每个字节一到就存入环形缓冲,不再 10ms 轮询丢帧 */
    HAL_NVIC_SetPriority(USART1_IRQn, 6, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
    HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart_rx_byte, 1);

    while (1)
    {
        /*
        状态机协议解析:主循环把“中断收进缓冲”的字节逐个取出来解析(uart_drain),
        提取 forward_speed 和 turn_speed 两个控制量。
        */
        uart_handler();                              // 缓冲字节 → 状态机 → forward_speed/turn_speed

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

        update_breathing_led(true);                // 呼吸灯

        HAL_Delay(10);                             // 延迟
    }
}

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