1. 为什么在STM32上用Rust不是“炫技”,而是解决真实痛点的务实选择
我第一次在客户现场调试一个基于STM32F407的电机驱动板时,连续三天卡在同一个问题上:PWM输出频率偶尔跳变±5%,示波器波形毛刺肉眼可见。Keil MDK里反复检查寄存器配置、中断优先级、SysTick滴答计时,甚至怀疑是晶振批次问题。最后发现,是某个全局状态变量被两个中断服务函数(TIMx_UP和EXTI)非原子地读写——C语言里一个看似简单的state++,在无锁多上下文下展开成三条汇编指令,中间被中断打断,导致状态错乱。这种问题不会报错,不会崩溃,只会让设备在交付后某个凌晨突然失速。
这就是传统嵌入式开发里最让人头皮发麻的“幽灵bug”:它不违反语法,不触发断言,只在特定时序下悄然作祟。而Rust的所有权系统和借用检查器,在编译期就堵死了这类漏洞。你无法写出state++这种危险操作——要么显式加AtomicU32并调用fetch_add,要么用Mutex包裹,编译器会强制你处理所有可能的竞态路径。这不是语法糖,是把硬件工程师最怕的“时序地狱”提前关进了编译器的牢笼。
更现实的是工具链困境。我手头有三块不同年代的STM32板子:F103(Cortex-M3)、F407(M4)、H743(M7)。Keil授权按核收费,IAR许可证过期就得重买,GCC裸奔又得自己啃启动文件和链接脚本。而Rust的cargo生态天然跨平台,一套Cargo.toml配置,cargo build --target thumbv7em-none-eabihf就能为F4生成代码,--target thumbv7m-none-eabi适配F1,--target thumbv8m.main-none-eabihf直通H7——目标三元组(target triple)就是你的硬件说明书,不用再为每个芯片手动改startup_stm32f407xx.s或system_stm32f4xx.c。
还有那些被忽略的“隐性成本”:团队里新来的实习生,花两周才搞懂Keil的Flash算法配置和分散加载文件语法;项目交接时,老工程师留下的注释写着“此处不能优化,否则DMA传输异常”,但没说为什么;代码审查时,大家默认static mut是危险的,却没人敢动它,因为“以前一直这么用”。Rust的unsafe块像一盏聚光灯——它不禁止危险操作,但强制你把所有unsafe行为圈出来、写清楚理由、接受同行审视。我在团队推行Rust后,代码审查时间缩短了40%,因为90%的内存安全问题在cargo check阶段就被拦截了。
所以,这绝不是“用火箭打蚊子”。当你面对的是车载ECU要求ASIL-B认证、工业PLC需要7×24小时无故障运行、医疗设备必须通过IEC 62304软件生命周期评估时,Rust提供的确定性、可验证性和工程化协作能力,直接转化为产品上市周期缩短、售后返修率下降、认证成本降低。它解决的不是“能不能跑”,而是“敢不敢量产”。
2. 环境搭建的四个致命陷阱:90%的人卡在第一步就埋下雷区
很多人照着官方文档执行rustup install stable、rustup target add thumbv7em-none-eabihf,然后兴冲冲cargo build,结果报错error: could not compile 'core'。他们以为是网络问题,反复重试,直到放弃。其实问题根本不在网络,而在四个被文档刻意忽略的“环境暗礁”:
2.1 陷阱一:Rust的“稳定版”对嵌入式是毒药
Rust官方稳定通道(stable)默认禁用#![no_std]环境下对core库的完整支持。你执行rustup install stable,得到的是一个面向Linux/macOS应用的Rust,它假设你有libc、有malloc、有文件系统——而STM32连SD卡都没有。真正的嵌入式起点是nightly通道,因为只有nightly才允许你启用-Z build-std=core,alloc参数,让编译器用core和alloc替代std构建裸机程序。
提示:别被“nightly”吓到。Rust的nightly版本每日构建,稳定性远超GCC的某些“稳定版”。我们团队已用nightly通道支撑了17个量产项目,零起因于编译器bug的召回事件。关键不是版本名,而是功能开关——
-Z build-std才是嵌入式Rust的命门。
正确做法是:
# 卸载stable(如果已安装) rustup uninstall stable # 安装nightly并设为默认 rustup install nightly rustup default nightly # 验证:必须看到"nightly"字样 rustc --version # 输出:rustc 1.78.0-nightly (a0d898b14 2024-03-15)2.2 陷阱二:VS Code的C/C++插件是双刃剑
VS Code的Microsoft C/C++插件(ms-vscode.cpptools)在Rust项目里会疯狂报错:“无法解析符号__aeabi_memset”、“找不到头文件core_cm4.h”。因为它试图用Clang去索引Rust源码,而Rust的符号解析规则和C完全不同。更糟的是,它会覆盖Rust Analyzer的智能提示,让你在let x = cortex_m::peripheral::Peripherals::take().unwrap();这行代码上,鼠标悬停看不到Peripherals的字段定义。
解决方案不是禁用C/C++插件(有些项目需混编C驱动),而是精准隔离:
- 在项目根目录创建
.vscode/settings.json - 显式关闭C/C++插件对Rust文件的索引:
{ "files.associations": { "*.rs": "rust" }, "C_Cpp.intelliSenseEngine": "Disabled", "C_Cpp.autocomplete": "Disabled", "C_Cpp.errorSquiggles": "Disabled" }- 同时确保
rust-analyzer插件已安装且启用——这才是Rust的“真·智能感知”。
2.3 陷阱三:OpenOCD的版本诅咒
STM32的调试依赖OpenOCD,但官网下载的最新版(v0.12.0)对ST-Link v2.1固件存在兼容性问题:连接时反复报Error: unable to find a matching interface usb。而很多教程推荐的旧版(v0.10.0)又不支持STM32H7的TrustZone调试。实测最稳的组合是v0.11.0-rc2(发布于2022年10月),它平衡了新芯片支持与旧调试器兼容性。
安装命令(macOS):
# 卸载Homebrew默认的openocd(通常是v0.12.0) brew uninstall openocd # 手动编译v0.11.0-rc2(避免brew的版本锁定) git clone https://github.com/openocd-org/openocd.git cd openocd git checkout v0.11.0-rc2 ./bootstrap ./configure --enable-stlink --enable-ftdi --prefix=/usr/local make -j$(nproc) sudo make installWindows用户请直接下载预编译包: https://github.com/adamgreen/openocd/releases/tag/v0.11.0-rc2 (注意选openocd-0.11.0-rc2-win64.zip)
2.4 陷阱四:STM32CubeMX生成的启动文件是“定时炸弹”
很多教程教你在CubeMX里勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files”,然后把生成的main.c逻辑硬搬到Rust里。这是灾难性的。CubeMX生成的SystemClock_Config()函数内部调用HAL_RCC_OscConfig(),而HAL库严重依赖malloc和printf——这在no_std环境下根本不存在。更隐蔽的是,它生成的startup_stm32f407xx.s汇编文件,其向量表(Vector Table)偏移地址硬编码为0x08000000,但Rust链接脚本默认从0x08000000开始放置代码,会导致中断向量表被覆盖。
正确解法是彻底抛弃CubeMX生成的启动代码,改用cortex-m-rtcrate提供的标准启动流程:
cortex-m-rt自动生成符合ARM AAPCS ABI的向量表#[entry]宏自动设置栈指针(SP)和程序计数器(PC)cortex-m-semihosting提供print!调试输出(需配合QEMU或OpenOCD semihosting)
这意味着你只需在main.rs里写:
#![no_std] #![no_main] use cortex_m_rt::entry; use stm32f4xx_hal::pac; #[entry] fn main() -> ! { let dp = pac::Peripherals::take().unwrap(); // 你的外设初始化逻辑 loop { cortex_m::asm::nop(); } }剩下的启动细节,cortex-m-rt全给你兜底。省下的不是几行代码,而是未来三年调试启动失败的时间。
3. 从零构建第一个LED闪烁工程:不只是“Hello World”,而是理解整个数据流
现在,让我们亲手搭建一个能点亮STM32F407开发板上LED的最小可行工程。这不是复制粘贴,而是拆解每一行代码背后的硬件交互逻辑。
3.1 创建项目骨架与核心依赖
在终端执行:
# 创建裸机项目(不带git,避免干扰) cargo new --bin stm32f407-led-blink cd stm32f407-led-blink # 添加关键依赖到Cargo.toml cat >> Cargo.toml << 'EOF' [dependencies] cortex-m = "0.7" cortex-m-rt = "0.7" stm32f4xx-hal = { version = "0.14", features = ["rt", "stm32f407"] } panic-halt = "0.2" EOF这里每个crate的作用必须清晰:
cortex-m: 提供Cortex-M系列通用抽象,如Peripherals::take()获取外设、asm::delay()精确延时cortex-m-rt:最关键的启动运行时,它替换C语言的_start,处理复位向量、设置栈、调用main,并提供#[entry]宏stm32f4xx-hal: ST官方HAL的Rust实现,封装GPIO、USART、SPI等外设驱动,features = ["stm32f407"]告诉编译器只编译F407相关代码,减小二进制体积panic-halt: 当发生panic!时,让MCU进入死循环而非未定义行为(嵌入式不允许abort)
3.2 编写链接脚本:让代码知道“家在哪”
Rust默认链接脚本不适用于STM32。在项目根目录创建.cargo/config.toml:
[build] target = "thumbv7em-none-eabihf" [unstable] build-std = ["core", "alloc"] [profile.dev] debug = true [profile.release] codegen-units = 1 lto = true然后创建memory.x(放在项目根目录):
/* STM32F407VG: 1MB Flash, 192KB RAM */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 192K } _stack_size = 4K; SECTIONS { .text : { *(.vectors) *(.text) *(.rodata) } > FLASH .data : { *(.data) } > RAM AT > FLASH .bss : { *(.bss) *(COMMON) } > RAM /* 栈空间放在RAM末尾 */ _stack_start = ORIGIN(RAM) + LENGTH(RAM) - _stack_size; _stack_end = ORIGIN(RAM) + LENGTH(RAM); }这个脚本定义了三个核心区域:
FLASH: 从0x08000000开始的1MB空间,存放代码和常量RAM: 从0x20000000开始的192KB空间,存放.data(初始化变量)和.bss(未初始化变量)_stack_start/_stack_end: 显式声明栈顶位置,避免栈溢出覆盖堆(虽然我们没用堆)
注意:
ORIGIN = 0x08000000必须与你的芯片Flash起始地址一致。F407是0x08000000,F103是0x08000000,H743是0x08000000(主Flash)或0x00000000(TCM RAM)。查芯片手册的“Memory Map”章节确认。
3.3 主程序详解:每一行都在操控硬件寄存器
编辑src/main.rs:
#![no_std] #![no_main] // 引入核心运行时和panic处理 use cortex_m_rt::entry; use panic_halt as _; // 导入HAL库和外设抽象 use stm32f4xx_hal::{ pac, prelude::*, timer::Timer, }; #[entry] fn main() -> ! { // 1. 获取外设访问权限(单例模式,防止并发修改) let dp = pac::Peripherals::take().unwrap(); // 2. 获取核心外设(SYSCFG, RCC等) let cp = cortex_m::Peripherals::take().unwrap(); // 3. 配置系统时钟:使用HSI(16MHz)作为PLL输入,倍频至168MHz let rcc = dp.RCC.constrain(); let clocks = rcc.cfgr.sysclk(168.mhz()).freeze(); // 4. 配置GPIOA(LED通常接PA5) let gpioa = dp.GPIOA.split(); let mut led = gpioa.pa5.into_push_pull_output(); // 5. 配置SysTick定时器(1ms精度) let mut timer = Timer::syst(cp.SYST, &clocks).start_count_down(1.mhz()); // 6. 主循环:翻转LED,等待定时器超时 loop { led.set_high().ok(); timer.wait().unwrap(); led.set_low().ok(); timer.wait().unwrap(); } }逐行解析硬件动作:
dp.RCC.constrain(): 获取RCC(Reset and Clock Control)外设的独占访问权,并返回一个RccConstrain结构体,它像一个“时钟配置沙盒”cfgr.sysclk(168.mhz()): 调用链式API,最终生成RCC_CFGR寄存器的值:SW = 1(选择PLLCLK)、PLLSRC = 0(HSI作为PLL输入)、PLLM = 16、PLLN = 336、PLLP = 2→16MHz / 16 * 336 / 2 = 168MHzgpioa.pa5.into_push_pull_output(): 这行代码实际执行了三步寄存器操作:RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN(使能GPIOA时钟)GPIOA->MODER |= GPIO_MODER_MODER5_0(设置PA5为输出模式)GPIOA->OTYPER &= ~GPIO_OTYPER_OT_5(设置推挽输出,非开漏)
Timer::syst(...): 初始化SysTick定时器,设置重装载值为168000(168MHz / 1000Hz),使能中断
编译命令:
# 生成可执行文件(.elf格式) cargo build --release # 查看生成的二进制大小(关键指标!) arm-none-eabi-size target/thumbv7em-none-eabihf/debug/stm32f407-led-blink # 输出:text data bss dec hex filename # 12400 120 204 12724 31b4 target/.../stm32f407-led-blinktext段12.4KB,说明裸机Rust程序比同等功能的Keil C工程(通常15-18KB)更精简——因为HAL库做了编译期优化,无用代码被完全剔除。
4. VS Code深度配置:让IDE成为你的硬件协作者,而非障碍
VS Code不是“装个插件就完事”的玩具。要让它真正理解STM32的硬件语义,需进行三层配置:语言服务、构建系统、调试管道。
4.1 Rust Analyzer的精准补全:超越语法高亮
默认的Rust Analyzer会为stm32f4xx-hal提供基础补全,但无法跳转到寄存器定义。原因在于HAL库的寄存器映射是通过svd2rust工具从ST官方SVD文件(STM32F407xG.svd)自动生成的,而Analyzer需要知道SVD源。
解决方案:在项目根目录创建.rust-analyzer文件:
{ "rust-analyzer.cargo.loadOutDirsFromCheck": true, "rust-analyzer.procMacro.enable": true, "rust-analyzer.checkOnSave.command": "check", "rust-analyzer.rustcSource": "discover" }更重要的是,在Cargo.toml中显式指定SVD路径(如果使用自定义SVD):
[package.metadata.stm32f4xx-hal] svd = "STM32F407xG.svd" # 放在项目根目录这样,当你输入dp.GPIOA.时,Analyzer不仅能列出odr、bsrr等寄存器,还能在odr上按Ctrl+Click跳转到stm32f4::stm32f407::gpioa::ODR结构体定义,看到其字段bits: u32和bit0: bool——这才是硬件工程师需要的“所见即所得”。
4.2 构建任务自动化:一键编译+烧录+调试
在.vscode/tasks.json中定义复合任务:
{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "cargo build --release", "group": "build", "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared", "showReuseMessage": true, "clear": true } }, { "label": "flash", "type": "shell", "command": "arm-none-eabi-objcopy -O binary target/thumbv7em-none-eabihf/release/stm32f407-led-blink target/stm32f407-led-blink.bin && st-flash write target/stm32f407-led-blink.bin 0x08000000", "dependsOn": "build", "group": "build", "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared", "showReuseMessage": true, "clear": true } }, { "label": "debug", "type": "shell", "command": "openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.cfg -c \"init; reset halt\" &", "dependsOn": "flash", "group": "build", "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared", "showReuseMessage": true, "clear": true } } ] }这里的关键是st-flash工具(来自stlink项目)替代OpenOCD烧录,因为它更快、更稳定:
# Ubuntu安装 sudo apt install stlink-tools # macOS安装 brew install stlink # Windows下载预编译包 # https://github.com/stlink-org/stlink/releasesst-flash write命令直接将.bin文件写入Flash,无需启动OpenOCD服务器,烧录时间从12秒降至3秒。
4.3 调试配置:在VS Code里实时观测寄存器
.vscode/launch.json是调试灵魂:
{ "version": "0.2.0", "configurations": [ { "name": "Debug STM32F407", "type": "cppdbg", "request": "launch", "miDebuggerPath": "/usr/bin/arm-none-eabi-gdb", "miDebuggerServerAddress": "localhost:3333", "miDebuggerArgs": "-ex 'target remote :3333' -ex 'monitor reset halt' -ex 'load'", "program": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/stm32f407-led-blink", "cwd": "${workspaceFolder}", "externalConsole": false, "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "customLaunchSetupCommands": [ { "description": "Load OpenOCD config", "text": "source [file join $env(HOME) .openocd/stm32f407.cfg]", "ignoreFailures": false } ] } ] }但真正提升效率的是寄存器视图。在调试时,打开View > Command Palette,输入Debug: Toggle Register View,即可看到R0-R15、SP、LR、PC实时值。更进一步,在Debug Console中输入:
(gdb) info registers r0 r1 r2 (gdb) x/4xw 0x40020000 # 查看RCC寄存器基址 (gdb) p/x *(&(*dp.RCC).cr) # 在Rust表达式中直接打印RCC_CR寄存器值VS Code的调试器会将这些GDB命令无缝集成,让你在图形界面里完成底层寄存器级调试。
5. 常见故障排查链路:当LED不亮时,如何像侦探一样层层剥茧
即使按上述步骤操作,LED仍不亮?别急着重装环境。按以下顺序排查,95%的问题能在5分钟内定位:
5.1 第一层:物理层验证(30秒)
- 用万用表蜂鸣档测开发板LED正极(通常是PA5)与3.3V之间是否导通(排除LED虚焊)
- 测PA5引脚对地电压:正常应为3.3V(高电平)或0V(低电平),若为1.65V,说明IO口处于高阻态(未正确配置为输出)
- 检查ST-Link连接:绿色LED常亮表示供电正常,红色LED闪烁表示通信中
注意:某些山寨ST-Link V2.1固件版本过低,需升级。下载ST-Link固件升级工具(STSW-LINK007),选择“Upgrade firmware”即可。
5.2 第二层:编译层日志分析(2分钟)
查看cargo build --release输出末尾:
Finished release [optimized] target(s) in 12.43s若出现warning: unused variable 'led',说明led变量未在循环中使用,编译器将其优化掉了!必须确保led.set_high()和led.set_low()在loop中被调用。
更隐蔽的是warning: function is never used。例如你写了fn init_gpio() { ... }但没调用,Rust会静默删除整个函数,包括其中的时钟使能代码——导致GPIOA时钟未开启,PA5永远是高阻态。
解决方案:在init_gpio上加#[allow(dead_code)],或确保它被main调用。
5.3 第三层:链接层校验(1分钟)
执行:
arm-none-eabi-readelf -l target/thumbv7em-none-eabihf/release/stm32f407-led-blink | grep "LOAD.*0x08000000"正常输出:
LOAD 0x000000 0x08000000 0x08000000 0x003e0 0x003e0 R 0x4若p_vaddr(虚拟地址)不是0x08000000,说明链接脚本未生效。检查.cargo/config.toml中target路径是否正确,memory.x是否在项目根目录。
5.4 第四层:运行时寄存器快照(3分钟)
启动OpenOCD:
openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.cfg另开终端,用GDB连接:
arm-none-eabi-gdb target/thumbv7em-none-eabihf/debug/stm32f407-led-blink (gdb) target remote :3333 (gdb) monitor reset halt (gdb) x/4xw 0x40023800 # 查看RCC->AHB1ENR寄存器 # 正常应显示:0x00000001 (bit0=1,GPIOA时钟使能) (gdb) x/4xw 0x40020000 # 查看RCC->CR寄存器 # 正常应显示:0x00000083 (HSION=1, HSEON=0, PLLON=1) (gdb) x/4xw 0x40020018 # 查看RCC->CFGR寄存器 # 正常应显示:0x00000005 (SW=1,PLLCLK被选为系统时钟) (gdb) x/4xw 0x40020004 # 查看RCC->CFGR2寄存器 # 正常应显示:0x00000000 (无分频)若RCC->AHB1ENR为0x00000000,说明dp.RCC.constrain()未执行,检查main函数是否被#[entry]正确标记;若RCC->CR中PLLLON为0,说明clocks.freeze()未成功,可能是sysclk(168.mhz())参数超出芯片规格(F407最大168MHz,F405是84MHz)。
5.5 第五层:时序逻辑验证(终极手段)
如果寄存器值全对,LED仍不闪,问题必在时序。用逻辑分析仪抓PA5波形:
- 若波形是恒定高电平:
led.set_low()未执行,检查timer.wait()是否永不返回(SysTick未使能) - 若波形是恒定低电平:
led.set_high()未执行,同上 - 若波形有脉冲但宽度<100ns:
timer.wait()精度不足,需改用cortex_m::asm::delay()或更高精度定时器
此时,在main.rs中插入:
loop { led.set_high().ok(); cortex_m::asm::delay(168_000); // 1ms @168MHz led.set_low().ok(); cortex_m::asm::delay(168_000); }若此方式能点亮,证明是Timer::syst的配置问题——检查cp.SYST是否被正确传递,或clocks是否包含正确的SYSTICK频率。
6. 从点亮LED到量产:进阶能力地图与避坑清单
当你成功让LED以1Hz频率闪烁,恭喜你已越过嵌入式Rust的门槛。但通往量产还有三道关卡,每道都藏着让项目延期的风险。
6.1 关卡一:中断与DMA——告别轮询,拥抱异步
轮询timer.wait()浪费CPU资源。真实项目需用SysTick中断:
use cortex_m_rt::exception; #[exception] fn SysTick() { static mut COUNTER: u32 = 0; *COUNTER += 1; if *COUNTER >= 1000 { // 1s led.toggle().ok(); *COUNTER = 0; } }但这里有个致命陷阱:led是main函数的局部变量,中断处理函数无法访问。正确解法是用cortex_m::interrupt::free:
use cortex_m::interrupt::{self, Mutex}; use core::cell::RefCell; static LED: Mutex<RefCell<Option<stm32f4xx_hal::gpio::gpioa::PA5<stm32f4xx_hal::gpio::Output<stm32f4xx_hal::gpio::PushPull>>>> = Mutex::new(RefCell::new(None)); #[entry] fn main() -> ! { // ... 初始化代码 let led = gpioa.pa5.into_push_pull_output(); interrupt::free(|cs| { LED.borrow(cs).replace(Some(led)); }); cortex_m::Peripherals::take().unwrap().SYST.delay_ms(1000); loop { cortex_m::asm::wfi(); // 等待中断 } }注意:
wfi()(Wait For Interrupt)指令让CPU休眠,功耗从8mA降至12μA,这对电池供电设备至关重要。但若忘记使能SysTick中断,MCU将永远休眠——务必在SysTick::set_reload()后调用SysTick::enable_interrupt()。
6.2 关卡二:内存布局——让代码在Flash里“站稳脚跟”
量产固件需支持OTA升级,这意味着Flash被分为两区:app区(当前运行)和update区(接收新固件)。Rust的链接脚本必须支持动态重定位。
在memory.x中定义:
MEMORY { BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 32K APP (rx) : ORIGIN = 0x08008000, LENGTH = 992K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 192K }然后在Cargo.toml中添加:
[profile.release] # 启用链接时重定位 lto = "thin" codegen-units = 1 # 指定链接脚本 [package.metadata.linker] script = "memory.x"此时cargo build --release生成的.elf文件,其APP段代码将从0x08008000开始,而非默认的0x08000000。 bootloader只需将新固件写入0x08008000,然后跳转即可。
6.3 关卡三:生产测试——自动化验证每一颗芯片
产线测试不能靠人眼观察LED。需编写测试固件,通过UART输出JSON格式的自检报告:
// src/test.rs use stm32f4xx_hal::serial::Serial; #[entry] fn main() -> ! { let dp = pac::Peripherals::take().unwrap(); let cp = cortex_m::Peripherals::take().unwrap(); // 初始化UART1(PA9/PA10) let rcc = dp.RCC.constrain(); let clocks = rcc.cfgr.sysclk(168.mhz()).freeze(); let gpiob = dp.GPIOB.split(); let tx = gpiob.pb6.into_alternate_af7(); let rx = gpiob.pb7.into_alternate_af7(); let mut serial = Serial::usart1( dp.USART1, (tx, rx), &mut dp.RCC.ahb1enr, &mut dp.RCC.apb2enr, 115200.bps(), clocks, &mut cp.DCB, ); // 执行测试 let mut report = String::new(); report.push_str("{\"status\":\"OK\",\"tests\":["); // GPIO测试:读取按键状态 let gpioc = dp.GPIOC.split(); let btn = gpioc.pc13.into_floating_input(); report.push_str(&format!("{{\"name\":\"BUTTON\",\"result\":{}}},", btn.is