Rust+STM32F103

stm32f1xx-hal 示例学习笔记(详细版)

来源: https://github.com/stm32-rs/stm32f1xx-hal/tree/master/examples

整理日期: 2026-05-26

目标板: LCKFB DKX STM32F103C8T6(立创开发板)


你的开发板信息

项目 规格
芯片 STM32F103C8T6(ARM Cortex-M3)
最大主频 72MHz
Flash 64KB
SRAM 20KB
外部晶振 8MHz (HSE)
LED PC13(低电平点亮)
USB PA11(USB DM), PA12(USB DP)
CAN PA11(CAN RX), PA12(CAN TX)
USART1 PA9(TX), PA10(RX) 或 PA15(TX), PB3(RX) 等
USART2 PA2(TX), PA3(RX)
USART3 PB10(TX), PB11(RX)
SPI1 PA5(SCK), PA6(MISO), PA7(MOSI)
I2C1 PB6(SCL), PB7(SDA)
ADC 通道 PA0-PA7, PB0-PB1
定时器 TIM1(高级), TIM2-TIM4(通用)
对应 feature stm32f103(中等密度)

Cargo.toml 中应该配置:

toml 复制代码
[dependencies.stm32f1xx-hal]
version = "0.10"
features = ["stm32f103", "rt"]

