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"]
目录
- 项目模板与开发环境
- 时钟系统详解
- 入门基础
- [hello.rs - Hello World](#hello.rs - Hello World)
- [panics.rs - 异常处理](#panics.rs - 异常处理)
- [led.rs - 点亮LED](#led.rs - 点亮LED)
- [blinky.rs - LED闪烁(定时器)](#blinky.rs - LED闪烁(定时器))
- [delay.rs - LED闪烁(SysTick延迟)](#delay.rs - LED闪烁(SysTick延迟))
- [delay-timer-blinky.rs - TIM2延迟闪烁](#delay-timer-blinky.rs - TIM2延迟闪烁)
- [GPIO 输入输出](#GPIO 输入输出)
- [外部中断 EXTI](#外部中断 EXTI)
- 定时器与中断
- [blinky_timer_irq.rs - TIM2中断闪烁(裸机)](#blinky_timer_irq.rs - TIM2中断闪烁(裸机))
- [timer-interrupt-rtic.rs - RTIC定时器中断](#timer-interrupt-rtic.rs - RTIC定时器中断)
- [rtic2-tick.rs - RTIC2 异步任务](#rtic2-tick.rs - RTIC2 异步任务)
- 串口通信
- [serial.rs - 串口回环测试](#serial.rs - 串口回环测试)
- [serial-fmt.rs - 串口格式化输出](#serial-fmt.rs - 串口格式化输出)
- [serial-interrupt-idle.rs - 串口中断+空闲检测](#serial-interrupt-idle.rs - 串口中断+空闲检测)
- [serial_9bits.rs - 9位串口通信](#serial_9bits.rs - 9位串口通信)
- [serial-dma-rx.rs - 串口DMA接收](#serial-dma-rx.rs - 串口DMA接收)
- [ADC 模数转换](#ADC 模数转换)
- [adc.rs - ADC基础读取](#adc.rs - ADC基础读取)
- [adc_temperature.rs - 温度传感器](#adc_temperature.rs - 温度传感器)
- [adc-dma-circ.rs - ADC循环DMA](#adc-dma-circ.rs - ADC循环DMA)
- [SPI 通信](#SPI 通信)
- [I2C 通信](#I2C 通信)
- [PWM 输出与输入](#PWM 输出与输入)
- [DAC 数模转换](#DAC 数模转换)
- [CRC 校验](#CRC 校验)
- [CAN 总线](#CAN 总线)
- [USB 串口](#USB 串口)
- [usb_serial.rs - USB轮询串口](#usb_serial.rs - USB轮询串口)
- [usb_serial_interrupt.rs - USB中断串口](#usb_serial_interrupt.rs - USB中断串口)
- 附录:常用概念总结
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-7cfg_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--- ADCUSB--- 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())--- 设置系统时钟为 48MHzrcc.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 |
关键步骤总结:
- 配置引脚为输入
make_interrupt_source()--- 将引脚连接到 EXTI 线trigger_on_edge()--- 设置触发边沿enable_interrupt()--- 使能 EXTI 中断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()--- 拆分为独立的Tx和Rx对象.reunite()--- 将Tx和Rx重新合并
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