当前位置: 首页 > news >正文

rust嵌入式开发零基础入门教程(六)

我们继续 Rust 嵌入式开发零基础入门教程的第六部分。在前几节课中,我们已经掌握了环境搭建、Rust 核心概念、GPIO 控制、中断处理以及 UART 串行通信。现在,我们将学习嵌入式系统中另一个非常重要的外设:定时器(Timers)


17. 理解定时器(Timers)

定时器是微控制器中一个极其常用的外设,它能够实现各种时间相关的任务,如:

  • 精确延时: 比简单的 delay_ms() 更精确和灵活。

  • 周期性任务: 每隔一定时间触发一次任务(例如每 1 秒切换一次 LED)。

  • PWM (脉冲宽度调制): 控制电机速度、LED 亮度等模拟输出。

  • 输入捕获: 测量外部信号的脉冲宽度或频率。

  • 输出比较: 在特定时间点触发输出事件。

17.1 定时器的基本原理

定时器本质上是一个计数器。它由一个内部时钟源驱动,每个时钟周期,计数器的值就会增加(或减少)。当计数器达到一个预设的比较值(Compare Value)或者达到其最大值(溢出)时,它可以触发一个事件,通常是:

  • 设置一个标志位: 软件可以检测这个标志位。

  • 触发一个中断: 这是最常见的用法,定时器达到预设值时,CPU 会暂停当前任务,转而执行定时器中断服务程序 (ISR)。

17.2 重要的定时器概念

  • 时钟源: 定时器计数器前进的依据。通常来自系统时钟(或其分频),也可以是外部时钟。

  • 预分频器(Prescaler): 用于降低定时器时钟的频率。例如,如果系统时钟是 84MHz,预分频器设置为 84,那么定时器实际的时钟就是 1MHz,这意味着计数器每 1 微秒增加 1。

  • 自动重载寄存器(Auto-Reload Register, ARR): 定义了计数器达到多少时会溢出并重新开始计数。这个值决定了定时器的周期。

  • 计数模式: 向上计数、向下计数或中心对齐计数。

  • 比较寄存器(Capture/Compare Register, CCR): 当计数器达到 CCR 的值时,可以触发一个特定的事件,常用于 PWM 或输出比较。

计算定时器周期:

定时器周期 = (ARR 值 + 1) * (预分频器值 + 1) / 定时器时钟频率

例如:定时器时钟 1MHz (预分频器 84),ARR 设置为 9999。 周期 = (9999 + 1) * (84 + 1) / 84MHz = 10000 * 85 / 84000000 = 0.010119秒 ≈ 10 毫秒


18. 编写一个使用定时器实现 LED 闪烁的程序

在之前的教程中,我们使用 delay_ms() 来实现 LED 闪烁。现在,我们将用定时器来做这件事,让 LED 每隔 500 毫秒自动切换一次状态,而无需占用主循环。

18.1 硬件准备

  • STM32 Nucleo-64 系列开发板: (如 STM32F401RE 或 STM32F411RE)。

    • 板载绿色 LED 通常连接到 PA5 引脚。

18.2 修改 Cargo.toml (无需额外修改,沿用上一节的配置)

我们依然使用 stm32f4xx-hal 库,所以 Cargo.toml 不需要额外的修改。确保你的 Cargo.toml 包含正确的 HAL 库和 features

Ini, TOML

# hello-cortex-m/Cargo.toml
# ... (其他配置保持不变)[dependencies]
cortex-m-rt = "0.7.0"
panic-halt = "0.2.0"# 确保你的 HAL 库及其 features 配置正确
stm32f4xx-hal = { version = "0.15.0", features = ["stm32f401", "rt"] } # 根据你的芯片型号调整# ... (其他配置保持不变)

18.3 编写 src/main.rs (定时器 LED 闪烁)

现在,打开 src/main.rs 文件,并将其内容替换为以下代码。

Rust