目录

  1. 项目模板与开发环境
  2. 时钟系统详解
  3. 入门基础
  4. [GPIO 输入输出](#GPIO 输入输出)
    • [gpio_input.rs - 按键控制LED](#gpio_input.rs - 按键控制LED)
    • [dynamic_gpio.rs - 动态GPIO切换](#dynamic_gpio.rs - 动态GPIO切换)
    • [nojtag.rs - 释放JTAG引脚](#nojtag.rs - 释放JTAG引脚)
  5. [外部中断 EXTI](#外部中断 EXTI)
  6. 定时器与中断
  7. 串口通信
  8. [ADC 模数转换](#ADC 模数转换)
  9. [SPI 通信](#SPI 通信)
  10. [I2C 通信](#I2C 通信)
  11. [PWM 输出与输入](#PWM 输出与输入)
    • [pwm.rs - PWM输出](#pwm.rs - PWM输出)
    • [pwm_input.rs - PWM输入捕获](#pwm_input.rs - PWM输入捕获)
    • [qei.rs - 正交编码器接口](#qei.rs - 正交编码器接口)
  12. [DAC 数模转换](#DAC 数模转换)
  13. [CRC 校验](#CRC 校验)
  14. [CAN 总线](#CAN 总线)
  15. [USB 串口](#USB 串口)
    • [usb_serial.rs - USB轮询串口](#usb_serial.rs - USB轮询串口)
    • [usb_serial_interrupt.rs - USB中断串口](#usb_serial_interrupt.rs - USB中断串口)
  16. 附录:常用概念总结

1. 项目模板与开发环境

Cargo.toml 完整模板

toml 复制代码
# [package] 段:定义包的元数据信息
[package]
# 包的名称,用于在 crates.io 上发布或被其他项目引用时标识
name = "stm32dome"
# 包的版本号,遵循语义化版本规范(Semantic Versioning)
version = "0.1.0"
# 使用的 Rust 版本约定,2024 是最新的版本,启用了最新的语言特性
edition = "2024"

# [dependencies] 段:定义项目的依赖项
[dependencies]
# embedded-hal:嵌入式硬件抽象层标准接口
# 版本 "1.0" 表示使用 1.0.x 的最新版本
# 定义了 GPIO、I2C、SPI、串口等通用 trait,使得驱动代码可在不同 MCU 间移植
embedded-hal = "1.0"

# nb:non-blocking(非阻塞)操作库
# 提供 Result 和 block! 宏,用于处理可能需要重试的操作(如串口发送)
nb = "1"

# cortex-m:ARM Cortex-M 处理器的底层支持库
# 提供系统寄存器访问、中断管理、系统定时器等功能
cortex-m = "0.7.7"

# cortex-m-rt:Cortex-M 运行时库
# 提供启动代码、中断向量表、内存初始化等运行时支持
# 有了它才能使用 #[entry] 宏定义程序入口点
cortex-m-rt = "0.7.5"

# panic-halt:panic 处理程序
# 当程序发生不可恢复的错误时,将 CPU 置于停止状态
# 其他替代方案包括 panic-semihosting、panic-itm 等
panic-halt = "1.0.0"
rtt-target = "0.6.2"

# [dependencies.stm32f1xx-hal]:特定依赖的详细配置
# 使用表格语法为 stm32f1xx-hal 提供更详细的配置
[dependencies.stm32f1xx-hal]
# 指定版本号
version = "0.11.0"
# features:启用编译时特性
# "stm32f103":选择 STM32F103 系列芯片的支持代码
# "medium":中等密度芯片配置(64-128KB Flash),C8T6 属于此类
# 其他选项还有 "low"(低密度,16-32KB)和 "high"(高密度,256KB+)
features = ["stm32f103", "medium"]


# [profile.dev] 段:开发模式的编译优化配置
# dev 模式用于开发调试,平衡编译速度和调试体验
[profile.dev]
# incremental = false:关闭增量编译
# 增量编译可以加快重新编译速度,但在嵌入式开发中可能导致问题
# 关闭后每次都会完整编译,确保构建的一致性
incremental = false

# codegen-units = 1:将整个 crate 作为单个代码生成单元
# 默认是 16,减少到 1 可以让编译器进行更激进的优化
# 但会增加编译时间,在嵌入式开发中常用此设置
codegen-units = 1

# panic = "abort":发生 panic 时直接终止程序
# 默认是 "unwind"(展开栈),但嵌入式环境通常不支持栈展开
# 设置为 abort 可以减少代码大小
panic = "abort"


# [profile.release] 段:发布模式的编译优化配置
# release 模式用于最终部署,优化代码大小和运行速度
[profile.release]
# codegen-units = 1:同样使用单个代码生成单元
# 在发布版本中,这可以让链接时优化(LTO)效果更好
codegen-units = 1

# debug = true:在发布版本中保留调试信息
# 默认是 false,设为 true 可以让你在发布版本中也能进行调试
# 不会影响代码大小或性能,只会增加编译产物的体积
debug = true

# lto = true:启用链接时优化(Link Time Optimization)
# 允许编译器在整个程序的层面上进行优化,而不仅仅是单个 crate
# 可以显著减少代码大小和提高性能,但会增加链接时间
lto = true

# panic = "abort":发布版本中也使用 abort 策略
# 确保 panic 时不会尝试栈展开,减少代码大小
panic = "abort"

内存布局文件 (memory.x)

复制代码
MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 64K
  RAM   : ORIGIN = 0x20000000, LENGTH = 20K
}

烧录命令

bash 复制代码
# 编译
cargo build --release

# 使用 probe-rs 烧录
cargo flash --chip STM32F103C8 --release

# 使用 openocd 烧录
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
  -c "program target/thumbv7m-none-eabi/release/my-project verify reset exit"

# 使用 st-flash 烧录
st-flash write target/thumbv7m-none-eabi/release/my-project.bin 0x08000000

# 调试
cargo embed --release

2. 时钟系统详解

为什么先学时钟? 因为几乎所有外设都依赖时钟才能工作。时钟配置是嵌入式开发中最基础、最重要的一步。配置错误会导致外设工作异常、串口波特率不准、USB 无法枚举等问题。

2.1 STM32F1 时钟树总览

STM32F103 的时钟系统非常灵活,有多个时钟源和分频器。以下是简化的时钟树:

复制代码
                          ┌─────────────┐
                          │   HSE       │  外部高速晶振 (DKX 板: 8MHz)
                          │  8 MHz      │
                          └──────┬──────┘
                                 │
                          ┌──────▼──────┐
                          │   HSI       │  内部 RC 振荡器
                          │  8 MHz      │  (精度差,±1%,启动快)
                          └──────┬──────┘
                                 │
                 ┌───────────────┼───────────────┐
                 │               │               │
                 │         ┌─────▼─────┐         │
                 │         │   PLL     │  锁相环  │
                 │         │  倍频器   │  ×2~×16 │
                 │         └─────┬─────┘         │
                 │               │               │
          ┌──────▼──────┐ ┌──────▼──────┐        │
          │ SYSCLK      │ │  USBCLK     │        │
          │ 系统时钟     │ │  USB 时钟   │        │
          │ 最大 72MHz   │ │  必须 48MHz │        │
          └──────┬──────┘ └─────────────┘        │
                 │                                │
        ┌────────┼────────┐                      │
        │        │        │                      │
   ┌────▼───┐ ┌──▼───┐ ┌──▼────┐          ┌─────▼─────┐
   │ AHB    │ │ APB1 │ │ APB2  │          │  SYSCLK   │
   │ 总线   │ │ 总线  │ │ 总线  │          │  来源选择  │
   │≤72MHz  │ │≤36MHz│ │≤72MHz │          │ HSI/HSE/PLL
   └───┬────┘ └──┬───┘ └──┬────┘          └───────────┘
       │         │        │
  ┌────▼───┐ ┌───▼────┐ ┌─▼──────┐
  │ Cortex │ │USART2/3│ │USART1  │
  │ SysTick│ │TIM2-4  │ │SPI1    │
  │ DMA    │ │I2C1/2  │ │ADC1/2  │
  │ Flash  │ │SPI2    │ │TIM1    │
  └────────┘ │USB     │ │GPIO    │
             └────────┘ └────────┘

核心概念:PLL(锁相环)

复制代码
PLL 时钟 = PLL 输入时钟 × PLL 倍频系数

如果选择 HSE 作为 PLL 输入:
  PLLCLK = HSE × 倍频系数 (×2 ~ ×16)

示例(DKX 板 8MHz 晶振):
  HSE × 9  = 8 × 9  = 72 MHz ← 最大系统时钟
  HSE × 6  = 8 × 6  = 48 MHz ← USB 需要
  HSE × 4  = 8 × 4  = 32 MHz

如果选择 HSI 作为 PLL 输入:
  PLLCLK = HSI × 2 × 倍频系数 / 2
  PLLCLK = HSI × 倍频系数 (×2 ~ ×16)
  但 HSI 必须先被 2 分频再进入 PLL

2.2 时钟源详解

HSI (High Speed Internal) --- 内部高速时钟
复制代码
特点:
├── 频率:8 MHz(RC 振荡器,有温漂)
├── 精度:±1%(出厂校准),温度变化会漂移
├── 优点:不需要外部元件,上电即可使用
├── 缺点:精度差,不适合 USB、CAN、精确波特率
└── 默认:上电后自动作为系统时钟源

什么时候用 HSI?

  • 简单的 LED 闪烁、按键检测等不需要精确时钟的场景
  • 外部晶振损坏时的备用方案
  • 快速启动场景(HSI 比 HSE 启动快)
HSE (High Speed External) --- 外部高速时钟
复制代码
特点:
├── 频率:4-16 MHz(DKX 板使用 8MHz 晶振)
├── 精度:±0.005%(取决于晶振质量)
├── 优点:精度高,适合 USB、CAN、精确串口波特率
├── 缺点:需要外部晶振,启动需要时间(几百微秒~几毫秒)
└── DKX 板:8MHz 无源晶振 + 2 个 20pF 负载电容

什么时候用 HSE?

  • 需要 USB 功能(必须用 HSE 或 HSE 通过 PLL)
  • 需要 CAN 总线(需要精确时钟)
  • 需要精确的串口波特率
  • 需要系统满频 72MHz 运行
LSE (Low Speed External) --- 外部低速时钟
复制代码
特点:
├── 频率:32.768 kHz(用于 RTC)
├── 精度:很高(晶振温漂小)
├── 用途:实时时钟 (RTC)、看门狗
└── DKX 板:可能没有焊接 LSE 晶振(需确认原理图)
LSI (Low Speed Internal) --- 内部低速时钟
复制代码
特点:
├── 频率:约 40 kHz(不精确)
├── 用途:独立看门狗 (IWDG)、RTC 备用时钟
└── 精度:较差(±30%)
PLL (Phase Locked Loop) --- 锁相环

PLL 是时钟系统的核心,用于将低频时钟倍频到高频。

复制代码
┌─────────────────────────────────────────────────┐
│                    PLL 详解                      │
├─────────────────────────────────────────────────┤
│                                                  │
│  输入源选择:                                      │
│  ┌──────┐    ┌─────────┐                         │
│  │ HSI/2│───►│         │    ┌───────────┐        │
│  └──────┘    │ PLL MUX │───►│  ÷ PLLMUL │──► PLLCLK
│  ┌──────┐───►│         │    │  (×2~×16) │        │
│  │ HSE  │    └─────────┘    └───────────┘        │
│  └──────┘                                        │
│                                                  │
│  常用配置:                                        │
│  ┌──────────┬──────────┬──────────────┐          │
│  │ 输入时钟 │ 倍频系数  │ 输出频率     │          │
│  ├──────────┼──────────┼──────────────┤          │
│  │ HSI 8MHz │ ×9       │ 36 MHz*     │          │
│  │ HSE 8MHz │ ×9       │ 72 MHz ✓    │          │
│  │ HSE 8MHz │ ×6       │ 48 MHz ✓    │          │
│  │ HSE 8MHz │ ×4       │ 32 MHz ✓    │          │
│  └──────────┴──────────┴──────────────┘          │
│                                                  │
│  * HSI 先被 2 分频(=4MHz),再 ×9 = 36MHz         │
│                                                  │
└─────────────────────────────────────────────────┘

2.3 各总线时钟详解

复制代码
                         SYSCLK (系统时钟)
                              │
                ┌─────────────┼─────────────┐
                │             │             │
           ┌────▼────┐  ┌────▼────┐  ┌─────▼─────┐
           │  AHB    │  │  APB1   │  │   APB2    │
           │  总线   │  │  总线   │  │   总线    │
           │ ÷HPRE  │  │ ÷PPRE1 │  │  ÷PPRE2  │
           │(1/2/4..)│  │(1/2/4..)│  │ (1/2/4..) │
           └────┬────┘  └────┬────┘  └─────┬─────┘
                │            │              │
                │            │              │
          ┌─────▼─────┐ ┌────▼────┐  ┌──────▼──────┐
          │Core, DMA  │ │USART2/3 │  │  USART1     │
          │SysTick    │ │I2C1/2   │  │  SPI1       │
          │Flash      │ │SPI2     │  │  ADC1/2     │
          │GPIO A~D   │ │TIM2-4   │  │  TIM1       │
          │           │ │USB      │  │  EXTI       │
          │           │ │CAN      │  │  AFIO       │
          └───────────┘ └─────────┘  └─────────────┘
AHB 总线 --- 高速总线
参数 说明
最大频率 72 MHz
预分频器 SYSCLK ÷ 1/2/4/8/16/64/128/256/512
连接设备 Cortex-M3 内核、DMA、Flash、GPIO、SysTick
配置方法 rcc::Config::hsi().hclk(72.MHz())

注意: SysTick 时钟来自 AHB(如果 SysTick 配置为使用处理器时钟),或 AHB/8。

APB1 总线 --- 低速外设总线
参数 说明
最大频率 36 MHz(硬限制,超过会损坏芯片)
预分频器 AHB ÷ 1/2/4/8/16
连接设备 USART2, USART3, I2C1/2, SPI2, TIM2-4, USB, CAN
配置方法 rcc::Config::hsi().pclk1(36.MHz())

重要: 如果 APB1 预分频系数 > 1,则定时器时钟 = APB1 × 2。

APB2 总线 --- 高速外设总线
参数 说明
最大频率 72 MHz
预分频器 AHB ÷ 1/2/4/8/16
连接设备 USART1, SPI1, ADC1/2, TIM1, GPIOA~D, EXTI, AFIO
配置方法 rcc::Config::hsi().sysclk(72.MHz()).pclk2(72.MHz())
ADC 时钟
参数 说明
最大频率 14 MHz
时钟来源 APB2 ÷ 2/4/6/8
配置方法 rcc::Config::hsi().adcclk(14.MHz())
USB 时钟
参数 说明
要求频率 48 MHz(必须精确)
时钟来源 PLL 输出(PLLCLK ÷ 1 或 1.5)
配置要求 SYSCLK 必须是 48MHz 或 72MHz

2.4 stm32f1xx-hal 中的时钟配置

HAL 库使用 Builder 模式 配置时钟,非常直观:

基本用法
rust 复制代码
use stm32f1xx_hal::{pac, prelude::*, rcc};

fn main() {
    let dp = pac::Peripherals::take().unwrap();
    let mut flash = dp.FLASH.constrain();  // Flash 等待周期配置

    // 方式 1: 简洁的配置方法
    let mut rcc = dp.RCC.freeze(
        rcc::Config::hsi()           // 使用内部 8MHz RC
            .sysclk(64.MHz())        // 系统时钟 64MHz
            .pclk1(32.MHz())         // APB1 时钟 32MHz
            .pclk2(64.MHz())         // APB2 时钟 64MHz
            .adcclk(8.MHz()),        // ADC 时钟 8MHz
        &mut flash.acr,
    );

    // 方式 2: 使用外部晶振 + PLL
    let mut rcc = dp.RCC.freeze(
        rcc::Config::hse(8.MHz())    // 使用 8MHz 外部晶振
            .sysclk(72.MHz())        // PLL 倍频到 72MHz
            .pclk1(36.MHz())         // APB1 分频到 36MHz
            .pclk2(72.MHz())         // APB2 不分频
            .adcclk(14.MHz()),       // ADC 14MHz
        &mut flash.acr,
    );
}
rcc::Config 的 builder 方法
rust 复制代码
// 所有可用的配置方法(带 * 表示常用)
rcc::Config::hsi()                    // 选择 HSI 作为时钟源
rcc::Config::hse(8.MHz())            // 选择 HSE 作为时钟源,指定频率

.sysclk(72.MHz())    *               // 设置目标系统时钟频率
.pclk1(36.MHz())    *                // 设置 APB1 目标频率
.pclk2(72.MHz())    *                // 设置 APB2 目标频率
.adcclk(14.MHz())   *                // 设置 ADC 时钟频率
.hclk(72.MHz())                       // 设置 AHB 时钟(通常 = SYSCLK)

// PLL 会根据目标频率自动计算倍频系数
// 无需手动设置!
冻结后获取时钟信息
rust 复制代码
let rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz()).sysclk(72.MHz()),
    &mut flash.acr,
);

// 获取实际的时钟频率
rprintln!("SYSCLK: {}", rcc.clocks.sysclk());   // 系统时钟
rprintln!("HCLK:   {}", rcc.clocks.hclk());     // AHB 时钟
rprintln!("PCLK1:  {}", rcc.clocks.pclk1());    // APB1 时钟
rprintln!("PCLK2:  {}", rcc.clocks.pclk2());    // APB2 时钟
rprintln!("ADCCLK: {}", rcc.clocks.adcclk());   // ADC 时钟
rprintln!("USBCLK valid: {}", rcc.clocks.usbclk_valid()); // USB 时钟是否有效
为什么需要 flash.acr

Flash 的读取速度有限,当系统时钟超过 24MHz 时,需要插入等待周期:

系统时钟 Flash 等待周期
0-24 MHz 0 等待周期
24-48 MHz 1 等待周期
48-72 MHz 2 等待周期

freeze() 会自动根据系统时钟频率设置正确的等待周期。

constrain() vs freeze() 的区别
rust 复制代码
// constrain() --- 约束 RCC,返回可配置的对象
// 用于手动配置每个外设时钟
let mut rcc = dp.RCC.constrain();

// freeze() --- 一步完成时钟配置并冻结
// 自动计算所有分频/倍频参数
let rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz()).sysclk(72.MHz()),
    &mut flash.acr,
);

一般推荐用 freeze(),更简单且不容易出错。

高级用法:直接指定分频/倍频系数
rust 复制代码
// 如果你需要完全控制时钟配置,可以使用 RawConfig
let rcc = dp.RCC.freeze(
    rcc::RawConfig {
        hse: Some(8_000_000),       // HSE 频率
        pllmul: Some(7),            // PLL 倍频系数 (×9,索引从 0 开始)
        hpre: rcc::HPre::Div1,     // AHB 预分频 = 不分频
        ppre1: rcc::PPre::Div2,    // APB1 预分频 = AHB ÷ 2
        ppre2: rcc::PPre::Div1,    // APB2 预分频 = 不分频
        usbpre: rcc::UsbPre::Div1_5, // USB 预分频
        adcpre: rcc::AdcPre::Div2,  // ADC 预分频 = APB2 ÷ 2
        ..Default::default()
    },
    &mut flash.acr,
);

2.5 常用时钟配置方案

方案 1: 最简配置(HSI 默认)
rust 复制代码
// 上电默认:HSI 8MHz,不使用 PLL
// SYSCLK = 8MHz, APB1 = 8MHz, APB2 = 8MHz
let mut rcc = dp.RCC.constrain();

// 或者用 freeze 也行
let rcc = dp.RCC.freeze(rcc::Config::hsi(), &mut flash.acr);

适用: LED 闪烁、按键检测、GPIO 测试等简单场景
不适合: USB、CAN、高波特率串口


方案 2: HSI 倍频到 64MHz
rust 复制代码
let mut rcc = dp.RCC.freeze(
    rcc::Config::hsi()
        .sysclk(64.MHz())         // HSI × 8 = 64MHz
        .pclk1(32.MHz())          // APB1 = AHB ÷ 2
        .pclk2(64.MHz())          // APB2 = AHB(不分频)
        .adcclk(8.MHz()),         // ADC = APB2 ÷ 8
    &mut flash.acr,
);
// 注意:HSI 最高只能倍频到 64MHz,不能到 72MHz
// 因为 HSI 进入 PLL 前会被 2 分频,8/2=4, 4×16=64

适用: 没有外部晶振但需要较高性能的场景
不适合: USB(需要精确 48MHz)


方案 3: HSE 倍频到 72MHz(推荐!DKX 板首选
rust 复制代码
let mut rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz())     // 使用 DKX 板的 8MHz 晶振
        .sysclk(72.MHz())         // PLL: 8 × 9 = 72MHz
        .pclk1(36.MHz())          // APB1 = 72 ÷ 2 = 36MHz(最大值)
        .pclk2(72.MHz())          // APB2 = 72MHz(不分频)
        .adcclk(14.MHz()),        // ADC = 72 ÷ 6 ≈ 12MHz(实际取 6 分频)
    &mut flash.acr,
);

适用: 几乎所有场景,最高性能配置
时钟:

  • SYSCLK = 72 MHz
  • AHB = 72 MHz
  • APB1 = 36 MHz
  • APB2 = 72 MHz
  • ADC = 12 MHz
  • Flash 等待周期 = 2

方案 4: HSE 倍频到 48MHz(USB 专用)
rust 复制代码
let mut rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz())     // 8MHz 晶振
        .sysclk(48.MHz())         // PLL: 8 × 6 = 48MHz
        .pclk1(24.MHz())          // APB1 = 48 ÷ 2 = 24MHz
        .pclk2(48.MHz()),         // APB2 = 48MHz
    &mut flash.acr,
);

// 验证 USB 时钟
assert!(rcc.clocks.usbclk_valid());  // USB 需要精确 48MHz

适用: USB 应用
时钟:

  • SYSCLK = 48 MHz
  • USBCLK = 48 MHz ✓(精确)
  • APB1 = 24 MHz
  • APB2 = 48 MHz
  • Flash 等待周期 = 1

方案 5: 72MHz + USB(进阶)
rust 复制代码
let mut rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz())
        .sysclk(72.MHz())         // PLL: 8 × 9 = 72MHz
        .pclk1(36.MHz())          // APB1 = 36MHz
        .pclk2(72.MHz()),         // APB2 = 72MHz
    &mut flash.acr,
);

// USB 时钟 = PLLCLK ÷ 1.5 = 72 ÷ 1.5 = 48MHz ✓
assert!(rcc.clocks.usbclk_valid());

适用: 既需要最高性能又需要 USB 的场景


2.6 时钟配置常见错误

错误 1: APB1 超过 36MHz
rust 复制代码
// ❌ 错误!APB1 最大 36MHz
let mut rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz())
        .sysclk(72.MHz())
        .pclk1(72.MHz()),  // 错误!超过 36MHz
    &mut flash.acr,
);

// ✓ 正确
let mut rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz())
        .sysclk(72.MHz())
        .pclk1(36.MHz()),  // 正确
    &mut flash.acr,
);
错误 2: USB 时钟不精确
rust 复制代码
// ❌ HSI 不适合 USB
let mut rcc = dp.RCC.freeze(
    rcc::Config::hsi().sysclk(48.MHz()),
    &mut flash.acr,
);
// HSI 精度 ±1%,USB 需要 ±0.25%,会导致枚举失败

// ✓ 必须使用 HSE
let mut rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz()).sysclk(48.MHz()),
    &mut flash.acr,
);
错误 3: 忘记 flash.acr
rust 复制代码
// ❌ 缺少 flash.acr 参数
let rcc = dp.RCC.freeze(rcc::Config::hse(8.MHz()).sysclk(72.MHz()));
// 编译错误!freeze 需要两个参数

// ✓ 正确
let mut flash = dp.FLASH.constrain();
let rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz()).sysclk(72.MHz()),
    &mut flash.acr,  // 必须传入!
);
错误 4: SYSCLK 超过 72MHz
rust 复制代码
// ❌ STM32F103 最大 72MHz
let mut rcc = dp.RCC.freeze(
    rcc::Config::hse(8.MHz())
        .sysclk(80.MHz()),  // 错误!
    &mut flash.acr,
);
// freeze 会自动选择接近但不超过 72MHz 的频率
错误 5: ADC 时钟超过 14MHz
rust 复制代码
// ❌ ADC 时钟最大 14MHz
// 如果 pclk2 = 72MHz,且不分频给 ADC,ADC 时钟 = 72/6 = 12MHz ✓
// 如果 pclk2 = 72MHz,且 ADC 不分频,ADC 时钟 = 72MHz ✗

