Rust for STM32 Developer – 第 2 节 – PWM 与 I2C

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() 方法访问的,无法直接在调试器中查看其正确值。

例如,在任务中设下断点:

image

便可以在变量栏中看到所有可观测变量值,也可以修改其值。可以在监视栏中添加变量名来监视其值。

image

Shared 资源推荐使用 defmt 追踪,或使用 cortex-m-rtic-trace crate 来追踪其值。后者大多用于嵌入式系统的实时调试和性能分析,可以在不影响系统运行的情况下收集和分析数据,在控制领域使用较多。不过多数据分析仍然建议使用 UART 输出的方式获得更佳的调试体验。

其实在 Static 中能够直接看到 Shared 资源的值,但是只能被观测一次,后续数据发生变化也不会更新。原因不明。

data of 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Ω 的上拉电阻,所以可以直接使用。

flowchart LR subgraph POWER[电源与地线] VCC[3.3V 电源轨] GND[GND 地线轨] end subgraph MCU[STM32F103C8T6(Blue Pill)] PB6[PB6 / I2C1_SCL] PB7[PB7 / I2C1_SDA] MCU_VCC[3.3V] MCU_GND[GND] end subgraph SENSOR[MPU6050 模块] SCL[SCL] SDA[SDA] SENSOR_VCC[VCC] SENSOR_GND[GND] end VCC --- MCU_VCC VCC --- SENSOR_VCC GND --- MCU_GND GND --- SENSOR_GND PB6 --- SCL PB7 --- SDA classDef power fill:#ffe6e6,stroke:#d33,stroke-width:1px; classDef signal fill:#e6f2ff,stroke:#268bd2,stroke-width:1px; class VCC,GND,MCU_VCC,MCU_GND,SENSOR_VCC,SENSOR_GND power; class PB6,PB7,SCL,SDA signal;

现在去除无关代码,还原模板状态。

#![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;
        }
    }
}

后续仍可通过计算得到倾角仰角和偏航角,转换为四元数,使用互补滤波器或卡尔曼滤波器来融合加速度计和陀螺仪的数据,从而得到更稳定的姿态估计。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