Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ICM20948 accelerometer #6756

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
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
11 changes: 7 additions & 4 deletions docs/Measuring_Resonances.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ board designs and different clones of them. If it is going to be connected to a
For ADXL345s, make sure that the board supports SPI mode (a small number of
boards appear to be hard-configured for I2C by pulling SDO to GND).

For MPU-9250/MPU-9255/MPU-6515/MPU-6050/MPU-6500s and LIS2DW/LIS3DH there are also
a variety of board designs and clones with different I2C pull-up resistors which
will need supplementing.
For MPU-9250/MPU-9255/MPU-6515/MPU-6050/MPU-6500/ICM20948s and LIS2DW/LIS3DH there
are also a variety of board designs and clones with different I2C pull-up resistors
which will need supplementing.

## MCUs with Klipper I2C *fast-mode* Support

Expand Down Expand Up @@ -136,7 +136,7 @@ GND+SCL

Note that unlike a cable shield, any GND(s) should be connected at both ends.

#### MPU-9250/MPU-9255/MPU-6515/MPU-6050/MPU-6500
#### MPU-9250/MPU-9255/MPU-6515/MPU-6050/MPU-6500/ICM20948

These accelerometers have been tested to work over I2C on the RPi, RP2040 (Pico)
and AVR at 400kbit/s (*fast mode*). Some MPU accelerometer modules include
Expand Down Expand Up @@ -355,6 +355,7 @@ accel_chip: mpu9250
probe_points:
100, 100, 20 # an example
```
If you are using the ICM20948, replace instances of "mpu9250" with "icm20948".

#### Configure MPU-9520 Compatibles With Pico

Expand All @@ -377,6 +378,7 @@ probe_points:
[static_digital_output pico_3V3pwm] # Improve power stability
pins: pico:gpio23
```
If you are using the ICM20948, replace instances of "mpu9250" with "icm20948".

#### Configure MPU-9520 Compatibles with AVR