// HAL 库会自动处理,但了解原理很重要

时钟配置速查表

场景 配置 SYSCLK APB1 APB2 USB
LED/按键 Config::hsi() 8 MHz 8 MHz 8 MHz
通用 hse(8).sysclk(72) 72 MHz 36 MHz 72 MHz
USB hse(8).sysclk(48) 48 MHz 24 MHz 48 MHz
低功耗 hsi().sysclk(8) 8 MHz 8 MHz 8 MHz
无外部晶振 hsi().sysclk(64) 64 MHz 32 MHz 64 MHz

3. 入门基础

3.1 hello.rs --- Hello World

最简单的示例,通过 OpenOCD 的半主机(semihosting)输出 "Hello, world"。

rust 复制代码
#![allow(clippy::empty_loop)]
#![deny(unsafe_code)]     // 禁止使用 unsafe 代码
#![no_main]               // 不使用标准 main 入口
#![no_std]                // 不使用标准库(嵌入式必须)

use panic_semihosting as _;  // panic 时通过 semihosting 输出信息

use rtt_target::{rprintln, rtt_init_print};  // RTT 打印宏(需要调试器连接)
use stm32f1xx_hal as _;              // 引入 HAL 库(确保链接时包含)
use cortex_m_rt::entry;              // 嵌入式入口点宏

#[entry]  // 标记 main 函数为程序入口
fn main() -> ! {  // -> ! 表示永不返回(嵌入式主循环)
    rprintln!("Hello, world!");  // 通过调试器打印
    loop {}  // 无限循环,防止程序退出
}

关键概念详解:

  • #![no_std] --- 禁用 Rust 标准库,嵌入式没有操作系统,不能用 std。只能用 core(语言内置的基础类型和操作)
  • #![no_main] --- 禁用标准 main 入口,由 cortex-m-rt 提供 #[entry] 宏作为入口
  • -> ! --- 发散函数类型,表示永不返回。嵌入式程序永远运行,不能"退出"
  • panic_semihosting --- 当发生 panic 时,通过 ARM 的 semihosting 机制把错误信息发送到调试器
  • rprintln!() --- 通过 RTT 打印,速度快(无需 semihosting 调试器通信开销),用于调试
  • stm32f1xx_hal as _ --- 引入 HAL 库但不使用具体名称,确保编译器不会忽略这个依赖

如何运行:

bash 复制代码
# 编译
cargo build --release

# 烧录并用 openocd 监控
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg
# 另一个终端
arm-none-eabi-gdb target/thumbv7m-none-eabi/release/hello
# 在 GDB 中连接:target remote :3333, 然后 continue

3.2 panics.rs --- 异常处理

展示如何定义 HardFault 和未处理异常的处理函数。

rust 复制代码
#![allow(clippy::empty_loop)]
#![no_main]
#![no_std]

use panic_semihosting as _;

use rtt_target::{rprintln, rtt_init_print};
use stm32f1xx_hal as _;
use cortex_m_rt::{entry, exception, ExceptionFrame};  // 异常处理宏

#[entry]
fn main() -> ! {
    rprintln!("Hello, world!");
    loop {}
}

// HardFault 处理:硬件错误时调用
// 常见原因:非法内存访问、非法指令、栈溢出等
#[exception]
unsafe fn HardFault(ef: &ExceptionFrame) -> ! {
    // ExceptionFrame 包含故障发生时的 CPU 寄存器状态
    panic!("{:#?}", ef);
}

// 默认异常处理:未被其他处理函数捕获的异常
#[exception]
unsafe fn DefaultHandler(irqn: i16) {
    // irqn 是中断号,负数表示系统异常,正数表示外部中断
    panic!("Unhandled exception (IRQn = {})", irqn);
}

关键概念:

  • ExceptionFrame --- 包含异常发生时的寄存器快照(PC, LR, xPSR 等),用于调试
  • HardFault --- 最严重的异常,通常是程序错误导致
  • DefaultHandler --- 兜底处理,所有未定义的异常都会到这里

3.3 led.rs --- 点亮LED

最基础的 GPIO 输出示例,点亮 LED 后保持。

rust 复制代码
#![allow(clippy::empty_loop)]
#![deny(unsafe_code)]
#![no_main]
#![no_std]

use panic_halt as _;

use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*};  // pac = Peripheral Access Crate

#[entry]
fn main() -> ! {
    // 获取外设所有权(只能调用一次,后续调用返回 None)
    let p = pac::Peripherals::take().unwrap();

    // 约束 RCC(复位和时钟控制)寄存器
    // constrain() 返回一个包含所有可配置时钟的对象
    let mut rcc = p.RCC.constrain();

    // 拆分 GPIOC 端口为独立的引脚对象
    // split() 返回每个引脚的独立句柄
    let mut gpioc = p.GPIOC.split(&mut rcc);

    // 根据芯片型号选择不同的引脚和电平
    cfg_select! {
        feature = "stm32f100" => {
            // STM32F100: PC9 高电平点亮
            gpioc.pc9.into_push_pull_output(&mut gpioc.crh).set_high();
        }
        feature = "stm32f101" => {
            // STM32F101: PC9 高电平点亮
            gpioc.pc9.into_push_pull_output(&mut gpioc.crh).set_high();
        }
        _ => {
            // STM32F103 (包括你的 DKX 板): PC13 低电平点亮
            // PC13 在 Blue Pill/DKX 板上是共阳接法,低电平 = 亮
            gpioc.pc13.into_push_pull_output(&mut gpioc.crh).set_low();
        }
    }

    loop {}  // 保持 LED 状态不变
}

关键概念详解:

  • pac::Peripherals::take() --- 使用 "take" 模式保证外设所有权独占,防止多个地方同时操作外设
  • .constrain() --- 约束外设,返回可配置的句柄。这是一种 RAII 模式
  • .split() --- 将 GPIO 端口拆分为独立的引脚,每个引脚有独立的类型
  • into_push_pull_output() --- 配置为推挽输出模式(可以输出高/低电平)
  • crh 寄存器 --- 配置引脚 8-15(PC13 在此范围内)
  • crl 寄存器 --- 配置引脚 0-7
  • cfg_select! --- 条件编译宏,根据 feature 选择不同代码

对于你的 DKX 板: PC13 低电平点亮


3.4 blinky.rs --- LED闪烁(定时器)

使用 SysTick 定时器实现 LED 闪烁。

rust 复制代码
#![deny(unsafe_code)]
#![no_std]
#![no_main]

use panic_halt as _;
use nb::block;  // 非阻塞 I/O 库的 block! 宏

use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*, timer::Timer};

#[entry]
fn main() -> ! {
    let cp = cortex_m::Peripherals::take().unwrap();  // Cortex-M 核心外设(SysTick, NVIC...)
    let dp = pac::Peripherals::take().unwrap();        // 芯片外设(GPIO, USART, TIM...)

    let mut rcc = dp.RCC.constrain();
    let mut gpioc = dp.GPIOC.split(&mut rcc);

    // 配置 PC13 为推挽输出
    // crh: Control Register High(引脚 8-15 的配置寄存器)
    let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

    // 使用 SysTick 定时器创建一个频率计数器
    // SysTick 是 Cortex-M 内核自带的 24 位递减定时器
    let mut timer = Timer::syst(cp.SYST, &rcc.clocks).counter_hz();
    timer.start(1.Hz()).unwrap();  // 设置频率为 1Hz(每秒溢出一次)

    // 主循环:每秒切换一次 LED 状态
    loop {
        block!(timer.wait()).unwrap();  // 阻塞等待定时器溢出
        led.set_high();                  // PC13 高电平 = 关灯(DKX/Blue Pill 共阳接法)
        block!(timer.wait()).unwrap();
        led.set_low();                   // PC13 低电平 = 开灯
    }
}

