ICM 20948: how to read magnetometer data in non-bypass mode?

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?