#![no_std] // 不使用 Rust 标准库
#![no_main] // 不使用 Rust 的默认 main 函数use panic_halt as _; // panic 时停止 CPU// 导入核心运行时和中断相关宏
use cortex_m_rt::{entry, exception};
// 导入外设访问和HAL库
use stm32f4xx_hal::{gpio::{Output, PinState, PushPull},pac::{self, interrupt}, // 导入 PAC 外设和 interrupt 宏prelude::*, // 导入常用的 traittimer::Timer, // 导入定时器模块
};// 导入 Mutex 和 RefCell,用于在中断中安全访问 LED
use cortex_m::interrupt::Mutex;
use core::cell::RefCell;// 全局静态 Mutex,用于在中断中安全访问 LED
static G_LED: Mutex<RefCell<Option<stm32f4xx_hal::gpio::PA5<Output<PushPull>>>>> =Mutex::new(RefCell::new(None));#[entry]
fn main() -> ! {// 1. 获取对外设的访问权限let dp = pac::Peripherals::take().unwrap();let cp = cortex_m::Peripherals::take().unwrap(); // 内核外设,用于 NVIC// 2. 配置时钟let rcc = dp.RCC.constrain();// 假设系统时钟为 84MHzlet clocks = rcc.cfgr.use_hse(8.MHz()).sysclk(84.MHz()).freeze();// 3. 配置 GPIO (LED)let gpioa = dp.GPIOA.split();let mut led = gpioa.pa5.into_push_pull_output();led.set_low(); // 初始状态:熄灭// 4. 配置定时器 TIM3 (用于周期性中断)// STM32F401RE 的 TIM3 连接到 APB1 总线,通常时钟是 SYSCLK / 2 = 84MHz / 2 = 42MHz// 我们想要 500ms (0.5s) 触发一次中断// 周期 = (ARR + 1) * (PSC + 1) / 定时器时钟// 0.5s = (ARR + 1) * (PSC + 1) / 42_000_000 Hz// 简化:如果 PSC = 41999,那么分频后时钟为 42_000_000 / (41999 + 1) = 42_000_000 / 42_000 = 1000 Hz (1ms 计数一次)// 那么 ARR = 500 - 1 = 499 就能实现 500mslet mut timer = Timer::tim3(dp.TIM3, clocks).start_count_down(500.millis());// 5. 启用定时器更新中断// 当定时器计数器归零并重新加载时,会触发更新中断timer.listen();// 6. 将 LED 实例存入全局 Mutexcortex_m::interrupt::free(|cs| {*G_LED.borrow(cs).borrow_mut() = Some(led);});// 7. 启用 NVIC 中的 TIM3 中断unsafe {cp.NVIC.set_priority(interrupt::TIM3, 1); // 设置中断优先级cortex_m::peripheral::NVIC::unmask(interrupt::TIM3); // 启用中断}// 8. 主循环 (空闲)loop {// 进入低功耗模式,等待中断唤醒cortex_m::asm::wfi();}
}// 9. 定义定时器中断服务程序 (ISR)
#[interrupt]
fn TIM3() {cortex_m::interrupt::free(|cs| {// 清除定时器中断标志位,非常重要!// 否则中断会不断重复触发let mut timer = stm32f4xx_hal::timer::Tim3::new(pac::TIM3, &clocks).start_count_down(100.millis());// 重新获取定时器实例并清除中断标志// 注意: 在中断处理函数中重新初始化定时器是不合适的。// 正确的做法是获取一个指向 main 函数中 `timer` 变量的静态可变引用。// 但 HAL 库的 Timer `listen()` 方法通常不返回,// 且清除中断标志的方式取决于 HAL 库设计。// 对于 stm32f4xx-hal 0.15.0,通常需要直接操作 PAC 清除或通过 timer 实例的 clear_interrupt()// 这里需要更正,因为直接创建新 Timer 实例是错误的。// 正确的做法是:// let mut timer = G_TIMER.borrow(cs).borrow_mut(); // 如果你把 Timer 也放进 G_TIMER// timer.clear_interrupt(Event::Update); // 清除更新事件中断// 由于我们将 Timer 实例留在了 main 函数中,无法直接在这里引用 `timer`。// 我们可以直接通过 PAC 清除标志位 (稍微底层一些)。// 另一种更安全和简洁的方式是,HAL 库通常在 `listen()` 之后,// 提供一个方法来获取并操作定时器,或者直接操作 PAC。// 查阅 stm32f4xx-hal 的 Timer 中断处理示例会更准确。// 但为了本教程的通用性和易理解性,我们暂不将 timer 放入全局 Mutex。// 这里假设我们需要清除 TIM3 的更新中断标志位。// 这通常涉及到直接访问 PAC 的 TIM3 寄存器:let tim3 = unsafe { &*pac::TIM3::ptr() };tim3.sr.modify(|_r, w| w.uif().clear_bit()); // 清除更新中断标志位 (UIF)// 获取全局的 LED 实例if let Some(led_pin) = G_LED.borrow(cs).borrow_mut().as_mut() {// 切换 LED 的状态if led_pin.get_state() == PinState::High {led_pin.set_low();} else {led_pin.set_high();}}});
}// --- 修正 `TIM3()` 中断处理函数 ---
// 因为直接在 ISR 中访问 `main` 函数中的 `timer` 变量是很复杂的,
// 而且 `stm32f4xx-hal` 的 `listen()` 不返回定时器实例。
// 最简单的处理是直接清除中断标志位。
// 我们可以通过 PAC 访问定时器寄存器来清除更新中断标志位 (UIF)。// 修正后的 `TIM3` 中断处理函数
// 注意:这个修正不涉及将 Timer 放入全局变量,而是直接操作 PAC 清除中断。
// 这种方式对于简单的计时器更新中断是可行的,但如果需要在 ISR 中修改定时器配置,
// 则需要将 Timer 实例也放入全局 Mutex。
#[interrupt]
fn TIM3() {cortex_m::interrupt::free(|cs| {// 获取 TIM3 的 PAC 实例来清除中断标志位// 这是安全的,因为我们只是通过指针获取共享引用,没有修改其状态。let tim3 = unsafe { &*pac::TIM3::ptr() };// 清除更新中断标志位 (UIF)tim3.sr.modify(|_r, w| w.uif().clear_bit());// 获取全局的 LED 实例并切换其状态if let Some(led_pin) = G_LED.borrow(cs).borrow_mut().as_mut() {// 切换 LED 的状态if led_pin.get_state() == PinState::High {led_pin.set_low();} else {led_pin.set_high();}}});
}

代码解释 (新增/修改部分):

  1. 导入 timer::Timer: 这是 HAL 库中用于操作定时器的模块。

  2. 定时器初始化 (Timer::tim3(dp.TIM3, clocks)):

    • Timer::tim3(): 调用 HAL 库的函数来获取 TIM3 外设的实例。不同的 tim 外设对应不同的函数(如 tim1tim2 等)。

    • dp.TIM3: 传入 TIM3 外设的 PAC 实例。

    • clocks: 传入时钟配置,HAL 库会用它来计算定时器的分频值和重载值。

  3. start_count_down(500.millis()):

    • 将定时器配置为倒计时模式,并设置其周期为 500 毫秒。HAL 库会根据你提供的时钟 (clocks) 自动计算合适的预分频器 (Prescaler) 和自动重载寄存器 (ARR) 值,以达到这个周期。

  4. timer.listen();:

    • 启用定时器的更新中断。当定时器从 500ms 倒数到 0 并重新加载时,就会触发一个更新事件,从而触发这个中断。

  5. 启用 NVIC 中的 TIM3 中断:

    • cp.NVIC.set_priority(interrupt::TIM3, 1);cortex_m::peripheral::NVIC::unmask(interrupt::TIM3);: 告诉 NVIC 允许 TIM3 中断发生。TIM3 是 TIM3 定时器的中断向量。

  6. loop { cortex_m::asm::wfi(); }:

    • 主循环进入 wfi (Wait For Interrupt) 模式。这意味着微控制器会进入低功耗状态,直到定时器中断发生时才被唤醒,执行 ISR,然后再次进入 wfi。这样,LED 闪烁由定时器完全独立地控制,而主 CPU 可以做其他事情或进入睡眠以省电。

  7. #[interrupt] fn TIM3():

    • 这是专门为 TIM3 定时器更新事件编写的中断服务程序。

    • tim3.sr.modify(|_r, w| w.uif().clear_bit());: 这是最关键的一步! 在 ISR 的开头,你必须清除引起中断的标志位。tim3.sr 是 TIM3 的状态寄存器,uif() 是更新中断标志位。如果不清除,中断会立即再次触发,导致 CPU 陷入无限中断循环。这里我们通过直接操作 PAC 寄存器来清除标志位。

    • ISR 内部的逻辑与之前按钮控制 LED 的逻辑类似:获取全局 LED 实例,然后切换其状态。


19. 构建、烧录和测试

19.1 构建项目

保存 src/main.rs 文件后,在项目根目录(hello-cortex-m)下,运行构建命令:

Bash

cargo build --release

如果编译成功,则生成固件文件。

19.2 烧录并运行

确保你的开发板已通过 USB 连接到电脑,并且 probe-run 配置正确:

Bash

cargo run --release

probe-run 会自动编译(如果需要),烧录,并运行你的程序。

19.3 观察结果

烧录成功后,你应该会看到开发板上的绿色 LED 开始以大约每 0.5 秒亮灭一次的频率进行闪烁。


恭喜你!

你已经成功地使用 Rust 嵌入式程序中的定时器来实现周期性任务!这是嵌入式系统中非常核心的一个概念,它比简单的软件延时更加精确、高效,并且能让你的主程序更专注于执行主要逻辑,而不是等待时间。

下一步可以尝试:

  • 修改闪烁频率: 尝试改变 500.millis() 中的值,例如 1.secs() (1秒) 或 200.micros() (200微秒),观察 LED 闪烁速度的变化。

  • 使用多个定时器: 如果你的微控制器有多个定时器,尝试配置另一个定时器来控制不同的任务(例如另一个 LED 以不同频率闪烁)。

  • PWM (脉冲宽度调制): 如果你的板子支持,可以尝试配置定时器生成 PWM 信号,来控制 LED 的亮度。这需要将 GPIO 引脚配置为定时器通道的复用功能。

  • 输入捕获/输出比较: 了解定时器的其他高级功能,例如测量外部脉冲的宽度或在特定时间点触发输出。

在下一个教程中,我们可能会探讨更复杂的外设通信协议(如 I2C 或 SPI),或者ADC/DAC等模拟功能。

http://www.lryc.cn/news/597511.html

相关文章:

  • 什么是MySQL 视图
  • 综合实验(3)
  • 暑期自学嵌入式——Day06(C语言阶段)
  • 7月23日星期三今日早报简报微语报早读
  • 51c大模型~合集158
  • Vue 3 组件通信全解析:从 Props 到 Pinia 的深入实践
  • 用 llama.cpp 构建高性能本地 AI 应用:从环境搭建到多工具智能体开发全流程实战
  • Python应用指南:构建和获取全球地铁线路数据及可视化
  • ToBToC的定义与区别
  • 从 XSS 到 Bot 攻击:常见网络攻击防不胜防?雷池 WAF 用全场景防护为网站筑牢安全墙
  • Java中IO多路复用技术详解
  • S段和G段到底有什么区别
  • 基于springboot的乡村旅游在线服务系统/乡村旅游网站
  • 网络--VLAN技术
  • 在 Ubuntu 20.04.5 LTS 系统上安装 Docker CE 26.1.4 完整指南
  • OpenLayers 快速入门(五)Controls 对象
  • centos9 ssh能连接密码不对
  • 电脑32位系统能改64位系统吗
  • GoLand 项目从 0 到 1:第一天 —— 搭建项目基础架构与核心雏形
  • 抖音集团基于Flink的亿级RPS实时计算优化实践
  • 学生信息管理系统 - HTML实现增删改查
  • istio-proxy用哪个端口代理http流量的?
  • Vue 浏览器本地存储
  • 游戏盾 SDK 和游戏盾转发版有什么区别呢?​
  • Docker Desktop 打包Unity WebGL 程序,在Docker 中运行Unity WebGL 程序
  • SeaweedFS深度解析(二):从Master到Volume
  • 人工智能——Opencv图像色彩空间转换、灰度实验、图像二值化处理、仿射变化
  • AI项目实施落地实例
  • 直播一体机技术方案解析:基于RK3588S的硬件架构特性​
  • 如何加固Endpoint Central服务器的安全?(下)