关键概念详解:

  • cortex_m::Peripherals --- Cortex-M 核心外设:

    • SYST --- SysTick 定时器
    • NVIC --- 中断控制器
    • DCB --- 调试控制块
    • DWT --- 数据观察点和触发单元
  • pac::Peripherals --- 芯片特意外设:

    • RCC --- 时钟控制
    • GPIOA/B/C/D --- GPIO 端口
    • USART1/2/3 --- 串口
    • TIM1/2/3/4 --- 定时器
    • SPI1/2 --- SPI 接口
    • I2C1/2 --- I2C 接口
    • ADC1/2 --- ADC
    • USB --- USB 外设
    • CAN --- CAN 控制器
  • Timer::syst() --- 使用 SysTick 定时器创建定时器对象

  • counter_hz() --- 创建以 Hz 为单位的频率计数器

  • block!() --- 将非阻塞操作转为阻塞(轮询等待直到完成)

  • 1.Hz() --- 使用 fugit 库的频率单位


3.5 delay.rs --- LED闪烁(SysTick延迟)

使用 SysTick 延迟实现 LED 闪烁(更简单)。

rust 复制代码
#![deny(unsafe_code)]
#![no_main]
#![no_std]

use panic_halt as _;
use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let cp = cortex_m::Peripherals::take().unwrap();

    let mut rcc = dp.RCC.constrain();
    let mut gpioc = dp.GPIOC.split(&mut rcc);

    // DKX 板:PC13 低电平点亮
    let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

    // 创建基于 SysTick 的阻塞延迟对象
    // 这是一个 embedded_hal DelayMs trait 的实现
    let mut delay = cp.SYST.delay(&rcc.clocks);

    loop {
        led.set_high();
        // 方式 1: 使用 embedded_hal 0.2 的 DelayMs trait
        delay.delay_ms(1_000_u16);  // 延迟 1000ms

        led.set_low();
        // 方式 2: 使用 fugit 库的时间单位
        delay.delay(1.secs());       // 延迟 1 秒
    }
}

两种延迟方式对比:

方式 代码 说明
embedded-hal delay.delay_ms(1000_u16) 标准 trait,跨平台兼容
fugit delay.delay(1.secs()) 类型安全的时间单位,编译期检查

3.6 delay-timer-blinky.rs --- TIM2延迟闪烁

使用通用定时器 TIM2 实现阻塞延迟。

rust 复制代码
#![deny(unsafe_code)]
#![allow(clippy::empty_loop)]
#![no_main]
#![no_std]

use panic_halt as _;
use cortex_m_rt::entry;
use stm32f1xx_hal as hal;
use crate::hal::{pac, prelude::*, rcc};

#[entry]
fn main() -> ! {
    if let (Some(dp), Some(_cp)) = (
        pac::Peripherals::take(),
        cortex_m::peripheral::Peripherals::take(),
    ) {
        let mut flash = dp.FLASH.constrain();

        // 配置系统时钟
        // HSE 8MHz → PLL → SYSCLK 48MHz
        // DKX 板有 8MHz 外部晶振,所以使用 hse()
        let mut rcc = dp.RCC
            .freeze(rcc::Config::hse(8.MHz()).sysclk(48.MHz()), &mut flash.acr);

        let mut gpioc = dp.GPIOC.split(&mut rcc);
        let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

        // 创建基于 TIM2 的微秒级延迟
        // 比 SysTick 更精确,且不占用系统定时器
        let mut delay = dp.TIM2.delay_us(&mut rcc);

        loop {
            led.set_high();
            // 使用 embedded_hal 0.2 的 DelayMs trait
            delay.delay_ms(1000_u32);  // 亮 1 秒

            led.set_low();
            // 使用 fugit 库的时间单位
            delay.delay(3.secs());      // 灭 3 秒
        }
    }
    loop {}
}

关键概念:

  • rcc::Config::hse(8.MHz()) --- 使用 8MHz 外部高速晶振(DKX 板上的晶振)
  • .sysclk(48.MHz()) --- 设置系统时钟为 48MHz
  • rcc.freeze() --- 冻结时钟配置,返回一个不可变的时钟状态
  • dp.TIM2.delay_us() --- 使用 TIM2 创建微秒级延迟
  • 优势:比 SysTick 更灵活,精度更高,不影响 SysTick 的其他用途

4. GPIO 输入输出

4.1 gpio_input.rs --- 按键控制LED

两个按键分别控制两个 LED,含消抖逻辑。

rust 复制代码
#![deny(unsafe_code)]
#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;
use stm32f1xx_hal::{gpio::PinState, pac, prelude::*};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let cp = cortex_m::Peripherals::take().unwrap();
    let mut rcc = dp.RCC.constrain();

    let mut gpioa = dp.GPIOA.split(&mut rcc);
    let mut gpioc = dp.GPIOC.split(&mut rcc);

    // 配置 LED(初始状态高 = 关灯)
    let mut red_led = gpioa.pa8
        .into_push_pull_output_with_state(&mut gpioa.crh, PinState::High);

    // 禁用 JTAG,释放 PA15、PB3、PB4 作为普通 GPIO
    // STM32F1 默认 PA13/PA14/PA15/PB3/PB4 是 JTAG/SWD 引脚
    // 使用普通 GPIO 前必须先释放
    let mut afio = dp.AFIO.constrain(&mut rcc);
    let (gpioa_pa15, _gpiob_pb3, _gpiob_pb4) =
        afio.mapr.disable_jtag(gpioa.pa15, _gpiob.pb3, _gpiob.pb4);

    // 配置按键为上拉输入
    // 上拉输入:未按下时读到高电平,按下接地时读到低电平
    let key_0 = gpioc.pc5.into_pull_up_input(&mut gpioc.crl);
    let key_1 = gpioa_pa15.into_pull_up_input(&mut gpioa.crh);

    // 按键消抖逻辑
    let mut key_up: bool = true;  // 标志按键是否已释放
    let mut delay = cp.SYST.delay(&rcc.clocks);

    loop {
        let key_result = (key_0.is_low(), key_1.is_low());

        if key_up && (key_result.0 || key_result.1) {
            // 有按键被按下,且之前按键已释放
            key_up = false;
            delay.delay_ms(10u8);  // 消抖延时 10ms

            match key_result {
                (true, _) => red_led.toggle(),    // key_0 按下,切换 LED
                (_, true) => green_led.toggle(),  // key_1 按下,切换 LED
                (_, _) => (),
            }
        } else if !key_result.0 && !key_result.1 {
            // 两个按键都释放
            key_up = true;
            delay.delay_ms(10u8);  // 释放消抖
        }
    }
}

JTAG 引脚说明:

  • STM32F1 默认使用 JTAG/SWD 调试接口
  • PA13(SWDIO), PA14(SWCLK), PA15(JTDI), PB3(JTDO), PB4(JNTRST) 默认被 JTAG 占用
  • 如果要用这些引脚做普通 GPIO,必须先禁用 JTAG
  • 注意:PA13/PA14 是 SWD 接口,一般不建议禁用(否则无法调试)

4.2 dynamic_gpio.rs --- 动态GPIO切换

展示如何在运行时动态切换引脚模式。

rust 复制代码
#![deny(unsafe_code)]
#![no_std]
#![no_main]

use panic_halt as _;
use nb::block;
use cortex_m_rt::entry;
use rtt_target::{rprintln, rtt_init_print};
use embedded_hal_02::digital::v2::{InputPin, OutputPin};
use stm32f1xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
    let cp = cortex_m::Peripherals::take().unwrap();
    let dp = pac::Peripherals::take().unwrap();
    let mut rcc = dp.RCC.constrain();
    let mut gpioc = dp.GPIOC.split(&mut rcc);

    // 创建动态引脚(可以在运行时切换输入/输出模式)
    let mut pin = gpioc.pc13.into_dynamic(&mut gpioc.crh);

    let mut timer = cp.SYST.counter_hz(&rcc.clocks);
    timer.start(1.Hz()).unwrap();

    loop {
        // 切换为浮空输入模式,读取电平
        pin.make_floating_input(&mut gpioc.crh);
        block!(timer.wait()).unwrap();
        rprintln!("{}", pin.is_high().unwrap());  // 打印当前电平

        // 切换为推挽输出模式,控制 LED
        pin.make_push_pull_output(&mut gpioc.crh);
        pin.set_high().unwrap();
        block!(timer.wait()).unwrap();
        pin.set_low().unwrap();
        block!(timer.wait()).unwrap();
    }
}

动态 GPIO 用途:

  • 某些协议(如单总线、I2C 软件模拟)需要在运行时切换引脚方向
  • 有限引脚的复用

5. 外部中断 EXTI

5.1 exti.rs --- 外部中断响应

通过 EXTI 外部中断响应引脚电平变化(PA7 上升/下降沿触发)。

rust 复制代码
#![allow(clippy::empty_loop)]
#![no_main]
#![no_std]

use panic_halt as _;
use core::mem::MaybeUninit;
use cortex_m_rt::entry;
use pac::interrupt;  // 中断宏
use stm32f1xx_hal::gpio::*;
use stm32f1xx_hal::{pac, prelude::*};

// 全局静态变量,供中断处理函数使用
// 使用 MaybeUninit 避免在启动前初始化,减少开销
// 警告:这是 unsafe 的,但比 Mutex 更轻量
static mut LED: MaybeUninit<stm32f1xx_hal::gpio::gpioc::PC13<Output>> = MaybeUninit::uninit();
static mut INT_PIN: MaybeUninit<stm32f1xx_hal::gpio::gpioa::PA7<Input>> = MaybeUninit::uninit();

// EXTI9_5 中断处理函数
// PA7 连接到 EXTI 线 7,在 EXTI9_5 范围内(EXTI5-EXTI9 共用一个处理函数)
#[interrupt]
fn EXTI9_5() {
    let led = unsafe { &mut *LED.as_mut_ptr() };
    let int_pin = unsafe { &mut *INT_PIN.as_mut_ptr() };

    if int_pin.check_interrupt() {  // 检查是否是此引脚触发的中断
        led.toggle();               // 切换 LED 状态
        int_pin.clear_interrupt_pending_bit();  // 清除中断挂起位
        // 如果不清除,中断会反复触发!
    }
}

