ICM 20948: how to read magnetometer data in non-bypass mode?
Using ICM 20948 on raspberrypi, I have a bash script (similar to the one below) that does read correct / expected magnetometer values: this means the magnetometer works. That one thing!
Now my understanding is that the bypass mode allows to read only mag data (not accel / gyro).
So I try to read mag data in non bypass mode along with accel / gyro data using this script on raspberrypi:
$ cat test_mag.sh
#!/bin/bash -e
# The magnetometer is **not** directly visible on the I2C bus of the raspberrypi:
# - The raspberrypi sees the ICM-20948 chip on its I2C bus but **not** the magnetometer.
# - The ICM-20948 chip sees the magnetometer on its **internal** I2C bus.
#
# The ICM-20948 chip sees the AK09916 magnetometer on its **internal** I2C bus at the address 0x0C.
# => ICM-20948 chip must be configured to proxy requests coming from raspberrypi.
# => From raspberrypi, to talk to 0x0C inside ICM-20948:
# - You must first configure registers inside 0x69.
# - These registers cause the ICM-20948 to:
# - Forward reads from the internal 0x0C device using slave 0.
# - Forward writes to the internal 0x0C device using slave 4.
# - Return the bytes to raspberrypi via special EXT_SENS_DATA registers.
#
# Note: Small delays are required for the I2C master to propagate writes and reads
BUS=1
ADDR_ICM=0x69 # ICM-20948 I2C address
ADDR_MAG=0x0C # AK09916 internal address
EXT_BASE=0x49 # First EXT_SENS_DATA register
# Helpers
i2cr() { i2cget -y $BUS $ADDR_ICM $1; }
i2cw() { i2cset -y $BUS $ADDR_ICM $1 $2; }
# Combine high/low bytes to signed 16-bit
to_int16() {
local hi=$1 lo=$2
local val=$(( (hi<<8) | lo ))
if [ $val -ge 32768 ]; then val=$((val-65536)); fi
echo $val
}
# Convert raw magnetometer LSB to µT (0.15 µT/LSB)
mag_scale() {
printf "%.2f" "$(echo "$1 * 0.15" | bc -l)"
}
echo "=== ICM-20948 initialization ==="
# Reset device and wake up
i2cw 0x7F 0x00 # Bank 0
i2cw 0x06 0x80 # PWR_MGMT_1 reset
sleep 0.01
i2cw 0x7F 0x00 # Bank 0
i2cw 0x06 0x01 # Clock source = auto select (gyro)
i2cw 0x07 0x00 # PWR_MGMT_2: enable accel/gyro
# Enable BYPASS and disable I2C master
i2cw 0x7F 0x00 # bank 0
i2cw 0x03 0x00 # USER_CTRL = 0x00 (release AUX I2C bus!)
i2cw 0x0F 0x02 # INT_PIN_CFG = BYPASS_EN
sleep 0.01
echo "BYPASS enabled: 0x0C should be visible"
i2cdetect -y $BUS
# Reset AK09916 before selecting mode: mandatory on many AK09916
i2cset -y $BUS $ADDR_MAG 0x32 0x01 # CNTL3 = 0x01 = soft-reset
sleep 0.01 # AK09916 needs ~10ms to reset
# AK09916: check WHO_AM_I.
WHO=$(i2cget -y $BUS $ADDR_MAG 0x01)
echo "AK09916 WHO_AM_I = $WHO"
# Put AK09916 in continuous mode via BYPASS
#
# AK09916 must be set to continuous mode **before** enabling I2C master
i2cset -y $BUS $ADDR_MAG 0x31 0x00 # CNTL2 = power-down
sleep 0.01
i2cset -y $BUS $ADDR_MAG 0x31 0x08 # CNTL2 = continuous 100Hz
sleep 0.01
# Read CNTL2 (0x31) of AK09916 to check AK09916 has set to continuous mode
CNTL2=$(i2cget -y $BUS 0x0C 0x31)
printf "AK09916 CNTL2: 0x%02X (0x08 = continuous mode)\n" $CNTL2
# Disable BYPASS and enable I2C master
i2cw 0x7F 0x00 # Bank 0
i2cw 0x0F 0x00 # Disable BYPASS, return control to sensor hub
sleep 0.01
echo "BYPASS disabled: 0x0C should NOT be visible"
i2cdetect -y $BUS
i2cw 0x03 0x20 # USER_CTRL: I2C_MST_EN
sleep 0.01
# Enable master cycle in low-power config
i2cw 0x7F 0x00 # Bank 0
i2cw 0x05 0x40 # LP_CONFIG: I2C_MST_CYCLE = 1
sleep 0.01
# Configure I2C master (Bank 3)
i2cw 0x7F 0x30 # Bank 3
i2cw 0x00 0x01 # I2C_MST_ODR_CONFIG: ENABLE slave sampling (critical!)
i2cw 0x01 0x0D # I2C_MST_CTRL ~100 kHz
i2cw 0x02 0x01 # I2C_MST_DELAY_CTRL: enable SLV0 delay (safer with AK09916)
sleep 0.01
# Configure SLV0 to read AK09916 ST1..ST2 (9 bytes)
i2cw 0x7F 0x30 # Bank 3
i2cw 0x03 $((ADDR_MAG<<1)) # SLV0_ADDR: read (7 bits address)
i2cw 0x04 0x10 # SLV0_REG: ST1
i2cw 0x05 0x89 # SLV0_CTRL: enable, length=9
sleep 0.01
# # Configure SLV4 for writes to AK09916 (if needed)
# #
# # Used to write to the AK09916 from the I2C master:
# # e.g. setting CNTL2 to continuous mode or resetting via CNTL3.
# # Not strictly necessary if the AK09916 is configured in bypass mode (as done above).
# i2cw 0x7F 0x30 # Bank 3
# i2cw 0x1B $((ADDR_MAG<<1)) # SLV4_ADDR: 0x18
# i2cw 0x1C 0x00 # SLV4_REG: (unused for reads)
# i2cw 0x1D 0x80 # SLV4_CTRL: enable, no data
# sleep 0.01
# --- DEBUG: check I2C master status and EXT_SENS_DATA ---
echo "--- DEBUG: I2C Master status ---"
i2cw 0x7F 0x00
status=$(i2cr 0x17) # I2C_MST_STATUS
printf "I2C_MST_STATUS (0x17): 0x%02X\n" $status
printf "Bit breakdown:\n"
printf " Bit 7 (PASS_THROUGH): %d\n" $(( (status >> 7) & 1 ))
printf " Bit 6 (SLV4_DONE): %d\n" $(( (status >> 6) & 1 ))
printf " Bit 5 (SLV4_NACK): %d\n" $(( (status >> 5) & 1 ))
printf " Bit 4 (SLV3_DONE): %d\n" $(( (status >> 4) & 1 ))
printf " Bit 3 (SLV3_NACK): %d\n" $(( (status >> 3) & 1 ))
printf " Bit 2 (SLV2_DONE): %d\n" $(( (status >> 2) & 1 ))
printf " Bit 1 (SLV2_NACK): %d\n" $(( (status >> 1) & 1 ))
printf " Bit 0 (SLV0_DONE): %d\n" $(( (status >> 0) & 1 ))
echo "--- DEBUG: first 12 EXT_SENS_DATA bytes ---"
for reg in $(seq 0x49 0x54); do
printf "0x%02X: 0x%02X\n" $reg $(i2cr $reg)
done
# Read accelerometer
i2cw 0x7F 0x00 # Bank 0
ACC_XH=$(i2cr 0x2D); ACC_XL=$(i2cr 0x2E)
ACC_YH=$(i2cr 0x2F); ACC_YL=$(i2cr 0x30)
ACC_ZH=$(i2cr 0x31); ACC_ZL=$(i2cr 0x32)
ACCX=$(to_int16 $((ACC_XH)) $((ACC_XL)))
ACCY=$(to_int16 $((ACC_YH)) $((ACC_YL)))
ACCZ=$(to_int16 $((ACC_ZH)) $((ACC_ZL)))
# Read gyroscope
i2cw 0x7F 0x00 # Bank 0
GYR_XH=$(i2cr 0x33); GYR_XL=$(i2cr 0x34)
GYR_YH=$(i2cr 0x35); GYR_YL=$(i2cr 0x36)
GYR_ZH=$(i2cr 0x37); GYR_ZL=$(i2cr 0x38)
GYRX=$(to_int16 $((GYR_XH)) $((GYR_XL)))
GYRY=$(to_int16 $((GYR_YH)) $((GYR_YL)))
GYRZ=$(to_int16 $((GYR_ZH)) $((GYR_ZL)))
# Wait for magnetometer data ready (ST1[0] = 1).
sleep 0.1 # After enabling the I2C master, wait at least 100 ms before reading EXT_SENS_DATA.
i2cw 0x7F 0x00 # Bank 0
WHO=$(i2cr $EXT_BASE) # Read the WHO_AM_I value from EXT_SENS_DATA_00 (0x49)
printf "AK09916 WHO_AM_I via I2C master (SLV0): 0x%02X\n" $WHO
for i in {1..200}; do
ST1=$(i2cr $EXT_BASE)
if (( (ST1 & 0x01) != 0 )); then break; fi
sleep 0.01
done
# Read magnetometer from EXT_SENS_DATA_00..08
i2cw 0x7F 0x00 # Bank 0
ST1=$(i2cr $EXT_BASE)
HX_L=$(i2cr $((EXT_BASE+1))); HX_H=$(i2cr $((EXT_BASE+2)))
HY_L=$(i2cr $((EXT_BASE+3))); HY_H=$(i2cr $((EXT_BASE+4)))
HZ_L=$(i2cr $((EXT_BASE+5))); HZ_H=$(i2cr $((EXT_BASE+6)))
ST2=$(i2cr $((EXT_BASE+7)))
HX=$(to_int16 $((HX_H)) $((HX_L)))
HY=$(to_int16 $((HY_H)) $((HY_L)))
HZ=$(to_int16 $((HZ_H)) $((HZ_L)))
HX_uT=$(mag_scale $HX)
HY_uT=$(mag_scale $HY)
HZ_uT=$(mag_scale $HZ)
# Print results
echo "=== Accelerometer (raw) === X=$ACCX Y=$ACCY Z=$ACCZ"
echo "=== Gyroscope (raw) === X=$GYRX Y=$GYRY Z=$GYRZ"
echo "=== Magnetometer (raw) === X=$HX Y=$HY Z=$HZ"
echo "=== Magnetometer (µT) === X=$HX_uT Y=$HY_uT Z=$HZ_uT"
echo "ST1=$ST1 ST2=$ST2"
$ ./test_mag.sh
=== ICM-20948 initialization ===
BYPASS enabled: 0x0C should be visible
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- 0c -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- 69 -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
AK09916 WHO_AM_I = 0x09
AK09916 CNTL2: 0x08 (0x08 = continuous mode)
BYPASS disabled: 0x0C should NOT be visible
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- 69 -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
--- DEBUG: I2C Master status ---
I2C_MST_STATUS (0x17): 0x01
Bit breakdown:
Bit 7 (PASS_THROUGH): 0
Bit 6 (SLV4_DONE): 0
Bit 5 (SLV4_NACK): 0
Bit 4 (SLV3_DONE): 0
Bit 3 (SLV3_NACK): 0
Bit 2 (SLV2_DONE): 0
Bit 1 (SLV2_NACK): 0
Bit 0 (SLV0_DONE): 1
--- DEBUG: first 12 EXT_SENS_DATA bytes ---
0x49: 0x00
0x4A: 0x00
0x4B: 0x00
0x4C: 0x00
0x4D: 0x00
0x4E: 0x00
0x4F: 0x00
0x50: 0x00
0x51: 0x00
0x52: 0x00
0x53: 0x00
0x54: 0x00
AK09916 WHO_AM_I via I2C master (SLV0): 0x00
=== Accelerometer (raw) === X=432 Y=-196 Z=16240
=== Gyroscope (raw) === X=-76 Y=102 Z=9
=== Magnetometer (raw) === X=0 Y=0 Z=0
=== Magnetometer (µT) === X=0.00 Y=0.00 Z=0.00
ST1=0x00 ST2=0x00
I can read accel / gyro data but not mag data.
How to fix this? Something missing or misconfigured?