Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = ["lab01", "lab02", "lab03", "lab04", "lab05"]
members = ["lab01", "lab02", "lab03", "lab04", "lab05", "lab06"]

[workspace.dependencies]
# Low level access to Cortex-M processors
Expand Down
8 changes: 8 additions & 0 deletions lab06/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[build]
target = "thumbv8m.main-none-eabihf"

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32U545RETxQ"

[env]
DEFMT_LOG = "debug"
32 changes: 32 additions & 0 deletions lab06/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "lab06"
version = "0.1.0"
edition = "2024"

[dependencies]
# Low level access to Cortex-M processors
cortex-m.workspace = true
# Boostrap crate for Cortex-M Processors
cortex-m-rt.workspace = true
# Defmt support
defmt.workspace = true
defmt-rtt.workspace = true
# Embedded HAL utilities
embassy-embedded-hal.workspace = true
# Async/await executor
embassy-executor.workspace = true
# Utilities for working with futures, compatible with no_std and not using alloc
embassy-futures.workspace = true
# STM32 HAL Implementation
embassy-stm32.workspace = true
# Synchronization primitives and data structures with async support
embassy-sync.workspace = true
# Timekeeping, delays and timeouts
embassy-time.workspace = true
embedded-graphics = "0.8.1"
embedded-hal = "1.0.0"
embedded-hal-async = "1.0.0"
heapless = "0.9.2"
mipidsi = "0.9.0"
# Panic handler that exits `probe-run` with an error code
panic-probe.workspace = true
46 changes: 46 additions & 0 deletions lab06/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.

// use std::env;
// use std::fs::File;
// use std::io::Write;
// use std::path::PathBuf;

fn main() {
// This section is not needed as the `embassy-stm32` crate
// provides `memory.x` and adds it to the linker's path.
//
// If using a crate that does not provide it, please
// uncomment the this section and the related imported
// items.

// Put `memory.x` in our output directory and ensure it's
// on the linker search path.
// let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
// File::create(out.join("memory.x"))
// .unwrap()
// .write_all(include_bytes!("memory.x"))
// .unwrap();
// println!("cargo:rustc-link-search={}", out.display());

// By default, Cargo will re-run a build script whenever
// any file in the project changes. By specifying `memory.x`
// here, we ensure the build script is only re-run when
// `memory.x` is changed.
// println!("cargo:rerun-if-changed=memory.x");

// Prevent the linker from page aligning segments
println!("cargo:rustc-link-arg-bins=--nmagic");

// Required for `cortex-m`
println!("cargo:rustc-link-arg-bins=-Tlink.x");
// Required for `defmt`
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
}
79 changes: 79 additions & 0 deletions lab06/src/bin/ex1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#![no_std]
#![no_main]

use defmt::info;
use defmt_rtt as _;
use embassy_executor::Spawner;
use embassy_stm32::{
bind_interrupts,
i2c::{self, I2c},
peripherals,
};
use embassy_time::Timer;
use panic_probe as _;

// For I2C to work, we need to bind the interrupts to the correct handlers.
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});

/// The lowest I2C address a device can have. Addresses below this are reserved
/// for special purposes.
const LOWEST_I2C_ADDR: u8 = 0x08;
/// The highest I2C address a device can have. Addresses above this are reserved
/// for special purposes.
const HIGHEST_I2C_ADDR: u8 = 0x77;

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
// Initialize the device peripherals
let peripherals = embassy_stm32::init(Default::default());
info!("Device started");

// Define the I2C pins. In this lab we will use the I2C1 peripheral, which
// is connected to PB6 (SCL) and PB7 (SDA).
let scl = peripherals.PB6;
let sda = peripherals.PB7;

// I2C definition
let mut i2c = I2c::new(
peripherals.I2C1,
scl,
sda,
Irqs,
peripherals.GPDMA1_CH0,
peripherals.GPDMA1_CH1,
Default::default(),
);

// Scan the I2C bus for devices. We do this by trying to read from each
// possible address.

for addr in LOWEST_I2C_ADDR..=HIGHEST_I2C_ADDR {
// To execute a "scan", we can either try to execute a read directly, or
// we can write a dummy register (0x00) and then wait for a response
//
// The first approach (read-only) may work with some devices, but it may
// not work devices which decide to ignore read requests without a
// preceding write. It will also take longer to get a timeout, which
// will make the scan take longer to complete.
//
// The second approach (write-read) is more likely to work with a wider
// range of devices, but it is not guaranteed that all devices will have
// a `0x00` register.
//
// For this lab, the BMP390 has its chip ID register at `0x00`, and the
// AT24C256 has its first memory address at `0x00`.
let mut rx_buf = [0u8; 1];
let res = i2c.write_read(addr, &[0x00], &mut rx_buf).await;
info!(
"Result for address 0x{:02x}: {:?} | Data: {:?}",
addr, res, rx_buf
);
}

loop {
Timer::after_secs(1).await;
}
}
161 changes: 161 additions & 0 deletions lab06/src/bin/ex2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#![no_std]
#![no_main]

use defmt::info;
use defmt_rtt as _;
use embassy_executor::Spawner;
use embassy_stm32::{
bind_interrupts,
i2c::{self, I2c},
peripherals,
};
use embassy_time::Timer;
use panic_probe as _;

// For I2C to work, we need to bind the interrupts to the correct handlers.
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::EventInterruptHandler<peripherals::I2C1>;
I2C1_ER => i2c::ErrorInterruptHandler<peripherals::I2C1>;
});

/// I2C device address for the BMP390. This is the default address when A0 is
/// connected to low. If A0 is connected to high, the address will be 0x77.
///
/// This will likely work even if A0 is not connected at all, in theory this
/// should be a "floating" pin, but in practice for our PM Lab board it will
/// function as if it were connected to low.
const BMP390_ADDR: u8 = 0x76;