#[entry]
fn main() -> ! {
    let mut p = pac::Peripherals::take().unwrap();
    let _cp = cortex_m::peripheral::Peripherals::take().unwrap();
    let mut rcc = p.RCC.constrain();

    // 使用作用域确保初始化期间不会发生中断
    {
        let mut gpioa = p.GPIOA.split(&mut rcc);
        let mut gpioc = p.GPIOC.split(&mut rcc);
        let mut afio = p.AFIO.constrain(&mut rcc);

        // 配置 LED
        let led = unsafe { &mut *LED.as_mut_ptr() };
        *led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

        // 配置中断引脚
        let int_pin = unsafe { &mut *INT_PIN.as_mut_ptr() };
        *int_pin = gpioa.pa7.into_floating_input(&mut gpioa.crl);

        // 将 PA7 连接到 EXTI 中断线
        int_pin.make_interrupt_source(&mut afio);

        // 设置触发方式:上升沿和下降沿都触发
        int_pin.trigger_on_edge(&mut p.EXTI, Edge::RisingFalling);

        // 使能该引脚的中断
        int_pin.enable_interrupt(&mut p.EXTI);
    } // 初始化结束,作用域确保 int_pin 的引用被释放

    // 在 NVIC 中取消屏蔽 EXTI9_5 中断
    // 这一步必须在初始化完成后才能做!
    unsafe {
        pac::NVIC::unmask(pac::Interrupt::EXTI9_5);
    }

    loop {}  // 主循环为空,所有工作在中断中完成
}

EXTI 中断线与引脚对应:

EXTI 线 可用引脚 中断处理函数名
EXTI0 PA0, PB0, PC0... EXTI0
EXTI1 PA1, PB1, PC1... EXTI1
EXTI4 PA4, PB4, PC4... EXTI4
EXTI5-9 PA5-PA9 等 EXTI9_5
EXTI10-15 PA10-PA15 等 EXTI15_10

关键步骤总结:

  1. 配置引脚为输入
  2. make_interrupt_source() --- 将引脚连接到 EXTI 线
  3. trigger_on_edge() --- 设置触发边沿
  4. enable_interrupt() --- 使能 EXTI 中断
  5. NVIC::unmask() --- 在 NVIC 中取消屏蔽

6. 定时器与中断

6.1 blinky_timer_irq.rs --- TIM2中断闪烁(裸机)

使用 TIM2 中断实现 LED 闪烁,不依赖 RTIC 框架。

rust 复制代码
#![no_main]
#![no_std]

use panic_halt as _;
use stm32f1xx_hal as hal;

use crate::hal::{
    gpio::{gpioc, Output, PinState, PushPull},
    pac::{interrupt, Interrupt, Peripherals, TIM2},
    prelude::*,
    rcc,
    timer::{CounterMs, Event},
};

use core::cell::RefCell;
use cortex_m::{asm::wfi, interrupt::Mutex};  // 中断安全的互斥类型
use cortex_m_rt::entry;

type LedPin = gpioc::PC13<Output<PushPull>>;

// 使用 Mutex<RefCell<Option<T>>> 模式安全地在中断间共享数据
// Mutex 不是操作系统级的互斥量,而是基于中断禁用的临界区
static G_LED: Mutex<RefCell<Option<LedPin>>> = Mutex::new(RefCell::new(None));
static G_TIM: Mutex<RefCell<Option<CounterMs<TIM2>>>> = Mutex::new(RefCell::new(None));

// TIM2 更新中断处理函数
#[interrupt]
fn TIM2() {
    // static mut 变量在多次中断调用间保持状态
    // 首次调用时从全局存储中取出资源
    static mut LED: Option<LedPin> = None;
    static mut TIM: Option<CounterMs<TIM2>> = None;

    // get_or_insert_with: 如果是 None,执行闭包获取值
    let led = LED.get_or_insert_with(|| {
        // interrupt::free 创建临界区(临时禁用中断)
        cortex_m::interrupt::free(|cs| {
            G_LED.borrow(cs).replace(None).unwrap()
        })
    });

    let tim = TIM.get_or_insert_with(|| {
        cortex_m::interrupt::free(|cs| {
            G_TIM.borrow(cs).replace(None).unwrap()
        })
    });

    // 切换 LED
    led.toggle();
    // 清除定时器更新标志
    tim.wait().ok();
}

#[entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();

    let mut flash = dp.FLASH.constrain();
    let mut rcc = dp.RCC.freeze(
        rcc::Config::hsi().sysclk(8.MHz()).pclk1(8.MHz()),
        &mut flash.acr,
    );

    // 配置 PC13
    let mut gpioc = dp.GPIOC.split(&mut rcc);
    let led = Output::new(gpioc.pc13, &mut gpioc.crh, PinState::High);

    // 将 LED 移入全局存储(所有权转移)
    cortex_m::interrupt::free(|cs| *G_LED.borrow(cs).borrow_mut() = Some(led));

    // 设置 TIM2 定时器
    let mut timer = dp.TIM2.counter_ms(&mut rcc);
    timer.start(1.secs()).unwrap();
    timer.listen(Event::Update);  // 使能更新事件中断

    // 将定时器移入全局存储
    cortex_m::interrupt::free(|cs| *G_TIM.borrow(cs).borrow_mut() = Some(timer));

    // 取消 NVIC 中断屏蔽
    unsafe {
        cortex_m::peripheral::NVIC::unmask(Interrupt::TIM2);
    }

    loop {
        wfi();  // Wait For Interrupt,CPU 进入低功耗休眠
    }
}

Mutex<RefCell<Option>> 模式详解:

复制代码
┌──────────────────────────────────────────────────┐
│                    main()                         │
│  1. 创建 LED, Timer                               │
│  2. interrupt::free → 移入 G_LED, G_TIM          │
│  3. NVIC::unmask → 使能中断                       │
│  4. wfi() 循环休眠                                │
└──────────────────────────────────────────────────┘
                           │
                    TIM2 中断触发
                           │
┌──────────────────────────────────────────────────┐
│               TIM2() 中断处理                     │
│  1. 从全局存储取出 LED/Timer(首次)               │
│  2. LED.toggle()                                  │
│  3. timer.clear_interrupt()                       │
└──────────────────────────────────────────────────┘

6.2 timer-interrupt-rtic.rs --- RTIC定时器中断

使用 RTIC 框架管理定时器中断(推荐方式)。

rust 复制代码
#![no_std]
#![no_main]

use panic_halt as _;

#[rtic::app(device = stm32f1xx_hal::pac)]  // 指定外设类型
mod app {
    use stm32f1xx_hal::{
        gpio::{gpioc::PC13, Output, PinState, PushPull},
        pac,
        prelude::*,
        timer::{CounterMs, Event},
    };

    #[shared]    // 可在多个任务间共享的资源
    struct Shared {}

    #[local]     // 仅属于单个任务的资源
    struct Local {
        led: PC13<Output<PushPull>>,
        timer_handler: CounterMs<pac::TIM1>,
    }

    // 系统初始化(最先执行,优先级最高)
    #[init]
    fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
        let mut rcc = cx.device.RCC.constrain();
        let mut gpioc = cx.device.GPIOC.split(&mut rcc);

        // 配置 LED
        let led = gpioc
            .pc13
            .into_push_pull_output_with_state(&mut gpioc.crh, PinState::High);

        // 配置 TIM1 定时器
        let mut timer = cx.device.TIM1.counter_ms(&mut rcc);
        timer.start(1.secs()).unwrap();
        timer.listen(Event::Update);  // 使能更新中断

        // 返回共享资源、本地资源、单调定时器
        (Shared {}, Local { led, timer_handler: timer }, init::Monotonics())
    }

    // 空闲任务(无其他任务执行时运行)
    #[idle]
    fn idle(_cx: idle::Context) -> ! {
        loop {
            cortex_m::asm::wfi();  // 休眠等待中断
        }
    }

    // TIM1 更新中断任务
    // binds = TIM1_UP → 绑定到 TIM1 的更新中断
    // local 中可以有编译期初始值语法:led_state: bool = false
    #[task(binds = TIM1_UP, priority = 1, local = [
        led,
        timer_handler,
        led_state: bool = false,    // 编译期初始化的静态变量
        count: u8 = 0
    ])]
    fn tick(cx: tick::Context) {
        if *cx.local.led_state {
            cx.local.led.set_high();   // 关灯
            *cx.local.led_state = false;
        } else {
            cx.local.led.set_low();    // 开灯
            *cx.local.led_state = true;
        }

        // 动态改变闪烁频率
        *cx.local.count += 1;
        if *cx.local.count == 4 {
            // 闪烁 4 次后改为 500ms 周期
            cx.local.timer_handler.start(500.millis()).unwrap();
        } else if *cx.local.count == 12 {
            // 再闪烁 8 次后恢复 1s 周期
            cx.local.timer_handler.start(1.secs()).unwrap();
            *cx.local.count = 0;
        }

        cx.local.timer_handler.clear_interrupt(Event::Update);
    }
}

RTIC vs 裸机中断对比:

特性 裸机 RTIC
资源共享 手动 Mutex<RefCell<Option<T>>> 自动管理
中断绑定 手动 NVIC::unmask #[task(binds = ...)]
优先级 手动配置 priority 属性
代码量
安全性 容易出错 编译期保证

6.3 rtic2-tick.rs --- RTIC2 异步任务

RTIC 2.x 的异步任务,使用单调度定时器。

rust 复制代码
#![no_main]
#![no_std]

use defmt_rtt as _;          // defmt RTT 调试输出
use panic_probe as _;         // probe-rs panic 处理
use rtic_time::Monotonic;     // 单调定时器 trait
use stm32f1xx_hal::{
    gpio::{Output, PC13},
    pac,
    prelude::*,
    rcc::Config,
};

// 使用 TIM3 作为单调度定时器(微秒级精度)
type Mono = stm32f1xx_hal::timer::MonoTimerUs<pac::TIM3>;

#[app(device = pac, dispatchers = [USART1], peripherals = true)]
mod app {
    use super::*;

    #[shared]
    struct Shared {}

    #[local]
    struct Local {
        led: PC13<Output>,
    }

    #[init]
    fn init(mut ctx: init::Context) -> (Shared, Local) {
        let mut flash = ctx.device.FLASH.constrain();
        let mut rcc = ctx.device.RCC
            .freeze(Config::hsi().sysclk(48.MHz()), &mut flash.acr);

        // 创建 TIM3 单调度定时器
        ctx.device.TIM3.monotonic_us(&mut ctx.core.NVIC, &mut rcc);

        let mut gpioc = ctx.device.GPIOC.split(&mut rcc);
        let led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
        defmt::info!("Start");

        // 启动异步任务
        tick::spawn().ok();
        (Shared {}, Local { led })
    }

    // 异步任务:每 500ms 切换 LED
    // async fn 允许在等待时让出 CPU
    #[task(local = [led, count: u32 = 0])]
    async fn tick(ctx: tick::Context) {
        loop {
            ctx.local.led.toggle();
            *ctx.local.count += 1;
            defmt::info!("Tick {}", *ctx.local.count);
            // 异步等待 500ms,期间不阻塞其他任务
            Mono::delay(500.millis().into()).await;
        }
    }
}

RTIC 2 新特性:

  • dispatchers --- 软件任务调度器(使用硬件中断队列)
  • MonoTimerUs --- 微秒级单调度定时器
  • async fn --- 异步任务,等待时可以让出 CPU 给其他任务
  • defmt --- 比 semihosting 和 RTT 都高效得多的日志输出

7. 串口通信

7.1 serial.rs --- 串口回环测试

USART 串口通信基础示例,短接 TX/RX 进行回环测试。

rust 复制代码
#![allow(clippy::empty_loop)]
#![deny(unsafe_code)]
#![no_main]
#![no_std]

