In our project we'll need to know the magnetic heading at any given position to help in precise alignment of the kit. For this we'll use a HMC5883L magnetometer module which communicates with other devices via I2c.
Table of Contents
- Requirements
- Connections
- I2C Configuration
HMC5883L Configuration
Identification- Mode
- Out Rate and Gain
- Final Code
Requirements
- 1 x Raspberry Pico board
- 1 x USB Cable type 2.0
- 1 x HC-05 Bluetooth module
- 1 x HMC5833L Compass Module
- 15 x M-M jumper wires
- 2 x Mini Breadboards
Connections
i2c0.ic_enable().modify(|_, w| w.enable().clear_bit());// disable i2c
// select controller mode & speed
i2c0.ic_con().modify(|_, w| {
w.speed().fast();
w.master_mode().enabled();
w.ic_slave_disable().slave_disabled();
w.ic_restart_en().enabled();
w.tx_empty_ctrl().enabled()
});
// Clear FIFO threshold
i2c0.ic_tx_tl().write(|w| unsafe { w.tx_tl().bits(0) });
i2c0.ic_rx_tl().write(|w| unsafe { w.rx_tl().bits(0) });
We have to set up the COUNT registers before using the I2C. From the RP2040's manual we learn that the minimum SCL high and low values are 600ns and 1300ns respectively.
These are used to find the high and low count periods which are used to program the respective counters.
// IC_xCNT = (ROUNDUP(MIN_SCL_HIGH_LOWtime*OSCFREQ,0))
// IC_HCNT = (600ns * 125MHz) + 1
// IC_LCNT = (1300ns * 125MHz) + 1
i2c0.ic_fs_scl_hcnt().write(|w| unsafe { w.ic_fs_scl_hcnt().bits(76) });
i2c0.ic_fs_scl_lcnt().write(|w| unsafe { w.ic_fs_scl_lcnt().bits(163) });
We finally have to perform spike suppression and program the data hold time during transmission.
// spkln = lcnt/16;
i2c0.ic_fs_spklen().write(|w| unsafe { w.ic_fs_spklen().bits(163/16) });
// sda_tx_hold_count = freq_in [cycles/s] * 300ns for scl < 1MHz
let sda_tx_hold_count = ((125_000_000 * 3) / 10000000) + 1;
i2c0.ic_sda_hold().modify(|_r, w| unsafe { w.ic_sda_tx_hold().bits(sda_tx_hold_count as u16) });
I2C0 is now set up. We equally have to configure the HMC5883L.
HMC5883L configuration
First we'll write helper functions for reading and writing the I2C lines.
Configuring the magnetometer requires writing to specific registers. The below functions are light tweaks from the rp2040-hal crate. Overall we follow three steps:
- Disable the I2C
- Write the slave address
- Enable the I2C
fn i2c_read(
i2c0: &mut I2C0,
addr: u16,
bytes: &mut [u8],
) -> Result<(), u32> {
i2c0.ic_enable().modify(|_, w| w.enable().clear_bit());// disable i2c
i2c0.ic_tar().modify(|_, w| unsafe { w.ic_tar().bits(addr) });// slave address
i2c0.ic_enable().modify(|_, w| w.enable().set_bit());// enable i2c
let last_index = bytes.len() - 1;
for (i, byte) in bytes.iter_mut().enumerate() {
let first = i == 0;
let last = i == last_index;
// wait until there is space in the FIFO to write the next byte
while TX_FIFO_SIZE - i2c0.ic_txflr().read().txflr().bits() == 0 {}
i2c0.ic_data_cmd().modify(|_, w| {
if first {
w.restart().enable();
} else {
w.restart().disable();
}
if last {
w.stop().enable();
} else {
w.stop().disable();
}
w.cmd().read()
});
//Wait until address tx'ed
while i2c0.ic_raw_intr_stat().read().tx_empty().is_inactive() {}
//Clear ABORT interrupt
//self.i2c0.ic_clr_tx_abrt.read();
while i2c0.ic_rxflr().read().bits() == 0 {
//Wait while receive FIFO empty
//If attempt aborts : not valid address; return error with
//abort reason.
let abort_reason = i2c0.ic_tx_abrt_source().read().bits();
//Clear ABORT interrupt
i2c0.ic_clr_tx_abrt().read();
if abort_reason != 0 {
return Err(abort_reason)
}
}
*byte = i2c0.ic_data_cmd().read().dat().bits();
}
Ok(())
}
fn i2c_write(
i2c0: &mut I2C0,
addr: u16,
bytes: &[u8],
) -> Result<(), u32> {
i2c0.ic_enable().modify(|_, w| w.enable().clear_bit());// disable i2c
i2c0.ic_tar().modify(|_, w| unsafe { w.ic_tar().bits(addr) });// slave address
i2c0.ic_enable().modify(|_, w| w.enable().set_bit());// enable i2c
let last_index = bytes.len() - 1;
for (i, byte) in bytes.iter().enumerate() {
let last = i == last_index;
i2c0.ic_data_cmd().modify(|_, w| {
if last {
w.stop().enable();
} else {
w.stop().disable();
}
unsafe { w.dat().bits(*byte)}
});
// Wait until address and data tx'ed
while i2c0.ic_raw_intr_stat().read().tx_empty().is_inactive() {}
// Clear ABORT interrupt
// self.i2c0.ic_clr_tx_abrt.read();
// If attempt aborts : not valid address; return error with
// abort reason.
let abort_reason = i2c0.ic_tx_abrt_source().read().bits();
// Clear ABORT interrupt
i2c0.ic_clr_tx_abrt().read();
if abort_reason != 0 {
// Wait until the STOP condition has occured
while i2c0.ic_raw_intr_stat().read().stop_det().is_inactive() {}
// Clear STOP interrupt
i2c0.ic_clr_stop_det().read().clr_stop_det();
return Err(abort_reason)
}
if last {
// Wait until the STOP condition has occured
while i2c0.ic_raw_intr_stat().read().stop_det().is_inactive() {}
// Clear STOP interrupt
i2c0.ic_clr_stop_det().read().clr_stop_det();
}
}
Ok(())
}
Import I2C0.
use rp2040_pac::I2C0;
Add the TX_FIFO_SIZE constant.
// Global constants
const TX_FIFO_SIZE: u8 = 16;// I2C FIFO size
Create buffer to hold data to be sent and received via I2C.
// Buffers
let mut writebuf: [u8; 1];// buffer to hold 1 byte
let mut readbuf: [u8; 1] = [0; 1];
Identification
We can begin by confirming the identification of the HMC5883L. The chip has three Identification registers, A, B and C, holding the values 48, 34 and 33 respectively.
To read a register in the compass we first have to send the slave address with the command bit set to read followed by a pointer to the address whose contents we want to read.
Reading ID Reg. A for example we have to send 0x3D 0x10.
Before proceeding further we should create the magnetometer's addresses constants to help write cleanly.
Add the following to our global constant section
// Slave Address
const HMC5883L_ADDR: u16 = 30;
// ID
const IDENTIFICATION_REG_A: u8 = 0xA;// HMC5883L
const IDENTIFICATION_REG_B: u8 = 0xB;// HMC5883L
const IDENTIFICATION_REG_C: u8 = 0xC;// HMC5883L
Continuing the configuration...
// Configure and confirm HMC5833L
// ID the compass
// Read ID REG A
writebuf = [IDENTIFICATION_REG_A];
i2c_write(&mut i2c0, HMC5883L_ADDR, &mut writebuf).unwrap();
i2c_read(&mut i2c0, HMC5883L_ADDR, &mut readbuf).unwrap();
let id_a = readbuf[0];
writeln!(serialbuf, "Id reg a: 0x{:02X}", id_a).unwrap();
transmit_uart_data(&uart_data, serialbuf);
// Read ID REG B
writebuf = [IDENTIFICATION_REG_B];
i2c_write(&mut i2c0, HMC5883L_ADDR, &mut writebuf).unwrap();
i2c_read(&mut i2c0, HMC5883L_ADDR, &mut readbuf).unwrap();
let id_b = readbuf[0];
writeln!(serialbuf, "Id reg b: 0x{:02X}", id_b).unwrap();
transmit_uart_data(&uart_data, serialbuf);
// Read ID REG C
writebuf = [IDENTIFICATION_REG_C];
i2c_write(&mut i2c0, HMC5883L_ADDR, &mut writebuf).unwrap();
i2c_read(&mut i2c0, HMC5883L_ADDR, &mut readbuf).unwrap();
let id_c = readbuf[0];
writeln!(serialbuf, "Id reg c: 0x{:02X}", id_c).unwrap();
transmit_uart_data(&uart_data, serialbuf);
if id_a == 0x48 && id_b == 0x34 && id_c == 0x33 {
writeln!(serialbuf, "Magnetometer ID confirmed!").unwrap();
transmit_uart_data(&uart_data, serialbuf);
}
Flash the program thus far into the Pico. The compass should be positively identified.
Final Code
The updated and rearranged code will be as so.
In the next part we'll run a simple example to demonstrate the working of the HMC5833L.
SOCIAL SHARE CARD GENERATOR