Expand All @@ -395,6 +397,7 @@ accel_chip: mpu9250
probe_points:
100, 100, 20 # an example
```
If you are using the ICM20948, replace instances of "mpu9250" with "icm20948".

Restart Klipper via the `RESTART` command.

Expand Down
175 changes: 175 additions & 0 deletions klippy/extras/icm20948.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Support for reading acceleration data from an icm20948 chip
#
# Copyright (C) 2024 Paul Hansel <[email protected]>
# Copyright (C) 2022 Harry Beyel <[email protected]>
# Copyright (C) 2020-2021 Kevin O'Connor <[email protected]>
#
# This file may be distributed under the terms of the GNU GPLv3 license.

# From https://invensense.tdk.com/wp-content/uploads/
# 2016/06/DS-000189-ICM-20948-v1.3.pdf

import logging
from . import bus, adxl345, bulk_sensor

ICM20948_ADDR = 0x68

ICM_DEV_IDS = {
0xEA: "icm-20948",
#everything above are normal ICM IDs
}


# ICM20948 registers
REG_DEVID = 0x00 # 0xEA
REG_FIFO_EN = 0x67 # FIFO_EN_2
REG_ACCEL_SMPLRT_DIV1 = 0x10 # MSB
REG_ACCEL_SMPLRT_DIV2 = 0x11 # LSB
REG_ACCEL_CONFIG = 0x14
REG_USER_CTRL = 0x03
REG_PWR_MGMT_1 = 0x06
REG_PWR_MGMT_2 = 0x07
REG_INT_STATUS = 0x19

SAMPLE_RATE_DIVS = { 4500: 0x00 }

#SET_CONFIG = 0x01 # FIFO mode 'stream' style
SET_ACCEL_CONFIG = 0x04 # 8g full scale, 1209Hz BW, ??? delay 4.5kHz samp rate
SET_PWR_MGMT_1_WAKE = 0x01
SET_PWR_MGMT_1_SLEEP= 0x41
SET_PWR_MGMT_2_ACCEL_ON = 0x07
SET_PWR_MGMT_2_OFF = 0x3F
SET_USER_FIFO_RESET = 0x0E
SET_USER_FIFO_EN = 0x40
SET_ENABLE_FIFO = 0x10
SET_DISABLE_FIFO = 0x00

FREEFALL_ACCEL = 9.80665 * 1000.
# SCALE = 1/4096 g/LSB @8g scale * Earth gravity in mm/s**2
SCALE = 0.000244140625 * FREEFALL_ACCEL

FIFO_SIZE = 512

BATCH_UPDATES = 0.100

# Printer class that controls ICM20948 chip
class ICM20948:
def __init__(self, config):
self.printer = config.get_printer()
adxl345.AccelCommandHelper(config, self)
self.axes_map = adxl345.read_axes_map(config, SCALE, SCALE, SCALE)
self.data_rate = config.getint('rate', 4500)
if self.data_rate not in SAMPLE_RATE_DIVS:
raise config.error("Invalid rate parameter: %d" % (self.data_rate,))
# Setup mcu sensor_icm20948 bulk query code
self.i2c = bus.MCU_I2C_from_config(config,
default_addr=ICM20948_ADDR,
default_speed=400000)
self.mcu = mcu = self.i2c.get_mcu()
self.oid = oid = mcu.create_oid()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oid is not used anywhere

self.query_icm20948_cmd = None
mcu.register_config_callback(self._build_config)
# Bulk sample message reading
chip_smooth = self.data_rate * BATCH_UPDATES * 2
self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, ">hhh")
self.last_error_count = 0
# Process messages in batches
self.batch_bulk = bulk_sensor.BatchBulkHelper(
self.printer, self._process_batch,
self._start_measurements, self._finish_measurements, BATCH_UPDATES)
self.name = config.get_name().split()[-1]
hdr = ('time', 'x_acceleration', 'y_acceleration', 'z_acceleration')
self.batch_bulk.add_mux_endpoint("icm20948/dump_icm20948", "sensor",
self.name, {'header': hdr})
def _build_config(self):
cmdqueue = self.i2c.get_command_queue()
self.mcu.add_config_cmd("config_icm20948 oid=%d i2c_oid=%d"
% (self.oid, self.i2c.get_oid()))
self.mcu.add_config_cmd("query_icm20948 oid=%d rest_ticks=0"
% (self.oid,), on_restart=True)
self.query_icm20948_cmd = self.mcu.lookup_command(
"query_icm20948 oid=%c rest_ticks=%u", cq=cmdqueue)
self.ffreader.setup_query_command("query_icm20948_status oid=%c",
oid=self.oid, cq=cmdqueue)
def read_reg(self, reg):
params = self.i2c.i2c_read([reg], 1)
return bytearray(params['response'])[0]
def set_reg(self, reg, val, minclock=0):
self.i2c.i2c_write([reg, val & 0xFF], minclock=minclock)
def start_internal_client(self):
aqh = adxl345.AccelQueryHelper(self.printer)
self.batch_bulk.add_client(aqh.handle_batch)
return aqh
# Measurement decoding
def _convert_samples(self, samples):
(x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map
count = 0
for ptime, rx, ry, rz in samples:
raw_xyz = (rx, ry, rz)
x = round(raw_xyz[x_pos] * x_scale, 6)
y = round(raw_xyz[y_pos] * y_scale, 6)
z = round(raw_xyz[z_pos] * z_scale, 6)
samples[count] = (round(ptime, 6), x, y, z)
count += 1
# Start, stop, and process message batches
def _start_measurements(self):
# In case of miswiring, testing ICM20948 device ID prevents treating
# noise or wrong signal as a correctly initialized device
dev_id = self.read_reg(REG_DEVID)
if dev_id not in ICM_DEV_IDS.keys():
raise self.printer.command_error(
"Invalid mpu id (got %x).\n"
"This is generally indicative of connection problems\n"
"(e.g. faulty wiring) or a faulty chip."
% (dev_id))
else:
logging.info("Found %s with id %x"% (ICM_DEV_IDS[dev_id], dev_id))

# Setup chip in requested query rate
self.set_reg(REG_PWR_MGMT_1, SET_PWR_MGMT_1_WAKE)
self.set_reg(REG_PWR_MGMT_2, SET_PWR_MGMT_2_ACCEL_ON)
# Add 20ms pause for accelerometer chip wake up
self.read_reg(REG_DEVID) # Dummy read to ensure queues flushed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are i2c_write_wait_ack which can be used above
Which ensures that that request is acknowledged by the device

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll swap that in. Thanks for the suggestion @nefelim4ag

systime = self.printer.get_reactor().monotonic()
next_time = self.mcu.estimated_print_time(systime) + 0.020
self.set_reg(REG_ACCEL_SMPLRT_DIV1, SAMPLE_RATE_DIVS[self.data_rate])
self.set_reg(REG_ACCEL_SMPLRT_DIV2, SAMPLE_RATE_DIVS[self.data_rate],
Copy link
Contributor

@nefelim4ag nefelim4ag Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those will reorder writes, cause your next set_reg will execute with minclock=0


BTW, you should be able to just write 2 bytes at address of DIV1
Same for power management

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ultimately duplicated from klippy/extras/mpu9250.py. I can change icm20948.py to import functions from there where they're unchanged.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I already found that, but this is your code now :D
I just suggest you not mindlessly copy things
minclock=0/reqclock=0 is the default behavior anyway

With wait_ack() you can just add a pause there, before write, or order other writes by minclock
If you merge coupled registers write, that will also make things cleaner, like:

SAMPLE_RATE_DIVS = { 4500: [0x00, 0x0] }
i2c_write_wait_ack([REG_ACCEL_SMPLRT_DIV1] + SAMPLE_RATE_DIVS[self.data_rate])
# or
set_reg(REG_ACCEL_SMPLRT_DIV1, SAMPLE_RATE_DIVS[self.data_rate])

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the datasheet, there is a 20ms delay between the chip wake and accelerometer ready (how ready, what does it mean?) but looks like all registers can be just written

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those will reorder writes, cause your next set_reg will execute with minclock=0

Commands are never reordered within a cmdqueue (and all the i2c commands will be on the same cmdqueue). The minclock and reqclock control the timing of when messages are released from the command queue, but they never reorder that queue.

There are a few ways to enforce a delay between i2c writes. One way is to do what mpu9250 does:

        # Add 20ms pause for accelerometer chip wake up
        self.read_reg(REG_DEVID) # Dummy read to ensure queues flushed
        systime = self.printer.get_reactor().monotonic()
        next_time = self.mcu.estimated_print_time(systime) + 0.020
        self.set_reg(REG_SMPLRT_DIV, SAMPLE_RATE_DIVS[self.data_rate],
                     minclock=self.mcu.print_time_to_clock(next_time))

This forces an i2c read so that we know previous commands have been completed at the micro-controller (that is, nothing in the command queue). Then we schedule the next i2c message to have a minimum clock that is 20ms in the future (from the current time as measured on the host). Note that the minclock is on the very next write.

Another mechanism would be something like:

        # Add 20ms pause for accelerometer chip wake up
        self.read_reg(REG_DEVID) # Dummy read to ensure queues flushed
        reactor = self.printer.get_reactor()
        reactor.pause(reactor.monotonic() + 0.020)
        self.set_reg(REG_SMPLRT_DIV, SAMPLE_RATE_DIVS[self.data_rate])
        ...

This differs from the above in that we pause the host instead of just scheduling the next message at a particular time. (Both will enforce at least 20ms of delay between i2c messages.)

As mentioned, it's possible to eliminate the dummy read by using i2c_write_ack() but that seems like more trouble than it is worth. This is an infrequently run "slow path" for sensor initialization.

Of course, if no delay is actually needed, that is preferable.

Hope that helps,
-Kevin

minclock=self.mcu.print_time_to_clock(next_time))
# self.set_reg(REG_CONFIG, SET_CONFIG) # No config register
self.set_reg(REG_ACCEL_CONFIG, SET_ACCEL_CONFIG)
# self.set_reg(REG_ACCEL_CONFIG2, SET_ACCEL_CONFIG2)
# Reset fifo
self.set_reg(REG_FIFO_EN, SET_DISABLE_FIFO)
self.set_reg(REG_USER_CTRL, SET_USER_FIFO_RESET)
self.set_reg(REG_USER_CTRL, SET_USER_FIFO_EN)
self.read_reg(REG_INT_STATUS) # clear FIFO overflow flag

# Start bulk reading
rest_ticks = self.mcu.seconds_to_clock(4. / self.data_rate)
self.query_icm20948_cmd.send([self.oid, rest_ticks])
self.set_reg(REG_FIFO_EN, SET_ENABLE_FIFO)
logging.info("ICM20948 starting '%s' measurements", self.name)
# Initialize clock tracking
self.ffreader.note_start()
self.last_error_count = 0
def _finish_measurements(self):
# Halt bulk reading
self.set_reg(REG_FIFO_EN, SET_DISABLE_FIFO)
self.query_icm20948_cmd.send_wait_ack([self.oid, 0])
self.ffreader.note_end()
logging.info("ICM20948 finished '%s' measurements", self.name)
self.set_reg(REG_PWR_MGMT_1, SET_PWR_MGMT_1_SLEEP)
self.set_reg(REG_PWR_MGMT_2, SET_PWR_MGMT_2_OFF)
def _process_batch(self, eventtime):
samples = self.ffreader.pull_samples()
self._convert_samples(samples)
if not samples:
return {}
return {'data': samples, 'errors': self.last_error_count,
'overflows': self.ffreader.get_last_overflows()}

def load_config(config):
return ICM20948(config)

def load_config_prefix(config):
return ICM20948(config)
9 changes: 8 additions & 1 deletion src/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ config WANT_MPU9250
bool
depends on HAVE_GPIO_I2C
default y
config WANT_ICM20948
bool
depends on HAVE_GPIO_I2C
default y
config WANT_HX71X
bool
depends on WANT_GPIO_BITBANGING
Expand All @@ -138,7 +142,7 @@ config WANT_SOFTWARE_SPI
default y
config NEED_SENSOR_BULK
bool
depends on WANT_ADXL345 || WANT_LIS2DW || WANT_MPU9250 \
depends on WANT_ADXL345 || WANT_LIS2DW || WANT_MPU9250 || WANT_ICM20948 \
|| WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 || WANT_SENSOR_ANGLE
default y
menu "Optional features (to reduce code size)"
Expand All @@ -161,6 +165,9 @@ config WANT_LIS2DW
config WANT_MPU9250
bool "Support MPU accelerometers"
depends on HAVE_GPIO_I2C
config WANT_ICM20948
bool "Support ICM20948 accelerometer"
depends on HAVE_GPIO_I2C
config WANT_HX71X
bool "Support HX711 and HX717 ADC chips"
depends on WANT_GPIO_BITBANGING
Expand Down
1 change: 1 addition & 0 deletions src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ src-$(CONFIG_WANT_THERMOCOUPLE) += thermocouple.c
src-$(CONFIG_WANT_ADXL345) += sensor_adxl345.c
src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c
src-$(CONFIG_WANT_MPU9250) += sensor_mpu9250.c
src-$(CONFIG_WANT_ICM20948) += sensor_icm20948.c
src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c
src-$(CONFIG_WANT_ADS1220) += sensor_ads1220.c
src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c
Expand Down
Loading