use panic_halt as _;
use cortex_m::asm;
use nb::block;
use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*, serial::Config};

#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut rcc = p.RCC.constrain();
    let mut gpiob = p.GPIOB.split(&mut rcc);

    // === USART3 引脚配置(DKX 板)===
    // TX: PB10 配置为复用推挽输出
    // 复用推挽输出 = GPIO 由硬件外设控制,而非软件
    let tx = gpiob.pb10.into_alternate_push_pull(&mut gpiob.crh);
    // RX: PB11 默认就是浮空输入
    let rx = gpiob.pb11;

    // 创建串口实例
    // USART3, 波特率 115200
    let mut serial = p
        .USART3
        .serial((tx, rx), Config::default().baudrate(115200.bps()), &mut rcc);

    // === 方式 1: 使用 serial 对象直接读写 ===
    let sent = b'X';
    block!(serial.tx.write_u8(sent)).unwrap();      // 发送字节
    let received = block!(serial.rx.read()).unwrap(); // 接收字节
    assert_eq!(received, sent);  // 验证
    asm::bkpt();  // 断点,用调试器检查

    // === 方式 2: 拆分为独立的 TX/RX ===
    let (mut tx, mut rx) = serial.split();
    let received = block!(rx.read()).unwrap();
    block!(tx.write_u8(received)).unwrap();  // 回显
    asm::bkpt();

    // === 方式 3: 重新合并 ===
    let mut serial = tx.reunite(rx);
    let sent = b'Z';
    block!(serial.write(sent)).ok();
    let received: u8 = block!(serial.read()).unwrap();
    assert_eq!(received, sent);
    asm::bkpt();

    loop {}
}

各串口可用引脚(DKX 板):

串口 TX 引脚 RX 引脚 备注
USART1 PA9 或 PB6(remap) PA10 或 PB7(remap) APB2
USART2 PA2 PA3 APB1
USART3 PB10 PB11 APB1

关键概念:

  • into_alternate_push_pull() --- 复用推挽输出,引脚由硬件外设控制
  • Config::default().baudrate() --- 串口配置(波特率、数据位、停止位等)
  • .split() --- 拆分为独立的 TxRx 对象
  • .reunite() --- 将 TxRx 重新合并

7.2 serial-fmt.rs --- 串口格式化输出

使用 write!/writeln! 宏通过串口发送格式化字符串。

rust 复制代码
#![deny(unsafe_code)]
#![allow(clippy::empty_loop)]
#![no_main]
#![no_std]

use panic_halt as _;
use cortex_m_rt::entry;
use stm32f1xx_hal::{
    pac,
    prelude::*,
    serial::{Config, Serial},
};
use core::fmt::Write;  // 导入 Write trait

#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut rcc = p.RCC.constrain();
    let mut gpiob = p.GPIOB.split(&mut rcc);

    let tx = gpiob.pb10.into_alternate_push_pull(&mut gpiob.crh);
    let rx = gpiob.pb11;

    let serial = Serial::new(
        p.USART3,
        (tx, rx),
        Config::default().baudrate(9600.bps()),
        &mut rcc,
    );

    let (mut tx, _rx) = serial.split();

    let number = 103;
    // 使用 write! 宏格式化输出
    writeln!(tx, "Hello formatted string {}", number).unwrap();
    // Windows 换行: write!(tx, "Hello formatted string {}\r\n", number)

    loop {}
}

用途: 通过 USB-TTL 串口模块连接到电脑,在串口终端(如 Arduino IDE 的串口监视器)查看输出。


7.3 serial-interrupt-idle.rs --- 串口中断+空闲检测

使用中断方式接收串口数据,配合空闲检测实现不定长数据接收。

rust 复制代码
#![no_main]
#![no_std]

use panic_halt as _;
use cortex_m_rt::entry;
use stm32f1xx_hal::{
    pac::{self, interrupt, USART1},
    prelude::*,
    serial::{Rx, Tx},
};

static mut RX: Option<Rx<USART1>> = None;
static mut TX: Option<Tx<USART1>> = None;

#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut rcc = p.RCC.constrain();
    let mut afio = p.AFIO.constrain(&mut rcc);
    let mut gpiob = p.GPIOB.split(&mut rcc);

    // USART1 Remap: PB6(TX), PB7(RX)
    let tx = gpiob.pb6.into_alternate_push_pull(&mut gpiob.crl);
    let rx = gpiob.pb7;

    // 创建串口并 remap 引脚
    let (mut tx, mut rx) = p
        .USART1
        .remap(&mut afio.mapr)
        .serial((tx, rx), 115_200.bps(), &mut rcc)
        .split();

    // 使能中断
    tx.listen();         // 使能 TXE 中断
    rx.listen();         // 使能 RXNE 中断(接收缓冲区非空)
    rx.listen_idle();    // 使能 IDLE 中断(总线空闲检测)

    cortex_m::interrupt::free(|_| unsafe {
        TX.replace(tx);
        RX.replace(rx);
    });
    unsafe {
        cortex_m::peripheral::NVIC::unmask(pac::Interrupt::USART1);
    }

    loop {
        cortex_m::asm::wfi()
    }
}

const BUFFER_LEN: usize = 4096;
static mut BUFFER: &mut [u8; BUFFER_LEN] = &mut [0; BUFFER_LEN];
static mut WIDX: usize = 0;

unsafe fn write(buf: &[u8]) {
    if let Some(tx) = TX.as_mut() {
        buf.iter().for_each(|w| if let Err(_err) = nb::block!(tx.write(*w)) {})
    }
}

#[interrupt]
unsafe fn USART1() {
    cortex_m::interrupt::free(|_| {
        if let Some(rx) = RX.as_mut() {
            if rx.is_rx_not_empty() {
                // 收到一个字节
                if let Ok(w) = nb::block!(rx.read()) {
                    BUFFER[WIDX] = w;
                    WIDX += 1;
                    // 缓冲区快满时,立即发送
                    if WIDX >= BUFFER_LEN - 1 {
                        write(&BUFFER[..]);
                        WIDX = 0;
                    }
                }
                rx.listen_idle();  // 重新使能空闲检测
            } else if rx.is_idle() {
                // 检测到总线空闲 → 一帧数据接收完成
                rx.unlisten_idle();
                write(&BUFFER[0..WIDX]);  // 发送已接收的数据
                WIDX = 0;
            }
        }
    })
}

空闲检测原理:

复制代码
数据帧: | byte1 | byte2 | byte3 | byte4 | [空闲] | byte1 | byte2 | ...
                                      ↑
                                 IDLE 中断触发
                                 说明一帧结束

7.4 serial_9bits.rs --- 9位串口通信

9位数据位模式,用第 9 位标记地址/数据。

rust 复制代码
// 配置为 9 位数据位
let serial = p.USART3.serial::<PushPull>(
    (tx_pin, rx_pin),
    Config::default()
        .baudrate(9600.bps())
        .wordlength_9bits()    // 9 位数据位
        .parity_none(),        // 无校验
    &mut rcc,
);

// 第 9 位 = 1 表示地址字节
// 第 9 位 = 0 表示数据字节
block!(serial_tx.write(SLAVE_ADDR as u16 | 0x100)).unwrap();  // 发送地址
block!(serial_tx.write(data_byte)).unwrap();                   // 发送数据

用途: 多机通信中区分地址帧和数据帧。


7.5 serial-dma-rx.rs --- 串口DMA接收

使用 DMA 传输数据,减轻 CPU 负担。

rust 复制代码
#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut rcc = p.RCC.constrain();

    // 拆分 DMA1 通道(DMA1 有 7 个通道)
    let channels = p.DMA1.split(&mut rcc);

    let mut gpioa = p.GPIOA.split(&mut rcc);
    let tx = gpioa.pa9.into_alternate_push_pull(&mut gpioa.crh);
    let rx = gpioa.pa10;

    let serial = Serial::new(p.USART1, (tx, rx), 9_600.bps(), &mut rcc);

    // 将串口 RX 与 DMA 通道 5 绑定
    let rx = serial.rx.with_dma(channels.5);

    // singleton! 宏:在静态内存中创建唯一实例
    // DMA 需要静态生命周期的缓冲区
    let buf = singleton!(: [u8; 8] = [0; 8]).unwrap();

    // 启动 DMA 接收(阻塞等待 8 字节)
    let (_buf, _rx) = rx.read(buf).wait();

    // _buf 现在包含接收到的 8 字节
    asm::bkpt();

    loop {}
}

DMA 的优势:

  • 不占用 CPU,数据由硬件自动搬运
  • 适合高速、大量数据传输
  • 减少中断频率

8. ADC 模数转换

8.1 adc.rs --- ADC基础读取

rust 复制代码
#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut flash = p.FLASH.constrain();

    // 配置时钟:ADC 时钟设为 2MHz
    // STM32F103 的 ADC 最大时钟为 14MHz
    let mut rcc = p.RCC.freeze(
        rcc::Config::hsi().adcclk(2.MHz()),
        &mut flash.acr,
    );
    rprintln!("adc freq: {}", rcc.clocks.adcclk());

    let mut adc1 = adc::Adc::new(p.ADC1, &mut rcc);

    // 配置 PB0 为模拟输入
    let mut gpiob = p.GPIOB.split(&mut rcc);
    let mut ch0 = gpiob.pb0.into_analog(&mut gpiob.crl);

    loop {
        let data: u16 = adc1.read(&mut ch0).unwrap();  // 读取 ADC(12位,0-4095)
        rprintln!("adc1: {}", data);
    }
}

ADC 关键参数:

  • 分辨率:12 位(0-4095)
  • 转换时间:取决于 ADC 时钟
  • 参考电压:VDDA(通常 3.3V)
  • 公式:电压 = 读数 / 4095 * 3.3V

DKX 板可用 ADC 通道:

引脚 ADC 通道
PA0 ADC1_IN0
PA1 ADC1_IN1
PA2 ADC1_IN2
PA3 ADC1_IN3
PA4 ADC1_IN4
PA5 ADC1_IN5
PA6 ADC1_IN6
PA7 ADC1_IN7
PB0 ADC1_IN8
PB1 ADC1_IN9

8.2 adc_temperature.rs --- 温度传感器

rust 复制代码
#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut flash = p.FLASH.constrain();

    // 配置时钟为较高频率
    let mut rcc = p.RCC.freeze(
        rcc::Config::hse(8.MHz())
            .sysclk(56.MHz())
            .pclk1(28.MHz())
            .adcclk(14.MHz()),
        &mut flash.acr,
    );

    rprintln!("sysclk freq: {}", rcc.clocks.sysclk());
    rprintln!("adc freq: {}", rcc.clocks.adcclk());

    let mut adc = p.ADC1.adc(&mut rcc);

    loop {
        let temp = adc.read_temp();  // 读取内部温度传感器
        rprintln!("temp: {}", temp);
    }
}

内部温度传感器:

  • 连接到 ADC1 通道 16
  • 精度不高(±1.5°C),适合粗略监测
  • 转换时间需要 17.1μs 以上

8.3 adc-dma-circ.rs --- ADC循环DMA

使用 DMA 循环缓冲区连续采集 ADC 数据。

