项目介绍
本项目参与 Funpack 系列活动第 3 季第 3 期,在 G0B1RE 上 使用 X-NUCLEO-IKS4A1 扩展板,来实现一个利用 QVAR 触摸电极交互的数据展示系统.
组件介绍
X-NUCLEO-IKS4A1
X-NUCLEO-IKS4A1 是集成了许多 MEMS 传感器的板卡,其中包含
- LSM6DSO16IS:MEMS 3轴加速度计、陀螺仪
- LIS2MDL:MEMS 3轴磁感应强度计
- LIS2DUXS12:超低功耗 MEMS 3轴加速度计
- LPS22DF:低功耗高精度 MEMS 压强传感器
- SHT40AD1B:高精度超低功耗相对湿度和温度传感器
- STTS22H:低压超低功耗温度传感器
- LSM6DSV16X:MEMS 3轴加速度计、陀螺仪,可实现数据融合
- MKE001A:触摸电极副板
同时板卡上提供多种跳线选项,可为开发者评估不同方案提供便利.
设计与实现思路
架构
我们采用 Rust 来设计程序,利用 embassy
生态可以构造安全高效的嵌入式程序,但是这也意味着在裸机环境下,很多传感器库需要自己实现.
QVAR 状态机
连入 QVAR 传感器后,MCU 可以读到一个 i16
范围内的数值,我们需要跟踪数值的状态来判断用户的滑动方向. 代码如下所示,我们构造了一个 Swiper
状态机,其需要一个阈值和时间间隔作为状态跟踪的依据,通过与区间起点的值对比,就可以知道是向左滑动还是向右滑动
#[derive(Debug, Format)]
pub struct Swiper {
last: Option<(Instant, i16)>,
threshold: u16,
interval: Duration,
}
#[derive(Debug, Format)]
pub enum SwiperAction {
ClickLeft,
ClickRight,
SwipeLeft,
SwipeRight,
}
impl Swiper {
pub fn new(threshold: u16, interval: Duration) -> Self {
Self {
last: None,
threshold,
interval,
}
}
pub fn put(&mut self, value: i16) -> Option<SwiperAction> {
let now = Instant::now();
if value.checked_abs().map(|v| v as u16 >= self.threshold).unwrap_or(true) {
if let Some((last, last_value)) = self.last {
if now.duration_since(last) < self.interval {
if value.is_positive() == last_value.is_positive() {
return None
} else {
self.last = Some((now, value));
if value.is_positive() {
Some(SwiperAction::SwipeRight)
} else {
Some(SwiperAction::SwipeLeft)
}
}
} else {
self.last = Some((now, value));
None
}
} else {
self.last = Some((now, value));
None
}
} else {
None
}
}
}
传感器驱动
为了实现传感器的驱动,我们需要准备好所有相关设备的手册,然后阅读我们关心的部分.
一般来说 ST 的传感器最少的初始化标配,就是将相关功能的 CTRL 寄存器中有关 FS、ODR 字段进行设置,然后就是数值转换的因子以及 bias,整理了下列表格供参考.
R 为原始值 | 加速度 (mg) | 陀螺仪 (mdps) | 温度 (°C) | 相对湿度 (%) | 压强 (hPa) | 磁强度 (mGs) |
---|---|---|---|---|---|---|
LSM6DSO16IS | R * 0.061 | R * 4.375 | 25 + R / 256 | |||
LIS2MDL | 25 + R / 8 | R * 1.5 | ||||
LIS2DUXS12 | R * 0.061 (FS=±2g) | 25 + R * 0.045 | ||||
LPS22DF | R / 100 | R / 4096 | ||||
SHT40AD1B | -45 + 175 * R / 65535 | -6 + 125 * R / 65535 | ||||
STTS22H | R / 100 |
我们利用 bitfield
以及 embassy-stm32
库可以将 I2C 传感器手册中比较琐碎的寄存器映射给抽象成比较方便的视角,例如下面是一个 LSM6DSO16IS 的驱动库,完整的驱动库可以见附件中 src/sensors
目录下的模块.
use bitfield::bitfield;
use embassy_stm32::i2c::{I2c, Instance, Error as I2cError};
use embassy_stm32::mode::{Mode, Async};
use super::EndianRead;
pub struct Device<'a, 't, T: Instance> {
i2c: &'a mut I2c<'t, T, Async>,
address: u8,
}
#[derive(Debug)]
pub enum Error {
IdMismatch,
I2c(I2cError)
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::IdMismatch => write!(f, "id mismatch"),
Error::I2c(e) => write!(f, "i2c error: {:?}", e),
}
}
}
impl From<I2cError> for Error {
fn from(error: I2cError) -> Self {
Self::I2c(error)
}
}
type Result<T> = core::result::Result<T, Error>;
bitfield! {
/// e
#[derive(Default)]
pub struct Ctrl1(u8);
impl Debug;
odr, set_odr: 7, 4;
fs, set_fs: 3, 2;
}
bitfield! {
#[derive(Default)]
pub struct Ctrl2(u8);
impl Debug;
odr, set_odr: 7, 4;
fs, set_fs: 3, 2;
fs_125, set_fs_125: 1;
}
impl<'a, 't, T: Instance> Device<'a, 't, T> {
pub const ID: u8 = 0x22;
pub const REGISTER_WHO_AM_I: u8 = 0x0f;
pub const REGISTER_CTRL1: u8 = 0x10;
pub const REGISTER_CTRL2: u8 = 0x11;
pub const REGISTER_OUT_TEMP_L: u8 = 0x20;
pub const REGISTER_OUT_TEMP_H: u8 = 0x21;
pub const REGISTER_OUTX_L_G: u8 = 0x22;
pub const REGISTER_OUTX_H_G: u8 = 0x23;
pub const REGISTER_OUTY_L_G: u8 = 0x24;
pub const REGISTER_OUTY_H_G: u8 = 0x25;
pub const REGISTER_OUTZ_L_G: u8 = 0x26;
pub const REGISTER_OUTZ_H_G: u8 = 0x27;
pub const REGISTER_OUTX_L_A: u8 = 0x28;
pub const REGISTER_OUTX_H_A: u8 = 0x29;
pub const REGISTER_OUTY_L_A: u8 = 0x2a;
pub const REGISTER_OUTY_H_A: u8 = 0x2b;
pub const REGISTER_OUTZ_L_A: u8 = 0x2c;
pub const REGISTER_OUTZ_H_A: u8 = 0x2d;
pub const TEMPERATURE_MIN: i32 = -40;
pub const TEMPERATURE_MAX: i32 = 85;
pub const TEMPERATURE_ZERO: i32 = 25;
pub const TEMPERATURE_FACTOR: i32 = 256;
pub async fn new(i2c: &'a mut I2c<'t, T, Async>, address: u8) -> Result<Self> {
let mut instance = Self {
i2c,
address,
};
if instance.whoami().await? != Self::ID {
Err(Error::IdMismatch)
} else {
Ok(instance)
}
}
pub fn new_unchecked(i2c: &'a mut I2c<'t, T, Async>, address: u8) -> Self {
Self {
i2c,
address,
}
}
async fn read_le<R: EndianRead>(&mut self, addresses: &[u8]) -> Result<R> {
let mut buf = R::Array::default();
for (i, address) in addresses.iter().enumerate() {
self.i2c.write(self.address, &[*address]).await?;
self.i2c.read(self.address, &mut buf.as_mut()[i..i+1]).await?;
}
Ok(R::from_le_bytes(buf))
}
pub async fn whoami(&mut self) -> Result<u8> {
let mut buf = [0; 1];
self.i2c.write(self.address, &[Self::REGISTER_WHO_AM_I]).await?;
self.i2c.read(self.address, &mut buf).await?;
Ok(buf[0])
}
pub async fn write_ctrl1(&mut self) -> Result<()> {
let mut ctrl = Ctrl1::default();
ctrl.set_odr(0b1010);
ctrl.set_fs(0b00);
self.i2c.write(self.address, &[Self::REGISTER_CTRL1, ctrl.0]).await?;
Ok(())
}
pub async fn write_ctrl2(&mut self) -> Result<()> {
let mut ctrl = Ctrl2::default();
ctrl.set_odr(0b1010);
ctrl.set_fs_125(true);
self.i2c.write(self.address, &[Self::REGISTER_CTRL2, ctrl.0]).await?;
Ok(())
}
pub async fn read_accelerometer(&mut self) -> Result<(f64, f64, f64)> {
let sensitivity: f64 = 0.061;
let x: i16 = self.read_le(&[Self::REGISTER_OUTX_L_A, Self::REGISTER_OUTX_H_A]).await?;
let y: i16 = self.read_le(&[Self::REGISTER_OUTY_L_A, Self::REGISTER_OUTY_H_A]).await?;
let z: i16 = self.read_le(&[Self::REGISTER_OUTZ_L_A, Self::REGISTER_OUTZ_H_A]).await?;
Ok((x as f64 * sensitivity, y as f64 * sensitivity, z as f64 * sensitivity))
}
pub async fn read_gyroscope(&mut self) -> Result<(f64, f64, f64)> {
let sensitivity: f64 = 4.375;
let x: i16 = self.read_le(&[Self::REGISTER_OUTX_L_G, Self::REGISTER_OUTX_H_G]).await?;
let y: i16 = self.read_le(&[Self::REGISTER_OUTY_L_G, Self::REGISTER_OUTY_H_G]).await?;
let z: i16 = self.read_le(&[Self::REGISTER_OUTZ_L_G, Self::REGISTER_OUTZ_H_G]).await?;
Ok((x as f64 * sensitivity, y as f64 * sensitivity, z as f64 * sensitivity))
}
pub async fn read_temperature(&mut self) -> Result<f64> {
let value: i16 = self.read_le(&[Self::REGISTER_OUT_TEMP_L, Self::REGISTER_OUT_TEMP_H]).await?;
Ok(Self::TEMPERATURE_ZERO as f64 + value as f64 / Self::TEMPERATURE_FACTOR as f64)
}
}
传感器初始化与读取
我们在主程序中这样初始化设备,这样确保对应地址上的设备的 ID 与驱动中的 ID 匹配,并且将有关寄存器进行预配置.
async fn init<'a, 'b, T: Instance>(i2c: &'a mut I2c<'b, T, Async>) {
let mut device = sensors::lsm6dso16is::Device::new(i2c, 0x6a).await.unwrap();
device.write_ctrl1().await.unwrap();
device.write_ctrl2().await.unwrap();
let mut device = sensors::lis2mdl::Device::new(i2c, 0x1e).await.unwrap();
device.write_cfg_a().await.unwrap();
let mut device = sensors::lis2duxs12::Device::new(i2c, 0x19).await.unwrap();
device.write_ctrl5().await.unwrap();
let mut device = sensors::lps22df::Device::new(i2c, 0x5d).await.unwrap();
device.write_ctrl1().await.unwrap();
let mut device = sensors::sht40ad1b::Device::new(i2c, 0x44).await.unwrap();
let mut device = sensors::stts22h::Device::new(i2c, 0x38).await.unwrap();
device.write_ctrl().await.unwrap();
let mut device = sensors::lsm6dsv16x::Device::new(i2c, 0x6b).await.unwrap();
device.write_ctrl1().await.unwrap();
device.write_ctrl2().await.unwrap();
device.write_ctrl7().await.unwrap();
}
因为在设计驱动时,考虑到每一次都需要重新检查 ID 比较迂腐,因此在后续读取时将直接使用 new_unchecked
来避免不必要的总线数据传输.
fn writeln_3d_tuple_f64<const N: usize>(s: &mut String<N>, t: (f64, f64, f64)) -> fmt::Result {
writeln!(s, "[{:.3},{:.3},{:.3}]", t.0, t.1, t.2)
}
let mut device = sensors::lsm6dso16is::Device::new_unchecked(i2c, 0x6a);
let accelerometer = device.read_accelerometer().await.unwrap();
let gyroscope = device.read_gyroscope().await.unwrap();
let temperature = device.read_temperature().await.unwrap();
write!(&mut s, "A=");
writeln_3d_tuple_f64(&mut s, accelerometer);
write!(&mut s, "G=");
writeln_3d_tuple_f64(&mut s, gyroscope);
writeln!(&mut s, "T={}", temperature);