PWM 与 I2C
本节内容仍然使用 STM32F103C8T6 作为实验板。
呼吸灯
在没有 DAC 的情况下,我们可以使用 PWM 来模拟一个 DAC,从而实现类似于呼吸灯的效果。通过改变 PWM 的占空比,我们可以控制 LED 的亮度,从而实现呼吸灯的效果。
PWM 的原理不过多介绍,具体可以参考 脈衝寬度調變 – Wikipedia 的相关资料。
先核对一下上节的源代码:
#![deny(unsafe_code)]
#![no_main]
#![no_std]
// Print panic message to probe console
use defmt_rtt as _;
use panic_probe as _;
// Create a monotonic timer based on the SysTick peripheral with a tick frequency of 1 kHz (1 ms)
rtic_monotonics::systick_monotonic!(Mono, 1_000);
#[rtic::app(device = stm32f1xx_hal::pac, peripherals = true, dispatchers = [EXTI0])]
mod app {
use crate::Mono;
use defmt::info;
use rtic_monotonics::systick::prelude::*;
use stm32f1xx_hal::{
gpio::{Output, PC13, PushPull}, prelude::*, rcc::Config
};
#[shared]
struct Shared {}
#[local]
struct Local {
led: PC13<Output<PushPull>>,
}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
let device = cx.device;
let mut flash = device.FLASH.constrain();
let rcc = device.RCC.constrain();
let mut rcc = rcc.freeze(
Config::hsi().sysclk(64.MHz()).pclk1(32.MHz()),
&mut flash.acr,
);
info!(
"sysclk={} Hz hclk={} Hz pclk1={} Hz pclk2={} Hz adcclk={} Hz",
rcc.clocks.sysclk().to_Hz(),
rcc.clocks.hclk().to_Hz(),
rcc.clocks.pclk1().to_Hz(),
rcc.clocks.pclk2().to_Hz(),
rcc.clocks.adcclk().to_Hz(),
);
let mut gpioc = device.GPIOC.split(&mut rcc);
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
led.set_high();
Mono::start(cx.core.SYST, 64_000_000);
led_control::spawn().unwrap();
(
Shared {
// Initialization of shared resources go here
},
Local {
// Initialization of local resources go here
led,
}
)
}
#[task(local = [led], priority = 1)]
async fn led_control(cx: led_control::Context) {
loop {
let mut now = Mono::now();
now += 1.secs();
cx.local.led.toggle();
info!("LED toggled!");
Mono::delay_until(now).await;
}
}
// Optional idle, can be removed if not needed.
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
// cortex_m::asm::wfi();
continue;
}
}
}
若要把 GPIO 输出调为 PWM 输出,需要使用定时器来生成 PWM 信号。STM32F103 系列的定时器可以配置为 PWM 模式,通过设置占空比来控制输出的电平时间,从而实现亮度调节。
需要注意的是,STM32F103 上
PC13引脚没有任何一个 Timer 可复用,所以需要外接一个 LED 来实现功能。此处我选择使用
PA0引脚来输出 PWM 信号,连接一个 LED。
要使用 PWM,先需要一个 AFIO (Alternate Function I/O)配置,将 GPIO 引脚配置为定时器的输出模式。然后,使用定时器的 CCR(Capture/Compare Register)来设置占空比。
定义 AFIO 配置和定时器初始化的代码如下:
let mut afio = device.AFIO.constrain(&mut rcc);
我们将使用 TIM2 定时器来生成 PWM 信号,配置代码如下:
let pwm = Timer2::new(device.TIM2, &mut rcc).pwm_hz::<Tim2NoRemap, _, _>(
gpioa.pa0.into_alternate_push_pull(&mut gpioa.crl),
&mut afio.mapr,
1.kHz(),
);
把 PA0 设置为 into_alternate_push_pull,表示将引脚配置为以复用功能推挽输出模式工作。afio.mapr 是 AFIO 的映射寄存器,用于配置引脚的复用功能。1.kHz() 表示 PWM 信号的分辨率为 1 kHz。
这样,PA0 就可以输出 PWM 信号了。
使用 split() 方法将 PWM 信号拆分为不同的通道,每个通道可以单独设置占空比和使能状态。由于只用一个通道来控制 LED,所以我们只需要拆分出(即接受)一个通道即可。然后把其占空比置零。注意,完成后需要调用 enable() 方法来使能该通道,否则 PWM 信号不会输出。
let mut ch1 = pwm.split();
ch1.set_duty(0);
ch1.enable();
然后导出这个外设到 Local 结构体中,以便在任务中使用。
注意:导出外设只需要导出受控的
ch1即可,不要导出 GPIO 引脚,因为其已被 PWM 配置消耗。
配置 Local 结构体如下:
ch1: PwmChannel<TIM2, { C1 }>,
ch1 的类型是 PwmChannel<TIM2, { C1 }>,表示这是 TIM2 的第一个通道。这个配置看起来比较抽象。
假设其它任务需要使用其占空比参数,可以导出一个 intensity 字段来表示 LED 的亮度。
Shared {
intensity: ch1.get_duty(),
}
在 Shared 结构体中定义之:
#[shared]
struct Shared {
intensity: u16,
}
这里导出的 intensity 其实本来就用不上,不过为了演示如何在 RTIC 中共享资源,还是定义了它。
最后在 led_control 任务中,使用一个简单的算法来改变占空比,从而实现呼吸灯的效果。
#[task(local = [led, ch1], shared = [intensity], priority = 1)]
async fn led_control(mut cx: led_control::Context) {
const PHASE_STEP: f32 = TAU / 2000.0;
let mut step: u32 = 0;
loop {
cx.local.led.toggle();
info!("led toggled, step={}", step);
step = step.wrapping_add(1);
let wave = libm::sinf(step as f32 * PHASE_STEP).abs();
let max = cx.local.ch1.get_max_duty();
let duty = (wave * max as f32) as u16;
cx.shared.intensity.lock(|intensity| *intensity = duty);
cx.local.ch1.set_duty(duty);
Mono::delay(1.millis()).await;
}
}
这里我使用了 f(x) = |sin(x)| 函数映射到占空比上,从而实现了 1 个平滑的呼吸灯效果。step 用于控制正弦函数的输入,PHASE_STEP 控制了呼吸灯的周期。TAU 是 2\pi 常量,为一个正弦周期,除以 2000.0 表示呼吸灯的周期为 2 秒(对于 f(x) = |sin(x)| 来说严格最小周期为 1 秒)。
此处模拟地使用了 intensity 共享占空比参数,虽然没有实际用途,但是可以演示其传递方法:
可以观察到 cx.shared.intensity 是一个加锁的共享资源,使用 lock() 方法来访问它。lock() 方法会阻塞当前任务,直到获取到锁为止,这样可以保证在访问共享资源时不会发生数据竞争。
lock()这里的
lock()方法是 RTIC 提供的一个机制,用于在任务中安全地访问共享资源。它会在访问共享资源时自动加锁,确保在同一时间只有一个任务可以访问该资源,从而避免数据竞争和潜在的错误。与
std环境下的Mutex类似,RTIC 的lock()方法使用闭包消耗共享资源,退出时自动释放锁。
需要注意的是 no_std 没有 sinf 函数可用,所以需要提前加入一个 libm crate 来提供数学函数的支持。
cargo add libm
wrapping_add()Rust 初学者可能不太能理解
wrapping_add()的作用,尤其是在no_std环境下。
wrapping_add()是 Rust 中的一个方法,用于在整数溢出时进行环绕加法。也就是说,当加法结果超过该整数类型的最大值时,它会从最小值重新开始,而不会引发 panic 或错误。这在处理计数器或循环计数时非常有用。
由于没有去掉 cx.local.led.toggle(),所以 LED 会高频地闪烁,如果没有用可以去掉。
监视
上一节提到使用 probe-rs 调试功能来监视变量的值,但是没有使用过。这个监视变量值的要求比 GDB 的监视更加严格。
- 对于局部变量,直接写变量名即可,但是必须在作用域设置断点后才能访问到值,且不能使变量被消耗。
- 对于
Shared共享资源,无法正确获取到其值,因为它是通过lock()方法访问的,无法直接在调试器中查看其正确值。
例如,在任务中设下断点:
便可以在变量栏中看到所有可观测变量值,也可以修改其值。可以在监视栏中添加变量名来监视其值。
Shared资源推荐使用defmt追踪,或使用cortex-m-rtic-tracecrate 来追踪其值。后者大多用于嵌入式系统的实时调试和性能分析,可以在不影响系统运行的情况下收集和分析数据,在控制领域使用较多。不过多数据分析仍然建议使用 UART 输出的方式获得更佳的调试体验。其实在 Static 中能够直接看到
Shared资源的值,但是只能被观测一次,后续数据发生变化也不会更新。原因不明。
![]()
PWM 完整示例如下:
#![deny(unsafe_code)]
#![no_main]
#![no_std]
// Print panic message to probe console
use defmt_rtt as _;
use panic_probe as _;
// Create a monotonic timer based on the SysTick peripheral with a tick frequency of 1 kHz (1 ms)
rtic_monotonics::systick_monotonic!(Mono, 1_000);
#[rtic::app(device = stm32f1xx_hal::pac, peripherals = true, dispatchers = [EXTI0])]
mod app {
use core::f32::consts::TAU;
use crate::Mono;
use defmt::{flush, info};
use rtic_monotonics::systick::prelude::*;
use stm32f1xx_hal::{
gpio::{Output, PC13, PushPull},
pac::TIM2,
prelude::*,
rcc::Config,
timer::{PwmChannel, Tim2NoRemap, Timer2, C1},
};
#[shared]
struct Shared {
intensity: u16,
}
#[local]
struct Local {
led: PC13<Output<PushPull>>,
ch1: PwmChannel<TIM2, { C1 }>,
}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
let device = cx.device;
let mut flash = device.FLASH.constrain();
let rcc = device.RCC.constrain();
let mut rcc = rcc.freeze(
Config::hsi().sysclk(64.MHz()).pclk1(32.MHz()),
&mut flash.acr,
);
info!("sysclk={} Hz hclk={} Hz pclk1={} Hz pclk2={} Hz adcclk={} Hz",
rcc.clocks.sysclk().to_Hz(),
rcc.clocks.hclk().to_Hz(),
rcc.clocks.pclk1().to_Hz(),
rcc.clocks.pclk2().to_Hz(),
rcc.clocks.adcclk().to_Hz(),
);
let mut gpioa = device.GPIOA.split(&mut rcc);
let mut gpioc = device.GPIOC.split(&mut rcc);
let mut afio = device.AFIO.constrain(&mut rcc);
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
let pwm = Timer2::new(device.TIM2, &mut rcc).pwm_hz::<Tim2NoRemap, _, _>(
gpioa.pa0.into_alternate_push_pull(&mut gpioa.crl),
&mut afio.mapr,
1.kHz(),
);
let mut ch1 = pwm.split();
ch1.set_duty(0);
ch1.enable();
led.set_high();
Mono::start(cx.core.SYST, rcc.clocks.hclk().to_Hz());
led_control::spawn().unwrap();
(
Shared {
intensity: ch1.get_duty(),
},
Local {
led,
ch1,
}
)
}
#[task(local = [led, ch1], shared = [intensity], priority = 1)]
async fn led_control(mut cx: led_control::Context) {
const PHASE_STEP: f32 = TAU / 2000.0;
let mut step: u32 = 0;
loop {
cx.local.led.toggle();
info!("led toggled.");
flush();
step = step.wrapping_add(1);
let wave = libm::sinf(step as f32 * PHASE_STEP).abs();
let max = cx.local.ch1.get_max_duty();
let duty = (wave * max as f32) as u16;
cx.shared.intensity.lock(|intensity| *intensity = duty);
cx.local.ch1.set_duty(duty);
Mono::delay(1.millis()).await;
}
}
// Optional idle, can be removed if not needed.
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
// cortex_m::asm::wfi();
continue;
}
}
}
出于严谨性考虑,把
Mono时钟设置的频率参数改为了rcc.clocks.hclk().to_Hz(),这样可以保证在不同的系统时钟频率下,Mono的时钟频率始终与系统时钟保持一致。
陀螺仪数据读取
MPU 6050 是一款常用的 6 轴陀螺仪传感器,集成了三轴陀螺仪和三轴加速度计。它通过 I2C 接口与微控制器通信,可以获取设备的角速度和加速度数据。
STM32F103C8T6 的 I2C 只有一对引脚可用,即 PB6(SCL)和 PB7(SDA)。在使用 I2C 通信时,需要将这两个引脚配置为开漏输出,并连接上拉电阻。Blue Pill 一般已经在板上集成了 4.7kΩ 的上拉电阻,所以可以直接使用。
现在去除无关代码,还原模板状态。
#![deny(unsafe_code)]
#![no_main]
#![no_std]
// Print panic message to probe console
use defmt_rtt as _;
use panic_probe as _;
// Create a monotonic timer based on the SysTick peripheral with a tick frequency of 1 kHz (1 ms)
rtic_monotonics::systick_monotonic!(Mono, 1_000);
#[rtic::app(device = stm32f1xx_hal::pac, peripherals = true, dispatchers = [EXTI0])]
mod app {
use crate::Mono;
use rtic_monotonics::systick::prelude::*;
use defmt::*;
use stm32f1xx_hal::{
prelude::*,
rcc::Config,
};
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
let device = cx.device;
let mut flash = device.FLASH.constrain();
let rcc = device.RCC.constrain();
let mut rcc = rcc.freeze(
Config::hsi().sysclk(64.MHz()).pclk1(32.MHz()),
&mut flash.acr,
);
info!("sysclk={} Hz hclk={} Hz pclk1={} Hz pclk2={} Hz adcclk={} Hz",
rcc.clocks.sysclk().to_Hz(),
rcc.clocks.hclk().to_Hz(),
rcc.clocks.pclk1().to_Hz(),
rcc.clocks.pclk2().to_Hz(),
rcc.clocks.adcclk().to_Hz(),
);
Mono::start(cx.core.SYST, rcc.clocks.hclk().to_Hz());
(
Shared {},
Local {}
)
}
// Optional idle, can be removed if not needed.
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
// cortex_m::asm::wfi();
continue;
}
}
}
同样地,就绪 GPIOB,并配置 PB6 和 PB7 为 I2C 的 SCL 和 SDA 引脚。I2C 选用标准速率 100kHz,要求引脚为开漏输出。
let pb6 = gpiob.pb6.into_alternate_open_drain(&mut gpiob.crl);
let pb7 = gpiob.pb7.into_alternate_open_drain(&mut gpiob.crl);
let i2c = device.I2C1.i2c(
(pb6, pb7),
Mode::standard(100.kHz()),
&mut rcc
);
方法 I2C1.i2c() 定义如下:
impl<I2C: Instance> I2cExt for I2C {
fn i2c(
self,
pins: (impl RInto<Self::Scl, 0>, impl RInto<Self::Sda, 0>),
mode: impl Into<Mode>,
rcc: &mut Rcc,
) -> I2c<Self> {
I2c::new(self, pins, mode, rcc)
}
}
此处需要的 pins 是一个元组,第一个元素是 SCL 引脚,第二个元素是 SDA 引脚,故上述 PB6 为 SCL,PB7 为 SDA。Mode 是 I2C 的工作模式,可以选择标准模式(100kHz)或快速模式(400kHz)。
使用 crl 配置寄存器,之前已经说过了,不再过多赘述。
导出类型为 stm32f1xx_hal::i2c::I2c<stm32f1xx_hal::pac::I2C1>>,注意多个 crate 之间的类型不兼容。
#[local]
struct Local {
i2c: I2c<I2C1>,
}
建立一个 task,读取 MPU6050 的数据。MPU6050 的 I2C 地址为 0x68 或 0x69,默认 0x68,可以通过读取寄存器来获取加速度和角速度数据。
#[task(local = [i2c], priority = 1)]
async fn read_mpu6050(cx: read_mpu6050::Context) {
use stm32f1xx_hal::i2c::{Address, Address::Seven};
const MPU6050_ADDR: Address = Seven(0x68);
// Wakeup MPU6050.
cx.local.i2c.write(MPU6050_ADDR, &[0x6B, 0x00]).unwrap_or_else(|e| {
error!("I2C init error: {:?}", defmt::Debug2Format(&e));
});
loop {
let mut buf = [0u8; 14];
cx.local.i2c.write_read(MPU6050_ADDR, &[0x3B], &mut buf).unwrap_or_else(|e| {
error!("I2C read error: {:?}", defmt::Debug2Format(&e));
});
info!("MPU6050 data: {:?}", buf);
Mono::delay(100.millis()).await;
}
}
对于 I2C 外设读写,成功概率不为 100%,所以需要使用 unwrap_or_else() 来处理错误,避免程序崩溃。write_read() 方法用于先写入寄存器地址,然后读取数据。
MPU6050 需要向
0x6B寄存器写入0x00来唤醒设备。
MPU6050 的数据寄存器从 0x3B 开始,连续读取 14 个字节的数据,分别为:
| 寄存器地址 | 数据内容 |
|---|---|
| 0x3B | ACCEL_XOUT_H |
| 0x3C | ACCEL_XOUT_L |
| 0x3D | ACCEL_YOUT_H |
| 0x3E | ACCEL_YOUT_L |
| 0x3F | ACCEL_ZOUT_H |
| 0x40 | ACCEL_ZOUT_L |
| 0x41 | TEMP_OUT_H |
| 0x42 | TEMP_OUT_L |
| 0x43 | GYRO_XOUT_H |
| 0x44 | GYRO_XOUT_L |
| 0x45 | GYRO_YOUT_H |
| 0x46 | GYRO_YOUT_L |
| 0x47 | GYRO_ZOUT_H |
| 0x48 | GYRO_ZOUT_L |
收集到调试数据:
[INFO ] MPU6050 data: [235, 136, 5, 20, 194, 96, 6, 0, 1, 43, 0, 241, 255, 220] (first_stm_f first-stm-f/src/main.rs:89)
有可能收集到错误,例如:
[ERROR] I2C read error: "ArbitrationLoss" (first_stm_f first-stm-f/src/main.rs:83)或
[ERROR] I2C read error: "NoAcknowledge(Address)" (first_stm_f first-stm-f/src/main.rs:86)其中
ArbitrationLoss表示总线仲裁丢失,可能是 I2C 设备卡死或掉电,此时可能需要断电重启 I2C 设备;NoAcknowledge(Address)表示设备没有响应,可能是受到外部干扰(手指触摸或短路)或其它不明的原因。收集解析错误是很必要的,否则会触发
panic!()进入HardFault,导致程序崩溃。若有其它错误类型,需自行排查错误来源。
接下来试着解析数据。先定义一个结构体来表示 MPU6050 的数据:
pub struct MPU6050 {
accel_x: f64,
accel_y: f64,
accel_z: f64,
temp: f64,
gyro_x: f64,
gyro_y: f64,
gyro_z: f64,
}
#[shared]
struct Shared {
mpu_data: MPU6050,
}
init 元组传回空数据:
(
Shared {
mpu_data: MPU6050 {
accel_x: 0.0,
accel_y: 0.0,
accel_z: 0.0,
temp: 0.0,
gyro_x: 0.0,
gyro_y: 0.0,
gyro_z: 0.0,
},
},
Local {
i2c,
}
)
task 使用并解析:
#[task(local = [i2c], shared = [mpu_data], priority = 1)]
注意:必须在定义结构体时使用
pub关键字,否则无法在Shared中使用。注意:将 task 的
cx设置为可变借用,即&mut cx,否则无法使用lock()方法修改共享资源。
使用 i16::from_be_bytes() 方法将两个字节的数组转换为一个有符号 16 位整数。from_be_bytes() 方法将字节数组解释为大端序(big-endian)格式,这与 MPU6050 的数据格式一致。
通过手册上的 LSB(Least Significant Bit)值,将原始数据转换为实际的物理量。加速度计的 LSB 值为 16384,陀螺仪的 LSB 值为 131,温度传感器的 LSB 值为 340。
const ACCEL_LSB: f64 = 16384.0; // LSB/g for ±2g
const GYRO_LSB: f64 = 131.0; // LSB/(°/s) for ±250°/s
...
cx.shared.mpu_data.lock(|mpu_data| {
mpu_data.accel_x = i16::from_be_bytes([buf[0], buf[1]]) as f64 / ACCEL_LSB;
mpu_data.accel_y = i16::from_be_bytes([buf[2], buf[3]]) as f64 / ACCEL_LSB;
mpu_data.accel_z = i16::from_be_bytes([buf[4], buf[5]]) as f64 / ACCEL_LSB;
mpu_data.temp = i16::from_be_bytes([buf[6], buf[7]]) as f64 / 340.0 + 36.53;
mpu_data.gyro_x = i16::from_be_bytes([buf[8], buf[9]]) as f64 / GYRO_LSB;
mpu_data.gyro_y = i16::from_be_bytes([buf[10], buf[11]]) as f64 / GYRO_LSB;
mpu_data.gyro_z = i16::from_be_bytes([buf[12], buf[13]]) as f64 / GYRO_LSB;
});
info!(
"Accel: ({} g, {} g, {} g), Temp: {} ℃, Gyro: ({}°/s, {}°/s, {}°/s)",
cx.shared.mpu_data.lock(|mpu_data| mpu_data.accel_x),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.accel_y),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.accel_z),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.temp),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.gyro_x),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.gyro_y),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.gyro_z),
);
得到可视的通用数据。
[INFO ] Accel: (-0.35986328125 g, 0.08984375 g, -0.952880859375 g), Temp: 40.388823529411766 ℃, Gyro: (1.1755725190839694°/s, 0.20610687022900764°/s, -0.2366412213740458°/s) (first_stm_f first-stm-f/src/main.rs:125)
完整代码如下:
#![deny(unsafe_code)]
#![no_main]
#![no_std]
// Print panic message to probe console
use defmt_rtt as _;
use panic_probe as _;
// Create a monotonic timer based on the SysTick peripheral with a tick frequency of 1 kHz (1 ms)
rtic_monotonics::systick_monotonic!(Mono, 1_000);
#[rtic::app(device = stm32f1xx_hal::pac, peripherals = true, dispatchers = [EXTI0])]
mod app {
use crate::Mono;
use rtic_monotonics::{Monotonic, systick::prelude::*};
use defmt::*;
use stm32f1xx_hal::{
i2c::{I2c, Mode}, pac::I2C1, prelude::*, rcc::Config
};
pub struct MPU6050 {
accel_x: f64,
accel_y: f64,
accel_z: f64,
temp: f64,
gyro_x: f64,
gyro_y: f64,
gyro_z: f64,
}
#[shared]
struct Shared {
mpu_data: MPU6050,
}
#[local]
struct Local {
i2c: I2c<I2C1>,
}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
let device = cx.device;
let mut flash = device.FLASH.constrain();
let rcc = device.RCC.constrain();
let mut rcc = rcc.freeze(
Config::hsi().sysclk(64.MHz()).pclk1(32.MHz()),
&mut flash.acr,
);
info!("sysclk={} Hz hclk={} Hz pclk1={} Hz pclk2={} Hz adcclk={} Hz",
rcc.clocks.sysclk().to_Hz(),
rcc.clocks.hclk().to_Hz(),
rcc.clocks.pclk1().to_Hz(),
rcc.clocks.pclk2().to_Hz(),
rcc.clocks.adcclk().to_Hz(),
);
let mut gpiob = device.GPIOB.split(&mut rcc);
let pb6 = gpiob.pb6.into_alternate_open_drain(&mut gpiob.crl);
let pb7 = gpiob.pb7.into_alternate_open_drain(&mut gpiob.crl);
let i2c = device.I2C1.i2c(
(pb6, pb7),
Mode::standard(100.kHz()),
&mut rcc
);
Mono::start(cx.core.SYST, rcc.clocks.hclk().to_Hz());
read_mpu6050::spawn().unwrap();
(
Shared {
mpu_data: MPU6050 {
accel_x: 0.0,
accel_y: 0.0,
accel_z: 0.0,
temp: 0.0,
gyro_x: 0.0,
gyro_y: 0.0,
gyro_z: 0.0,
}
},
Local {
i2c,
}
)
}
#[task(local = [i2c], shared = [mpu_data], priority = 1)]
async fn read_mpu6050(mut cx: read_mpu6050::Context) {
use stm32f1xx_hal::i2c::{Address, Address::Seven};
const MPU6050_ADDR: Address = Seven(0x68);
const ACCEL_LSB: f64 = 16384.0; // LSB/g for ±2g
const GYRO_LSB: f64 = 131.0; // LSB/(°/s) for ±250°/s
cx.local.i2c.write(MPU6050_ADDR, &[0x6B, 0x00]).unwrap_or_else(|e| {
error!("I2C init error: {:?}", defmt::Debug2Format(&e));
});
loop {
let mut buf = [0u8; 14];
cx.local.i2c.write_read(MPU6050_ADDR, &[0x3B], &mut buf).unwrap_or_else(|e| {
error!("I2C read error: {:?}", defmt::Debug2Format(&e));
});
// info!("MPU6050 data: {:?}", buf);
cx.shared.mpu_data.lock(|mpu_data| {
mpu_data.accel_x = i16::from_be_bytes([buf[0], buf[1]]) as f64 / ACCEL_LSB;
mpu_data.accel_y = i16::from_be_bytes([buf[2], buf[3]]) as f64 / ACCEL_LSB;
mpu_data.accel_z = i16::from_be_bytes([buf[4], buf[5]]) as f64 / ACCEL_LSB;
mpu_data.temp = i16::from_be_bytes([buf[6], buf[7]]) as f64 / 340.0 + 36.53;
mpu_data.gyro_x = i16::from_be_bytes([buf[8], buf[9]]) as f64 / GYRO_LSB;
mpu_data.gyro_y = i16::from_be_bytes([buf[10], buf[11]]) as f64 / GYRO_LSB;
mpu_data.gyro_z = i16::from_be_bytes([buf[12], buf[13]]) as f64 / GYRO_LSB;
});
info!(
"Accel: ({} g, {} g, {} g), Temp: {} ℃, Gyro: ({}°/s, {}°/s, {}°/s)",
cx.shared.mpu_data.lock(|mpu_data| mpu_data.accel_x),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.accel_y),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.accel_z),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.temp),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.gyro_x),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.gyro_y),
cx.shared.mpu_data.lock(|mpu_data| mpu_data.gyro_z),
);
Mono::delay(100.millis()).await;
}
}
// Optional idle, can be removed if not needed.
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
// cortex_m::asm::wfi();
continue;
}
}
}
后续仍可通过计算得到倾角仰角和偏航角,转换为四元数,使用互补滤波器或卡尔曼滤波器来融合加速度计和陀螺仪的数据,从而得到更稳定的姿态估计。