rust 复制代码
#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut rcc = p.RCC.freeze(
        rcc::Config::hsi().adcclk(2.MHz()),
        &mut flash.acr,
    );

    // 拆分 DMA1 通道 1
    let dma_ch1 = p.DMA1.split(&mut rcc).1;

    let adc1 = adc::Adc::new(p.ADC1, &mut rcc);
    let mut gpioa = p.GPIOA.split(&mut rcc);
    let adc_ch0 = gpioa.pa0.into_analog(&mut gpioa.crl);

    // 将 ADC 与 DMA 绑定
    let adc_dma = adc1.with_dma(adc_ch0, dma_ch1);

    // 创建双缓冲区(循环模式需要两个半缓冲区)
    // singleton! 确保缓冲区在静态内存中
    let buf = singleton!(: [[u16; 8]; 2] = [[0; 8]; 2]).unwrap();

    // 启动循环 DMA 读取
    let mut circ_buffer = adc_dma.circ_read(buf);

    // DMA 自动在两个半缓冲区间交替写入
    while circ_buffer.readable_half().unwrap() != Half::First {}
    let _first_half = circ_buffer.peek(|half, _| *half).unwrap();

    while circ_buffer.readable_half().unwrap() != Half::Second {}
    let _second_half = circ_buffer.peek(|half, _| *half).unwrap();

    let (_buf, adc_dma) = circ_buffer.stop();
    let (_adc1, _adc_ch0, _dma_ch1) = adc_dma.split();

    asm::bkpt();
    loop {}
}

循环 DMA 工作原理:

复制代码
     缓冲区 A          缓冲区 B
┌─────────────┐  ┌─────────────┐
│ [0] [1] ... │  │ [0] [1] ... │
│    [7]      │  │    [7]      │
└─────────────┘  └─────────────┘
       ↑ DMA 写入        ↑ DMA 写入
       └── 交替进行 ──┘

Half::First  → 缓冲区 A 可读
Half::Second → 缓冲区 B 可读

9. SPI 通信

rust 复制代码
#![deny(unsafe_code)]
#![allow(clippy::empty_loop)]
#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;

// SPI 模式定义
pub const MODE: Mode = Mode {
    phase: Phase::CaptureOnSecondTransition,  // CPHA = 1
    polarity: Polarity::IdleHigh,              // CPOL = 1
};  // SPI Mode 3 (CPOL=1, CPHA=1)

use stm32f1xx_hal::{
    gpio::{Output, PA4},
    pac::{Peripherals, SPI1},
    prelude::*,
    spi::{Mode, Phase, Polarity, Spi},
};

fn setup() -> (Spi<SPI1, u8>, PA4<Output>) {
    let dp = Peripherals::take().unwrap();
    let mut rcc = dp.RCC.constrain();
    let mut gpioa = dp.GPIOA.split(&mut rcc);

    // SPI1 默认引脚
    let sck = gpioa.pa5;   // 时钟信号
    let miso = gpioa.pa6;  // 主入从出
    let mosi = gpioa.pa7;  // 主出从入
    let cs = gpioa.pa4.into_push_pull_output(&mut gpioa.crl);  // 片选(手动控制)

    // 如果要使用 PB3/PB4/PB5(重映射),需要:
    // .remap(&mut afio.mapr)

    let spi = dp
        .SPI1
        .spi(
            (Some(sck), Some(miso), Some(mosi)),  // 使用的引脚
            MODE,                                  // SPI 模式
            1.MHz(),                               // 时钟频率
            &mut rcc,
        );

    (spi, cs)  // CS 引脚手动控制
}

#[entry]
fn main() -> ! {
    let (_spi, _cs) = setup();
    loop {}
}

SPI 模式详解:

模式 CPOL CPHA 时钟空闲 数据采样
Mode 0 0 0 第一个边沿
Mode 1 0 1 第二个边沿
Mode 2 1 0 第一个边沿
Mode 3 1 1 第二个边沿

DKX 板 SPI1 引脚: PA5(SCK), PA6(MISO), PA7(MOSI), PA4(CS)


10. I2C 通信

rust 复制代码
#![no_std]
#![no_main]

use panic_probe as _;
use rtt_target::{rprint, rprintln, rtt_init_print};
use cortex_m_rt::entry;
use stm32f1xx_hal::{self as hal, gpio::GpioExt, i2c::I2c, pac, prelude::*};

// I2C 有效地址范围(0x08-0x77 是标准设备地址)
const VALID_ADDR_RANGE: core::ops::Range<u8> = 0x08..0x78;

#[entry]
fn main() -> ! {
    rtt_init_print!();  // 初始化 RTT 调试输出
    let dp = pac::Peripherals::take().unwrap();
    let mut rcc = dp.RCC.constrain();

    let gpiob = dp.GPIOB.split(&mut rcc);

    // I2C1 配置
    // PB6 = SCL (时钟线)
    // PB7 = SDA (数据线)
    let scl = gpiob.pb6;
    let sda = gpiob.pb7;
    let mut i2c = I2c::new(
        dp.I2C1,
        (scl, sda),
        hal::i2c::Mode::standard(100.kHz()),  // 标准模式 100kHz
        &mut rcc,
    );

    rprintln!("Start i2c scanning...");
    rprintln!();

    // 扫描总线上的所有设备地址
    for addr in 0x00_u8..0x80 {
        let byte: [u8; 1] = [0; 1];
        // 尝试向每个地址写入一个字节
        // 如果 ACK 说明设备存在
        if VALID_ADDR_RANGE.contains(&addr) && i2c.write(addr, &byte).is_ok() {
            rprint!("{:02x}", addr);  // 找到设备,打印地址
        } else {
            rprint!("..");            // 无设备
        }
        if addr % 0x10 == 0x0F {
            rprintln!();
        } else {
            rprint!(" ");
        }
    }

    rprintln!();
    rprintln!("Done!");
    loop {}
}

I2C 地址扫描输出示例:

复制代码
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
10: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
20: .. .. .. .. .. .. .. .. 28 .. .. .. .. .. .. ..
30: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
40: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
50: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
60: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
70: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..

(地址 0x28 处有设备,比如 MPU6050/MPU9250 等)

调试输出方式对比:

方式 速度 需要额外硬件
Semihosting hprintln!() 很慢 调试器
RTT rprintln!() 调试器
defmt defmt::info!() 最快 调试器
UART write!() 中等 USB-TTL
ITM iprintln!() SWO 线

11. PWM 输出与输入

11.1 pwm.rs --- PWM输出

rust 复制代码
#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut rcc = p.RCC.constrain();
    let gpioa = p.GPIOA.split(&mut rcc);

    // TIM2 通道引脚
    let c1 = gpioa.pa0;  // 通道 1
    let c2 = gpioa.pa1;  // 通道 2
    let c3 = gpioa.pa2;  // 通道 3

    // 创建 PWM 管理器
    let (mut pwm_mgr, (pwm_c1, pwm_c2, pwm_c3, ..)) = p.TIM2.pwm_hz(1.kHz(), &mut rcc);

    // 将 PWM 通道绑定到引脚并使能
    let mut c1 = pwm_c1.with(c1);
    c1.enable();
    let mut c2 = pwm_c2.with(c2);
    c2.enable();
    let mut c3 = pwm_c3.with(c3);
    c3.enable();

    // 调整 PWM 周期
    pwm_mgr.set_period(ms(500).into_rate());  // 改为 500ms
    asm::bkpt();

    pwm_mgr.set_period(1.kHz());  // 恢复 1kHz
    asm::bkpt();

    // 获取最大占空比值(取决于定时器的分辨率)
    let max = pwm_mgr.get_max_duty();

    // 设置不同占空比
    c3.set_duty(max);       // 100% 占空比(最亮/最快)
    asm::bkpt();

    c3.set_duty(max / 4);   // 25% 占空比(较暗/较慢)
    asm::bkpt();

    c3.set_duty(0);         // 0% 占空比(熄灭/停止)
    asm::bkpt();

    loop {}
}

定时器 PWM 通道与引脚映射(DKX 板):

定时器 CH1 CH2 CH3 CH4
TIM2 PA0 PA1 PA2 PA3
TIM3 PA6 PA7 PB0 PB1
TIM4 PB6 PB7 PB8 PB9

PWM 应用:

  • LED 调光
  • 电机速度控制
  • 舵机角度控制(50Hz, 占空比 2.5%-12.5%)
  • 音频信号生成

11.2 pwm_input.rs --- PWM输入捕获

测量外部 PWM 信号的频率和占空比。

rust 复制代码
#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut rcc = p.RCC.constrain();
    let mut afio = p.AFIO.constrain(&mut rcc);
    let mut dbg = p.DBGMCU;

    let gpioa = p.GPIOA.split(&mut rcc);
    let gpiob = p.GPIOB.split(&mut rcc);

    // 禁用 JTAG 释放 PB4/PB5
    let (_pa15, _pb3, pb4) = afio.mapr.disable_jtag(gpioa.pa15, gpiob.pb3, gpiob.pb4);
    let pb5 = gpiob.pb5;

    // TIM3 配置为 PWM 输入模式
    let pwm_input = p.TIM3.remap(&mut afio.mapr).pwm_input(
        (pb4, pb5),                                        // PB4=IC1, PB5=IC2
        &mut dbg,
        Configuration::Frequency(10.kHz()),                 // 参考频率 10kHz
        &mut rcc,
    );
    let timer_clk = <pac::TIM3 as RccBus>::Bus::timer_clock(&rcc.clocks);

    loop {
        // 读取输入信号的频率
        let _freq = pwm_input
            .read_frequency(ReadMode::Instant, timer_clk)
            .unwrap();
        // 读取输入信号的占空比
        let _duty_cycle = pwm_input.read_duty(ReadMode::Instant).unwrap();
    }
}

11.3 qei.rs --- 正交编码器接口

读取旋转编码器的位置和速度。

rust 复制代码
#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let cp = cortex_m::Peripherals::take().unwrap();
    let mut rcc = dp.RCC.constrain();

    let gpiob = dp.GPIOB.split(&mut rcc);

    // TIM4 配置为 QEI 模式
    let c1 = gpiob.pb6;  // 编码器 A 相
    let c2 = gpiob.pb7;  // 编码器 B 相

    let qei = Timer::new(dp.TIM4, &mut rcc).qei((c1, c2), QeiOptions::default());
    let mut delay = cp.SYST.delay(&rcc.clocks);

    loop {
        let before = qei.count();
        delay.delay_ms(1_000_u16);  // 等待 1 秒
        let after = qei.count();

        // 1 秒内计数的变化量 = 转速(每秒脉冲数)
        let elapsed = after.wrapping_sub(before) as i16;
        rprintln!("{}", elapsed);
    }
}

用途: 电机编码器、旋转旋钮等正交编码器设备的速度/位置测量。


12. DAC 数模转换

注意:STM32F103C8T6 (DKX 板) 没有 DAC,DAC 仅在高密度设备(STM32F103xC/D/E)上可用。

rust 复制代码
// DAC_OUT1 = PA4, DAC_OUT2 = PA5
let pa4 = gpioa.pa4.into_analog(&mut gpioa.crl);
let pa5 = gpioa.pa5.into_analog(&mut gpioa.crl);

