diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index b01360adf..a17138cff 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1923,6 +1923,39 @@ Support for LIS3DH accelerometers. # See the "adxl345" section for information on this parameter. ``` +### [bmi160] + +BMI160 accelerometer. This sensor can be queried via I2C or SPI bus. +``` +[bmi160] +#i2c_address: +# Default is 105 (0x69). If SA0 is tied to GND, use 104 (0x68). +# Only used for I2C. +#i2c_mcu: +#i2c_bus: +#i2c_speed: +# See the "common I2C settings" section for a description of the +# above parameters. Only used for I2C. +#cs_pin: +#spi_speed: +#spi_bus: +#spi_software_sclk_pin: +#spi_software_mosi_pin: +#spi_software_miso_pin: +# See the "common SPI settings" section for a description of the +# above parameters. Only used for SPI. +#axes_map: x, y, z +# See the "adxl345" section for information on this parameter. +``` + +**Important:** Many BMI160 modules use ambiguous pin labels. For SPI: +- Use **SCL** for clock (not SCX) +- Use **SDA** for MOSI (not SDX) +- Use **SA0** for MISO +- Use **CS** for chip select + +The pins labeled SCX/SDX are for the auxiliary magnetometer bus. + ### [mpu9250] Support for MPU-9250, MPU-9255, MPU-6515, MPU-6050, and MPU-6500 diff --git a/klippy/extras/bmi160.py b/klippy/extras/bmi160.py new file mode 100644 index 000000000..ae95c2aa1 --- /dev/null +++ b/klippy/extras/bmi160.py @@ -0,0 +1,190 @@ +# bmi160.py +# +# Support for reading acceleration data from a BMI160 chip +# +# Copyright (C) 2025 Francisco Stephens +# Based on adxl345.py +# Copyright (C) 2020-2023 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import bus, adxl345, bulk_sensor + +# BMI160 registers +REG_CHIPID = 0x00 +REG_ACC_DATA_START = 0x12 +REG_ACC_CONF = 0x40 +REG_ACC_RANGE = 0x41 +REG_FIFO_DOWNS = 0x45 +REG_FIFO_CONFIG_0 = 0x46 +REG_FIFO_CONFIG_1 = 0x47 +REG_FIFO_DATA = 0x24 +REG_FIFO_LENGTH_0 = 0x22 +REG_CMD = 0x7E + +REG_MOD_READ = 0x80 + +# BMI160 commands for CMD register +CMD_ACC_PM_SUSPEND = 0x10 +CMD_ACC_PM_NORMAL = 0x11 +CMD_FIFO_FLUSH = 0xB0 + +# BMI160 constants +BMI160_DEV_ID = 0xD1 +# Target 1600Hz ODR, normal bandwidth, no undersampling +SET_ACC_CONF_1600HZ = 0x2C +# Set accelerometer range to +/-8g +SET_ACC_RANGE_8G = 0x08 +# Enable accelerometer FIFO, headerless mode +SET_FIFO_CONFIG_1 = 0x40 +# No FIFO downsampling +SET_FIFO_DOWNS = 0x00 + +# Scale factor for +/-8g range (Datasheet: 4096 LSB/g) +FREEFALL_ACCEL = 9.80665 * 1000. +SCALE = FREEFALL_ACCEL / 4096. + +BATCH_UPDATES = 0.100 + +BMI_I2C_ADDR = 0x69 + +# Printer class that controls BMI160 chip +class BMI160: + def __init__(self, config): + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + adxl345.AccelCommandHelper(config, self) + self.axes_map = adxl345.read_axes_map(config, SCALE, SCALE, SCALE) + self.data_rate = 1600 + # Setup mcu sensor_bmi160 bulk query code + # Check for SPI or I2C + if config.get('cs_pin', None) is not None: + # Using 1MHz to match working Arduino test + self.bus = bus.MCU_SPI_from_config(config, 0, default_speed=1000000) + self.bus_type = 'spi' + else: + self.bus = bus.MCU_I2C_from_config(config, + default_addr=BMI_I2C_ADDR, default_speed=400000) + self.bus_type = 'i2c' + self.mcu = mcu = self.bus.get_mcu() + self.oid = oid = mcu.create_oid() + self.query_bmi160_cmd = None + mcu.add_config_cmd("config_bmi160 oid=%d bus_oid=%d bus_oid_type=%s" + % (oid, self.bus.get_oid(), self.bus_type)) + mcu.add_config_cmd("query_bmi160 oid=%d rest_ticks=0" + % (oid,), on_restart=True) + 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, "= 0x40 and reg != REG_CMD: + stored_val = self.read_reg(reg) + if stored_val != val: + raise self.printer.command_error( + "Failed to set BMI160 register [0x%x] to 0x%x: " + "got 0x%x. This is generally indicative of connection " + "problems (e.g. faulty wiring) or a faulty bmi160 " + "chip." % (reg, val, stored_val)) + def start_internal_client(self): + aqh = adxl345.AccelQueryHelper(self.printer) + self.batch_bulk.add_client(aqh.handle_batch) + return aqh + 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 + del samples[count:] + def _start_measurements(self): + # 1. Force SPI Mode (The Magic Dummy Read) + if self.bus_type == 'spi': + self.read_reg(0x7F) + self.reactor.pause(0.010) # 10ms for mode switch + + # 2. Verify ID + dev_id = self.read_reg(REG_CHIPID) + if dev_id != BMI160_DEV_ID: + raise self.printer.command_error( + "Invalid bmi160 id (got %x vs %x).\n" + "This is generally indicative of connection problems\n" + "(e.g. faulty wiring) or a faulty bmi160 chip." + % (dev_id, BMI160_DEV_ID)) + + # 3. Wake Up FIRST (Match Arduino Sequence) + # Send Normal Mode command + self.set_reg(REG_CMD, CMD_ACC_PM_NORMAL) + # CRITICAL: Wait 50ms for startup/PLL locking + self.reactor.pause(0.050) + + # 4. Configure Registers (While Awake) + self.set_reg(REG_ACC_CONF, SET_ACC_CONF_1600HZ) + self.set_reg(REG_ACC_RANGE, SET_ACC_RANGE_8G) + self.set_reg(REG_FIFO_DOWNS, SET_FIFO_DOWNS) + self.set_reg(REG_FIFO_CONFIG_1, SET_FIFO_CONFIG_1) + + # 5. Flush FIFO + self.set_reg(REG_CMD, CMD_FIFO_FLUSH) + self.reactor.pause(0.010) + + # 6. Start Bulk Reading + # Start timer roughly immediately + rest_ticks = self.mcu.seconds_to_clock(4. / self.data_rate) + self.query_bmi160_cmd.send([self.oid, rest_ticks]) + logging.info("BMI160 starting '%s' measurements", self.name) + self.ffreader.note_start() + self.last_error_count = 0 + + def _finish_measurements(self): + self.set_reg(REG_CMD, CMD_ACC_PM_SUSPEND) + self.query_bmi160_cmd.send_wait_ack([self.oid, 0]) + self.ffreader.note_end() + logging.info("BMI160 finished '%s' measurements", self.name) + 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 BMI160(config) + +def load_config_prefix(config): + return BMI160(config) diff --git a/src/Kconfig b/src/Kconfig index 6740946d8..0046e9686 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -144,6 +144,10 @@ config WANT_LIS2DW bool depends on WANT_SPI || WANT_I2C default y +config WANT_BMI160 + bool + depends on WANT_SPI || WANT_I2C + default y config WANT_MPU9250 bool depends on WANT_I2C @@ -174,7 +178,7 @@ config WANT_SENSOR_ANGLE default y config NEED_SENSOR_BULK bool - depends on WANT_ADXL345 || WANT_LIS2DW || WANT_MPU9250 || WANT_ICM20948 \ + depends on WANT_ADXL345 || WANT_LIS2DW || WANT_BMI160 || WANT_MPU9250 || WANT_ICM20948 \ || WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 || WANT_SENSOR_ANGLE default y config WANT_LOAD_CELL_PROBE @@ -232,6 +236,9 @@ config WANT_ADXL345 config WANT_LIS2DW bool "Support lis2dw and lis3dh 3-axis accelerometers" depends on WANT_SPI || WANT_I2C +config WANT_BMI160 + bool "Support BMI160 accelerometer" + depends on WANT_SPI || WANT_I2C config WANT_MPU9250 bool "Support MPU accelerometers" depends on WANT_I2C diff --git a/src/Makefile b/src/Makefile index 974204bc5..020a7bb95 100644 --- a/src/Makefile +++ b/src/Makefile @@ -20,6 +20,7 @@ src-$(CONFIG_WANT_SOFTWARE_I2C) += i2c_software.c src-$(CONFIG_WANT_THERMOCOUPLE) += thermocouple.c src-$(CONFIG_WANT_ADXL345) += sensor_adxl345.c src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c +src-$(CONFIG_WANT_BMI160) += sensor_bmi160.c src-$(CONFIG_WANT_MPU9250) += sensor_mpu9250.c src-$(CONFIG_WANT_ICM20948) += sensor_icm20948.c src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c diff --git a/src/sensor_bmi160.c b/src/sensor_bmi160.c new file mode 100644 index 000000000..64a268bd0 --- /dev/null +++ b/src/sensor_bmi160.c @@ -0,0 +1,257 @@ +// sensor_bmi160.c +// +// Support for gathering acceleration data from BMI160 chip +// +// Copyright (C) 2025 Francisco Stephens +// Based on sensor_lis2dw.c +// Copyright (C) 2023 Zhou.XianMing +// Copyright (C) 2020-2025 Kevin O'Connor +// +// This file may be distributed under the terms of the GNU GPLv3 license. +#include // memcpy +#include "autoconf.h" // CONFIG_WANT_SPI +#include "board/gpio.h" // irq_disable +#include "board/irq.h" // irq_disable +#include "board/misc.h" // timer_read_time +#include "basecmd.h" // oid_alloc +#include "command.h" // DECL_COMMAND +#include "sched.h" // DECL_TASK +#include "sensor_bulk.h" // sensor_bulk_report +#include "spicmds.h" // spidev_transfer +#include "i2ccmds.h" // i2cdev_s + +#define BMI_AR_DATAX0 0x12 +#define BMI_AM_READ 0x80 +#define BMI_FIFO_STATUS 0x22 +#define BMI_FIFO_DATA 0x24 + +#define BYTES_PER_SAMPLE 6 +#define BYTES_PER_BLOCK 48 + +struct bmi160 { + struct timer timer; + uint32_t rest_ticks; + union { + struct spidev_s *spi; + struct i2cdev_s *i2c; + }; + uint8_t bus_type; + uint8_t flags; + uint16_t fifo_bytes_pending; + struct sensor_bulk sb; +}; + +enum { + BMI_PENDING = 1<<0, +}; + +enum { + SPI_SERIAL, I2C_SERIAL, +}; + +DECL_ENUMERATION("bus_oid_type", "spi", SPI_SERIAL); +DECL_ENUMERATION("bus_oid_type", "i2c", I2C_SERIAL); + +static struct task_wake bmi160_wake; + +// Event handler that wakes bmi160_task() periodically +static uint_fast8_t +bmi160_event(struct timer *timer) +{ + struct bmi160 *ax = container_of(timer, struct bmi160, timer); + ax->flags |= BMI_PENDING; + sched_wake_task(&bmi160_wake); + return SF_DONE; +} + +void +command_config_bmi160(uint32_t *args) +{ + struct bmi160 *ax = oid_alloc(args[0], command_config_bmi160 + , sizeof(*ax)); + ax->timer.func = bmi160_event; + + switch (args[2]) { + case SPI_SERIAL: + if (CONFIG_WANT_SPI) { + ax->spi = spidev_oid_lookup(args[1]); + ax->bus_type = SPI_SERIAL; + break; + } else { + shutdown("bus_type spi unsupported"); + } + case I2C_SERIAL: + if (CONFIG_WANT_I2C) { + ax->i2c = i2cdev_oid_lookup(args[1]); + ax->bus_type = I2C_SERIAL; + break; + } else { + shutdown("bus_type i2c unsupported"); + } + default: + shutdown("bus_type invalid"); + } +} +DECL_COMMAND(command_config_bmi160, "config_bmi160 oid=%c" + " bus_oid=%c bus_oid_type=%c"); + +// Helper code to reschedule the bmi160_event() timer +static void +bmi160_reschedule_timer(struct bmi160 *ax) +{ + irq_disable(); + ax->timer.waketime = timer_read_time() + ax->rest_ticks; + sched_add_timer(&ax->timer); + irq_enable(); +} + +// Update local status tracking from newly read fifo status register +static void +update_fifo_status(struct bmi160 *ax, uint16_t fifo_bytes) +{ + // BMI160 FIFO can hold up to 1024 bytes + if (fifo_bytes > 1024) + ax->sb.possible_overflows++; + ax->fifo_bytes_pending = fifo_bytes; +} + +// Query fifo status register +static void +query_fifo_status(struct bmi160 *ax) +{ + uint16_t fifo_bytes = 0; + if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) { + uint8_t fifo[3] = { BMI_FIFO_STATUS | BMI_AM_READ, 0x00, 0x00 }; + spidev_transfer(ax->spi, 1, sizeof(fifo), fifo); + fifo_bytes = (fifo[2] << 8) | fifo[1]; + } else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) { + uint8_t fifo_reg[1] = {BMI_FIFO_STATUS}; + uint8_t fifo_val[2]; + int ret = i2c_dev_read(ax->i2c, sizeof(fifo_reg), fifo_reg + , sizeof(fifo_val), fifo_val); + i2c_shutdown_on_err(ret); + fifo_bytes = (fifo_val[1] << 8) | fifo_val[0]; + } + update_fifo_status(ax, fifo_bytes); +} + +// Read 8 samples from FIFO via SPI +static void +read_fifo_block_spi(struct bmi160 *ax) +{ + uint8_t msg[BYTES_PER_BLOCK + 1] = {0}; + msg[0] = BMI_FIFO_DATA | BMI_AM_READ; + + spidev_transfer(ax->spi, 1, sizeof(msg), msg); + memcpy(ax->sb.data, &msg[1], BYTES_PER_BLOCK); +} + +// Read 8 samples from FIFO via i2c +static void +read_fifo_block_i2c(struct bmi160 *ax) +{ + uint8_t msg_reg[] = {BMI_FIFO_DATA}; + + int ret = i2c_dev_read(ax->i2c, sizeof(msg_reg), msg_reg + , BYTES_PER_BLOCK, ax->sb.data); + i2c_shutdown_on_err(ret); +} + +// Read from fifo and transmit data to host +static void +read_fifo_block(struct bmi160 *ax, uint8_t oid) +{ + if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) + read_fifo_block_spi(ax); + else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) + read_fifo_block_i2c(ax); + ax->sb.data_count = BYTES_PER_BLOCK; + sensor_bulk_report(&ax->sb, oid); + ax->fifo_bytes_pending -= BYTES_PER_BLOCK; +} + +// Query accelerometer data +static void +bmi160_query(struct bmi160 *ax, uint8_t oid) +{ + if (ax->fifo_bytes_pending < BYTES_PER_BLOCK) + query_fifo_status(ax); + + if (ax->fifo_bytes_pending >= BYTES_PER_BLOCK) + read_fifo_block(ax, oid); + + // check if we need to run the task again (more packets in fifo?) + if (ax->fifo_bytes_pending >= BYTES_PER_BLOCK) { + // More data in fifo - wake this task again + sched_wake_task(&bmi160_wake); + } else { + // Sleep until next check time + ax->flags &= ~BMI_PENDING; + bmi160_reschedule_timer(ax); + } +} + +void +command_query_bmi160(uint32_t *args) +{ + struct bmi160 *ax = oid_lookup(args[0], command_config_bmi160); + + sched_del_timer(&ax->timer); + ax->flags = 0; + if (!args[1]) + // End measurements + return; + + // Start new measurements query + ax->rest_ticks = args[1]; + ax->fifo_bytes_pending = 0; + sensor_bulk_reset(&ax->sb); + bmi160_reschedule_timer(ax); +} +DECL_COMMAND(command_query_bmi160, "query_bmi160 oid=%c rest_ticks=%u"); + +void +command_query_bmi160_status(uint32_t *args) +{ + struct bmi160 *ax = oid_lookup(args[0], command_config_bmi160); + uint32_t time1 = 0; + uint32_t time2 = 0; + uint16_t fifo_bytes = 0; + + if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) { + uint8_t fifo[3] = { BMI_FIFO_STATUS | BMI_AM_READ, 0x00, 0x00 }; + time1 = timer_read_time(); + spidev_transfer(ax->spi, 1, sizeof(fifo), fifo); + time2 = timer_read_time(); + fifo_bytes = (fifo[2] << 8) | fifo[1]; + } else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) { + uint8_t fifo_reg[1] = {BMI_FIFO_STATUS}; + uint8_t fifo_val[2]; + time1 = timer_read_time(); + int ret = i2c_dev_read(ax->i2c, sizeof(fifo_reg), fifo_reg + , sizeof(fifo_val), fifo_val); + time2 = timer_read_time(); + i2c_shutdown_on_err(ret); + fifo_bytes = (fifo_val[1] << 8) | fifo_val[0]; + } + update_fifo_status(ax, fifo_bytes); + + sensor_bulk_status(&ax->sb, args[0], time1, time2-time1 + , ax->fifo_bytes_pending); +} +DECL_COMMAND(command_query_bmi160_status, "query_bmi160_status oid=%c"); + +void +bmi160_task(void) +{ + if (!sched_check_wake(&bmi160_wake)) + return; + uint8_t oid; + struct bmi160 *ax; + foreach_oid(oid, ax, command_config_bmi160) { + uint_fast8_t flags = ax->flags; + if (flags & BMI_PENDING) + bmi160_query(ax, oid); + } +} +DECL_TASK(bmi160_task);