/// Register addresses and values for the `PWR_CTRL` register. (Turns on and off
/// measurements and sets the power mode.)
///
/// | Bits | Description |
/// |------|-------------------------------------------------|
/// | 0 | Pressure measurement on/off. 1 = on, 0 = off |
/// | 1 | Temperature measurement on/off. 1 = on, 0 = off |
/// | 2-3 | Reserved, must be set to 0 |
/// | 4-5 | Power mode. 00 = sleep, 01/10 = forced (one
/// measurement), 11 = normal |
/// | 6-7 | Reserved, must be set to 0 |
const REGISTER_PWR_CTRL: u8 = 0x1B;
/// Bits to set in the `PWR_CTRL` register to set normal power mode.
const PWR_MODE_ON: u8 = 0b0011_0000;
/// Bits to set in the `PWR_CTRL` register to enable temperature measurement.
const PWR_TEMP_EN: u8 = 0b0000_0010;
/// Value to write to the `PWR_CTRL` register to enable temperature measurement
/// and set normal power mode.
const PWR_VAL: u8 = PWR_MODE_ON | PWR_TEMP_EN;

/// Register addresses and values for the `OSR` register. (Controls how many
/// samples are taken and averaged for each measurement)
///
/// | Bits | Description |
/// |------|-------------------------------------------------|
/// | 0-2 | Pressure oversampling.
/// 000 = no oversampling (1 sample),
/// 001 = x2 (2 samples),
/// 010 = x4 (4 samples),
/// 011 = x8 (8 samples),
/// 100 = x16 (16 samples),
/// 101 = x32 (32 samples), |
/// | 3-5 | Temperature oversampling.
/// 000 = no oversampling (1 sample),
/// 001 = x2 (2 samples),
/// 010 = x4 (4 samples),
/// 011 = x8 (8 samples),
/// 100 = x16 (16 samples),
/// 101 = x32 (32 samples), |
/// | 6-7 | Reserved, must be set to 0 |
const REGISTER_OSR: u8 = 0x1C;
/// Bits to set in the `OSR` register to set temperature oversampling to x2.
const OSR_TEMP_X2: u8 = 0b0000_1000;
/// Value to write to the `OSR` register to set temperature oversampling to x2
/// and pressure oversampling to none.
const OSR_VAL: u8 = OSR_TEMP_X2;

/// Register addresses for the raw temperature data (Least significant bits).
const REGISTER_TEMP_XLSB: u8 = 0x07;
/// Register addresses for the raw temperature data (Middle significant bits).
const REGISTER_TEMP_LSB: u8 = 0x08;
/// Register addresses for the raw temperature data (Most significant bits).
const REGISTER_TEMP_MSB: u8 = 0x09;

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
// Initialize the device peripherals
let peripherals = embassy_stm32::init(Default::default());
info!("Device started");

// Define the I2C pins. In this lab we will use the I2C1 peripheral, which
// is connected to PB6 (SCL) and PB7 (SDA).
let scl = peripherals.PB6;
let sda = peripherals.PB7;

// I2C definition
let mut i2c = I2c::new(
peripherals.I2C1,
scl,
sda,
Irqs,
peripherals.GPDMA1_CH0,
peripherals.GPDMA1_CH1,
Default::default(),
);

// Before we can read any data from the sensor, we need to configure the power.
i2c.write(BMP390_ADDR, &[REGISTER_PWR_CTRL, PWR_VAL])
.await
.unwrap();

// And we also need to configure the oversampling settings.
i2c.write(BMP390_ADDR, &[REGISTER_OSR, OSR_VAL])
.await
.unwrap();

// Read the raw temperature data. There are 2 ways we can do this.
loop {
// The first way is to read all 3 bytes in one go. Since the registers
// are consecutige, the BMP automatically increments the register
// address after each byte, so we can just read from the first register,
// and so long as we have more bytes to read, the main (us, the MCU)
// will send ACK to continue reading. After we read the last byte (as
// determined by the size of the array), we will send a NACK indicating
// to the BMP that we are done reading. This will ensure that the
// temperature data is consistent.
let mut raw_temp_data = [0u8; 3];
i2c.write_read(BMP390_ADDR, &[REGISTER_TEMP_XLSB], &mut raw_temp_data)
.await
.unwrap();

// Alternatively, we can read each byte one by one. This is more likely
// to cause inconsistent data, since the BMP may update the temperature
// data in between our reads, but it is still possible to get the
// correct data if we are lucky.
let mut raw_xlsb = [0u8; 1];
let mut raw_lsb = [0u8; 1];
let mut raw_msb = [0u8; 1];
i2c.write_read(BMP390_ADDR, &[REGISTER_TEMP_XLSB], &mut raw_xlsb)
.await
.unwrap();
i2c.write_read(BMP390_ADDR, &[REGISTER_TEMP_LSB], &mut raw_lsb)
.await
.unwrap();
i2c.write_read(BMP390_ADDR, &[REGISTER_TEMP_MSB], &mut raw_msb)
.await
.unwrap();

info!("Raw data: {:?}", raw_temp_data);
info!(
"Raw byte by byte: {:?}",
[raw_xlsb[0], raw_lsb[0], raw_msb[0]]
);

// The raw temperature data is a 24-bit signed integer
let raw_temp: i32 = ((raw_temp_data[2] as i32) << 16)
| ((raw_temp_data[1] as i32) << 8)
| (raw_temp_data[0] as i32);
info!("Raw temperature value: {}", raw_temp);

Timer::after_millis(400).await;
}
}
Loading
Loading