let (mut ch1, mut ch2) = dp.DAC.constrain((pa4, pa5), &mut rcc);
ch1.enable();
ch2.enable();

// DAC 是 12 位:0 (0V) ~ 4095 (≈VREF)
ch1.set_value(2048);  // 输出 1.65V
ch2.set_value(4095);  // 输出 3.3V

13. CRC 校验

rust 复制代码
#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut rcc = p.RCC.constrain();
    let mut crc = p.CRC.new(&mut rcc);

    crc.reset();                // 复位 CRC 计算器
    crc.write(0x12345678);      // 写入数据

    let val = crc.read();       // 读取 CRC 结果
    rprintln!("found={:08x}, expected={:08x}", val, 0xdf8a8a2b_u32);

    loop {}
}

用途: 数据完整性校验、通信协议的 CRC 校验。


14. CAN 总线

rust 复制代码
use bxcan::Fifo;
use bxcan::filter::Mask32;

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let mut flash = dp.FLASH.constrain();

    // CAN 需要外部晶振保证时钟精度
    let mut rcc = dp.RCC.freeze(rcc::Config::hse(8.MHz()), &mut flash.acr);

    let mut can1 = {
        let gpioa = dp.GPIOA.split(&mut rcc);
        let rx = gpioa.pa11;  // CAN RX
        let tx = gpioa.pa12;  // CAN TX

        let can = dp.CAN.can(dp.USB, (tx, rx), &mut rcc);

        // 配置位时序:125kBit/s, 采样点 87.5%
        bxcan::Can::builder(can)
            .set_bit_timing(0x001c_0003)
            .leave_disabled()
    };

    // 配置过滤器(接收所有帧)
    let mut filters = can1.modify_filters();
    filters.enable_bank(0, Fifo::Fifo0, Mask32::accept_all());
    drop(filters);

    // 使能 CAN
    let mut can = can1;
    block!(can.enable_non_blocking()).unwrap();

    // 回环测试:接收帧后立即发回
    loop {
        if let Ok(frame) = block!(can.receive()) {
            block!(can.transmit(&frame)).unwrap();
        }
    }
}

CAN 引脚(DKX 板): PA11(CAN RX), PA12(CAN TX) --- 注意与 USB 引脚共用


15. USB 串口

15.1 usb_serial.rs --- USB轮询串口

rust 复制代码
#![no_std]
#![no_main]

extern crate panic_semihosting;

use cortex_m::asm::delay;
use cortex_m_rt::entry;
use stm32f1xx_hal::usb::{Peripheral, UsbBus};
use stm32f1xx_hal::{pac, prelude::*, rcc};
use usb_device::prelude::*;
use usbd_serial::{SerialPort, USB_CLASS_CDC};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let mut flash = dp.FLASH.constrain();

    // USB 必须使用 48MHz 系统时钟
    let mut rcc = dp.RCC.freeze(
        rcc::Config::hse(8.MHz()).sysclk(48.MHz()).pclk1(24.MHz()),
        &mut flash.acr,
    );

    assert!(rcc.clocks.usbclk_valid());  // 验证 USB 时钟有效

    let mut gpioc = dp.GPIOC.split(&mut rcc);
    let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
    led.set_high();  // 关灯

    let mut gpioa = dp.GPIOA.split(&mut rcc);

    // USB D+ 线上有上拉电阻
    // 开发时需要拉低 D+ 触发 USB RESET
    let mut usb_dp = gpioa.pa12.into_push_pull_output(&mut gpioa.crh);
    usb_dp.set_low();                           // 拉低 D+
    delay(rcc.clocks.sysclk().raw() / 100);     // 短暂延时

    // 配置 USB 外设
    let usb = Peripheral {
        usb: dp.USB,
        pin_dm: gpioa.pa11,                                  // USB DM = PA11
        pin_dp: usb_dp.into_floating_input(&mut gpioa.crh),  // USB DP = PA12
    };
    let usb_bus = UsbBus::new(usb);

    // 创建 CDC-ACM 串口设备
    let mut serial = SerialPort::new(&usb_bus);

    // 构建 USB 设备
    let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd))
        .device_class(USB_CLASS_CDC)
        .strings(&[StringDescriptors::default()
            .manufacturer("Fake Company")
            .product("Serial port")
            .serial_number("TEST")])
        .unwrap()
        .build();

    loop {
        if !usb_dev.poll(&mut [&mut serial]) {
            continue;
        }

        let mut buf = [0u8; 64];
        match serial.read(&mut buf) {
            Ok(count) if count > 0 => {
                led.set_low();  // 点亮 LED
                // 将收到的字符转为大写后回显
                for c in buf[0..count].iter_mut() {
                    if 0x61 <= *c && *c <= 0x7a {
                        *c &= !0x20;  // 'a'~'z' → 'A'~'Z'
                    }
                }
                // 写回(可能需要多次写入)
                let mut write_offset = 0;
                while write_offset < count {
                    match serial.write(&buf[write_offset..count]) {
                        Ok(len) if len > 0 => {
                            write_offset += len;
                        }
                        _ => {}
                    }
                }
            }
            _ => {}
        }
        led.set_high();  // 关灯
    }
}

USB 关键点:

  • 必须 48MHz 系统时钟(USB 协议要求精确时钟)
  • PA11 = USB D-, PA12 = USB D+
  • 开发时需要手动触发 USB RESET
  • VID/PID 0x16c0:0x27dd 是测试用的非正式 ID
  • 需要 release 模式编译(debug 模式 FLASH 会溢出)

15.2 usb_serial_interrupt.rs --- USB中断串口

使用中断方式处理 USB 通信。

rust 复制代码
// 全局 USB 对象
static mut USB_BUS: Option<UsbBusAllocator<UsbBusType>> = None;
static mut USB_SERIAL: Option<SerialPort<UsbBusType>> = None;
static mut USB_DEVICE: Option<UsbDevice<UsbBusType>> = None;

#[entry]
fn main() -> ! {
    // ... USB 初始化代码 ...

    // 使能 USB 中断
    unsafe {
        NVIC::unmask(Interrupt::USB_HP_CAN_TX);   // 高优先级
        NVIC::unmask(Interrupt::USB_LP_CAN_RX0);  // 低优先级
    }

    loop { wfi(); }  // 所有工作在中断中完成
}

#[interrupt]
fn USB_HP_CAN_TX() {
    usb_interrupt();
}

#[interrupt]
fn USB_LP_CAN_RX0() {
    usb_interrupt();
}

fn usb_interrupt() {
    let usb_dev = unsafe { USB_DEVICE.as_mut().unwrap() };
    let serial = unsafe { USB_SERIAL.as_mut().unwrap() };

    if !usb_dev.poll(&mut [serial]) {
        return;
    }

    let mut buf = [0u8; 8];
    match serial.read(&mut buf) {
        Ok(count) if count > 0 => {
            // 处理接收到的数据
            for c in buf[0..count].iter_mut() {
                if 0x61 <= *c && *c <= 0x7a {
                    *c &= !0x20;  // 转大写
                }
            }
            serial.write(&buf[0..count]).ok();
        }
        _ => {}
    }
}

轮询 vs 中断:

  • 轮询:简单,但 CPU 一直在忙等
  • 中断:CPU 可以休眠,节省功耗

16. 附录:常用概念总结

Rust 嵌入式项目模板

rust 复制代码
#![no_std]
#![no_main]

use panic_halt as _;  // panic 处理器
use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    // 1. 获取外设
    let dp = pac::Peripherals::take().unwrap();
    let cp = cortex_m::Peripherals::take().unwrap();

    // 2. 配置时钟
    let mut rcc = dp.RCC.constrain();

    // 3. 配置 GPIO
    let mut gpioa = dp.GPIOA.split(&mut rcc);

    // 4. 主循环
    loop {
        // 你的代码
    }
}

时钟配置

配置方法 说明 适用场景
Config::hsi() 内部 RC 振荡器 8MHz 简单应用,无需外部晶振
Config::hse(8.MHz()) 外部晶振 8MHz USB、CAN 等需要高精度时钟
.sysclk(72.MHz()) 系统时钟 最高性能
.sysclk(48.MHz()) 系统时钟 USB 需要 48MHz
.pclk1(36.MHz()) APB1 时钟 最大 36MHz
.pclk2(72.MHz()) APB2 时钟 最大 72MHz
.adcclk(14.MHz()) ADC 时钟 最大 14MHz

GPIO 模式速查

方法 模式 用途
into_push_pull_output() 推挽输出 驱动 LED、控制引脚
into_push_pull_output_with_state() 带初始状态推挽输出 同上
into_open_drain_output() 开漏输出 I2C、单总线
into_pull_up_input() 上拉输入 按键(接地触发)
into_pull_down_input() 下拉输入 按键(接 VCC 触发)
into_floating_input() 浮空输入 外部有上下拉的信号
into_analog() 模拟输入 ADC 采集
into_alternate_push_pull() 复用推挽输出 USART TX, SPI SCK
into_alternate_open_drain() 复用开漏输出 I2C SCL/SDA
into_dynamic() 动态模式 运行时切换模式

常用 Panic 处理器

处理器 说明 调试输出
panic_halt 静默停止
panic_semihosting 通过 semihosting 输出 需调试器
panic_probe probe-rs 支持 probe-rs
panic_itm 通过 ITM 输出 需 SWO

各外设与 DMA 通道映射

DMA1 通道 外设
通道 1 ADC1, TIM2_CH3
通道 2 SPI1_RX, USART3_TX
通道 3 SPI1_TX, USART3_RX
通道 4 SPI2_RX, USART1_TX
通道 5 SPI2_TX, USART1_RX
通道 6 USART2_RX, TIM1_CH1
通道 7 USART2_TX, TIM1_CH2

编译注意事项

bash 复制代码
# 必须使用 thumbv7m-none-eabi 目标
rustup target add thumbv7m-none-eabi

# release 编译(代码更小)
cargo build --release

# USB 项目可能需要 release 模式(debug FLASH 溢出)
cargo build --release --features stm32f103
相关推荐
拎得清n14 小时前
寄存器点灯
单片机·嵌入式硬件
破晓单片机1 天前
067、STM32项目分享:语音儿童学习书桌系统
stm32·单片机·嵌入式硬件
10WTW011 天前
微机原理 8259A 可编程中断控制器
单片机·嵌入式硬件
ServBay1 天前
别再用初级写法写Rust了,8个写法你值得拥有
后端·rust
破晓单片机1 天前
068、STM32项目分享:智能小区门禁系统
stm32·单片机·嵌入式硬件
望眼欲穿的程序猿1 天前
Hello World
嵌入式硬件·rust
ACP广源盛139246256731 天前
GSV5600@ACP#多接口协议转换芯片,物理 AI 便携终端的互联核心
大数据·人工智能·分布式·嵌入式硬件·spark
望眼欲穿的程序猿1 天前
ESP32-S3 定时器中断
单片机·嵌入式硬件
电气_空空1 天前
基于 LabVIEW 的深海气密采水器测控系统
单片机·嵌入式硬件·毕业设计·labview
牛牛,牛1 天前
榨干最后一微安:STM32 的低功耗设计与中断唤醒机制深度剖析
单片机·嵌入式硬件