From 6ce0fcfc4eb20b5b5d77253322793a7aa73f95ef Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 2 Mar 2026 00:06:25 +0100 Subject: [PATCH] lilygo_t_watch_s3_plus: add bma423 IMU sensor --- .../lib/drivers/imu_sensor/bma423/bma423.py | 379 ++++++++++++++++++ .../lib/drivers/imu_sensor/bma423/git.version | 1 + .../lib/mpos/board/lilygo_t_watch_s3_plus.py | 11 +- internal_filesystem/lib/mpos/main.py | 2 +- 4 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 internal_filesystem/lib/drivers/imu_sensor/bma423/bma423.py create mode 100644 internal_filesystem/lib/drivers/imu_sensor/bma423/git.version diff --git a/internal_filesystem/lib/drivers/imu_sensor/bma423/bma423.py b/internal_filesystem/lib/drivers/imu_sensor/bma423/bma423.py new file mode 100644 index 00000000..6dae8f08 --- /dev/null +++ b/internal_filesystem/lib/drivers/imu_sensor/bma423/bma423.py @@ -0,0 +1,379 @@ +# BMA423 driver that aims to support features detection. +# +# Copyright (C) 2024 Salvatore Sanfilippo -- All Rights Reserved +# This code is released under the MIT license +# https://opensource.org/license/mit/ +# +# Written reading the specification at: +# https://www.mouser.com/datasheet/2/783/BST-BMA423-DS000-1509600.pdf + +from machine import Pin +import time + +# Registers +REG_CHIP_ID = const(0x00) # Chip identification number. +REG_INT_STATUS_0 = const(0x1C) # Interrupt status for features detection. +REG_INT_STATUS_1 = const(0x1D) # Interrupt status for data ready. +REG_STEP_COUNTER_0 = const(0x1E) # For bytes starting here. Number of steps. +REG_TEMPERATURE = const(0x22) # Temperature sensor reading: kelvin units. +REG_INTERNAL_STATUS = const(0x2A) # Error / status bits. +REG_ACC_CONF = const(0x40) # Out data rate, bandwidth, read mode. +REG_ACC_RANGE = const(0x41) # Acceleration range selection. +REG_PWR_CONF = const(0x7c) # Power mode configuration. +REG_PWR_CTL = const(0x7d) # Used to power-on the device parts +REG_CMD = const(0x7e) # Write there to send commands +REG_INT1_IO_CTRL = const(0x53) # Electrical config of interrupt 1 pin. +REG_INT2_IO_CTRL = const(0x54) # Electrical config of interrupt 2 pin. +REG_INT_LATCH = const(0x55) # Interrupt latch mode. +REG_INT1_MAP = const(0x56) # Interrput map for detected features and pin1. +REG_INT2_MAP = const(0x57) # Interrput map for detected features and pin2. +REG_INT_MAP_DATA = const(0x58) # Interrupt map for pin1/2 data events. +REG_INIT_CTRL = const(0x59) # Initialization register. +FEATURES_IN_SIZE = const(70) # Size of the features configuration area + +# Commands for the REG_CMD register +REG_CMD_SOFTRESET = const(0xB6) + +class BMA423: + # Acceleration range can be selected among the available settings of + # 2G, 4G, 8G and 16G. If we want to be able to measure higher + # max accelerations, the relative precision decreases as we have + # a fixed 12 bit reading. + def __init__(self,i2c,*,acc_range=2): + default_addr = [0x18,0x19] # Changes depending on SDO pin + # pulled to ground or V+ + self.i2c = i2c + self.myaddr = None + self.features_in = bytearray(FEATURES_IN_SIZE) + + found_devices = i2c.scan() + print("BMA423: scan i2c bus:", [hex(x) for x in found_devices]) + for addr in default_addr: + if addr in found_devices: + self.myaddr = addr + break + if self.myaddr == None: + raise Exception("BMA423 not found at i2c bus") + print("BMA423: device with matching address found at",hex(self.myaddr)) + + # Device initialization. + self.reset() + chip_id = self.get_reg(REG_CHIP_ID) + if chip_id != 0x13: + raise Exception("BMA423 chip ID is not 0x13 as expected. Different sensor connected?") + print("BMA423: chip correctly identified.") + + # Set default parameters. By default we enable the accelerometer + # so that the user can read the acceleration vector from the + # device without much setup work. + self.enable_accelerometer(acc=True,aux=False) + self.set_accelerometer_perf(True) + self.set_accelerometer_avg(2) + self.set_accelerometer_freq(100) + self.set_advanced_power_save(False,False) + self.set_range(acc_range) + + # Soft reset using the commands register. + def reset(self): + self.set_reg(REG_CMD,REG_CMD_SOFTRESET) # Reset the chip. + time.sleep(1) # Datasheet claims we need to wait that much. I know. + + # Enable or disable advanced power saving (ADP). + # + # When data is not being sampled, power saving mode slows down the + # clock and makes latency higher. + # Fifo self wakeup controls if the FIFO works when ADP is enabled. + # Step counting less reliable if APS is enabled (note of the implementator). + def set_advanced_power_save(self,adp=False,fifo_self_wakeup=False): + adp = int(adp) & 1 + fifo_self_wakeup = (int(fifo_self_wakeup) & 1) << 1 + self.set_reg(REG_PWR_CONF,adp|fifo_self_wakeup) + + # Enable/Disable accelerometer and aux sensor. + def enable_accelerometer(self,*,acc=True,aux=False): + val = 0 + if acc: val |= 0x4 # acc_en bit, enable accelerometer acquisition. + if aux: val |= 0x1 # aux_en bit, enable aux sensor. + self.set_reg(REG_PWR_CTL,val) + + # Enable/Disable performance mode. When performance mode is enabled + # the accelerometer performs continuous sampling at the specified + # sampling rate. + def set_accelerometer_perf(self,perf_mode): + val = self.get_reg(REG_ACC_CONF) + val = (val & 0b01111111) | (int(perf_mode) << 7) + self.set_reg(REG_ACC_CONF,val) + + # Set average mode. The mode selected depends on the fact performance + # mode is enabled/disabled. + # Valid values: + # perf mode on: 0 = osr4, 1 = osr2, 2 = normal, 3 = cic. + # perf mode off: 0 = avg1, 1 = avg2, 2 = avg4, 3 = avg8 + # 4 = avg16, 5 = avg32, 6 = avg64, 7 = avg128. + def set_accelerometer_avg(self,avg_mode): + val = self.get_reg(REG_ACC_CONF) + val = (val & 0b10001111) | avg_mode << 4 + self.set_reg(REG_ACC_CONF,val) + + # Set accelerometer sampling frequency, either as a frequency in + # hz that we convert using a table, or as immediate value if the + # user wants to select one of the low frequency modes (see datasheet). + def set_accelerometer_freq(self,freq): + table = {25:6, 50:7, 100:8, 200:9, 400:10, 800:11, 1600:12} + if freq in table: + freq = table[freq] + elif freq == 0 or freq >= 0x0d: + raise Exception("Invalid frequency or raw value") + val = self.get_reg(REG_ACC_CONF) + val = (val & 0b11110000) | freq + self.set_reg(REG_ACC_CONF,val) + + # Write in the FEATURES-IN configuration to enable specific + # features. + def enable_features_detection(self,*features): + self.read_features_in() + for f in features: + if f == "step-count": + self.features_in[0x3B] |= 0x10 # Enable step counter. + else: + raise Exception("Unrecognized feature name",f) + self.write_features_in() + + # Prepare the device to load the binary configuration in the + # bma423conf.bin file (data from Bosch). This step is required for + # features detection. + def load_features_config(self): + saved_pwr_conf = self.get_reg(REG_PWR_CONF) # To restore it later. + self.set_reg(REG_PWR_CONF,0x00) # Disable adv_power_save. + time.sleep_us(500) # Wait time synchronization. + self.set_reg(REG_INIT_CTRL,0x00) # Prepare for loading configuration. + self.transfer_config() # Load binary features config. + self.set_reg(REG_INIT_CTRL,0x01) # Enable features. + time.sleep_ms(140) # Wait ASIC initialization. + + # The chip is ready for further configuration when the + # status "message" turns 1. + wait_epoch = 0 + while True: + status = self.get_reg(REG_INTERNAL_STATUS) & 0b11111 + if status == 1: break # Initialization successful + time.sleep_ms(50) + wait_epoch += 1 + if wait_epoch == 20: + raise Exception("Timeout during init, internal_status: ", + status) + print("BMA423: features engine initialized successfully.") + self.set_reg(REG_PWR_CONF,saved_pwr_conf) + + # Write to the ASIC memory. This is useful to set the device + # features configuration. + # + # Writing / reading from ASIC works setting two registers that + # point to the memory area(0x5B/5C), and then reading/writing from/to + # the register 0x5E. Note that while normally writing / reading + # to a given register will write bytes to successive registers, in + # the case of 0x5E it works like a "port", so we keep reading + # or writing from successive parts of the ASIC memory. + def write_config_mem(self,idx,buf): + # The index of the half-word (so index/2) must + # be placed into this two undocumented registers + # 0x5B and 0x5C. Data goes in 0xE. + # Thanks for the mess, Bosch! + self.set_reg(0x5b,(idx//2)&0xf) # Set LSB (bits 3:0) + self.set_reg(0x5c,(idx//2)>>4) # Set MSB (bits 11:5) + self.set_reg(0x5e,buf) + + # see write_config_mem(). + def read_config_mem(self,idx,count): + self.set_reg(0x5b,(idx//2)&0xf) # Set LSB (bits 3:0) + self.set_reg(0x5c,(idx//2)>>4) # Set MSB (bits 11:5) + return self.get_reg(0x5e,count) + + # Read the steps counter. + def get_steps(self): + data = self.get_reg(REG_STEP_COUNTER_0,4) + return data[0] | data[1]<<8 | data[2]<<16 | data[3]<<24 + + # The BMA423 features detection requires that we transfer a binary + # blob via the features configuration register (and other two undocumented + # registers that set the internal target address at which the register + # points). If this are the weights of a small neural network, or just + # parameters, I'm not sure. More info (LOL, not really) here: + # + # https://github.com/boschsensortec/BMA423_SensorDriver + def transfer_config(self): + print("Uploading features configuration...") + f = open("bma423conf.bin","rb") + buf = bytearray(8) # Binary config is multiple of 8 in len. + idx = 0 + while f.readinto(buf,8) == 8: + self.write_config_mem(idx,buf) + idx += 8 + print("Done: total transfer: ", idx) + + # Verify the content. + print("BMA423: Verifying stored configuration...") + idx = 0 + f.seek(0) + while f.readinto(buf,8) == 8: + content = self.read_config_mem(idx,8) + idx += 8 + if content != buf: + raise Exception("Feature config data mismatch at",idx) + f.close() + + # Enable interrupt for the specified list of events. + # + # 'chip_pin' is 1 or 2 (the chip has two interrupt pins), you should + # select the one you want to use or the one you have an actual + # connection to with your host. + # 'pin' is your machine.Pin instance in your host. + # 'callback' is the function to call when the specified events will fire. + # 'events' is a list of strings specifying what events you want to + # listen for. Valid events are: + # "data": new acceleration reading available. + # "fifo-wm: fifo watermark reached. + # "fifo-full": fifo is full. + # "step": step feature. + # "activity": detect walking, running, ... + # "tilt": tilt on wrist. + # "double-tap": double tap. + # "single-tap": single tap. + # "any-none": any motion / no motion detected. + # Note: you can't subscribe to both double and single tap. + def enable_interrupt(self,chip_pin,pin,callback,events): + self.callback = callback + + # Features detection only work in latch mode. + self.set_reg(REG_INT_LATCH,0x01) + # feature name -> bit to set in INT1/2_MAP. + feature_bits = {"any-none":6,"tilt":3,"activity":2,"step":1} + # data source name -> bit to set for [pin1,pin2] in + # INT_MAP_DATA. + data_bits = {"data":[2,6],"fifo-wm":[1,5],"fifo-full":[0,4]} + + # Set features/data interrupt maps register values. + feature_map,data_map = 0,0 + for e in events: + if e in feature_bits: + feature_map |= (1 << feature_bits[e]) + elif e in data_bits: + data_map |= data_bits[e][chip_pin-1] + else: + raise Exception(f"Unknown event {e} when enabling interrupt.") + if feature_map != 0: + map_int_reg = REG_INT1_MAP if chip_pin == 1 else REG_INT2_MAP + self.set_reg(map_int_reg,feature_map) + if data_map != 0: self.set_reg(REG_INT_MAP_DATA,data_map) + + # XXX: set config registers according to single/double tap. + # XXX: set FEATURES_IN registers. + + # Configure the electrical interrput pin behavior. + ctrl_reg = REG_INT1_IO_CTRL if chip_pin == 1 else REG_INT2_IO_CTRL + # Output enabled 0x8, active high 0x2, all other bits zero, that + # is: input_enabled=no, edge_ctrl=level-trigger, od=push-pull. + self.set_reg(ctrl_reg, 0x8 | 0x2) + + # Finally enable the interrupt in the host pin. + pin.irq(handler=self.irq, trigger=Pin.IRQ_RISING) + + # Set range of 2, 4 or 8 or 16g + def set_range(self,acc_range): + range_to_regval = {2:0,4:1,8:2,16:3} + if not acc_range in range_to_regval: + raise Exception(f"Invalid range {acc_range}: use 2, 4, 8, 16", + acc_range) + self.range = acc_range + self.set_reg(REG_ACC_RANGE,range_to_regval[acc_range]) + + # Convert the raw 12 bit number in two's complement as a signed + # number. + def convert_to_int12(self,raw_value): + if not raw_value & 0x800: return raw_value + raw_value = ((~raw_value) & 0x7ff) + 1 + return -raw_value + + # Normalize the signed 12 bit acceleration value to + # acceleration value in "g" according to the currently + # selected range. + def normalize_reading(self,reading): + return self.range / 2047 * reading + + # Return x,y,z acceleration. + def get_xyz(self): + rawdata = self.get_reg(0x12,6) + acc_x = (rawdata[0] >> 4) | (rawdata[1] << 4) + acc_y = (rawdata[2] >> 4) | (rawdata[3] << 4) + acc_z = (rawdata[4] >> 4) | (rawdata[5] << 4) + acc_x = self.convert_to_int12(acc_x) + acc_y = self.convert_to_int12(acc_y) + acc_z = self.convert_to_int12(acc_z) + acc_x = self.normalize_reading(acc_x) + acc_y = self.normalize_reading(acc_y) + acc_z = self.normalize_reading(acc_z) + return (acc_x,acc_y,acc_z) + + # Return the chip tempereature in celsius. + # If the temperature is invalid, None is returned. + def get_temperature(self): + raw = self.get_reg(REG_TEMPERATURE) + if raw == 0x80: return None + if raw & 0x80: + raw = -((~raw)+1) # Conver 2 complement to signed integer. + return 23+raw + + def irq(self,pin): + if self.callback == None: + printf("BMA423: not handled IRQ. Please, set a callback.") + return + data = {} + + print("IRQ CALLED") + + if len(data) == None: return + self.callback(data) + + # Return the single byte at the specified register + def get_reg(self, register, count=1): + if count == 1: + return self.i2c.readfrom_mem(self.myaddr,register,1)[0] + else: + return self.i2c.readfrom_mem(self.myaddr,register,count) + + # Write 'value' to the specified register + def set_reg(self, register, value): + if isinstance(value,bytearray) or isinstance(value,bytes): + self.i2c.writeto_mem(self.myaddr,register,value) + else: + self.i2c.writeto_mem(self.myaddr,register,bytes([value])) + + def read_features_in(self): + self.i2c.readfrom_mem_into(self.myaddr,0x5E,self.features_in) + + def write_features_in(self): + self.i2c.writeto_mem(self.myaddr,0x5E,self.features_in) + +# Example usage and quick test to see if your device is working. +if __name__ == "__main__": + from machine import SoftI2C, Pin + import time + + # Called when a feature/data interrupt triggers. + def mycallback(data): + print(data) + + i2c = SoftI2C(scl=11,sda=10) + sensor = BMA423(i2c) + sensor.enable_interrupt(1,Pin(14,Pin.IN),mycallback,["data"]) + + # Enable steps counting + sensor.load_features_config() + sensor.enable_features_detection("step-count") + + while True: + print("(x,y,z),temp,steps", + sensor.get_xyz(), + sensor.get_temperature(), + sensor.get_steps()) + time.sleep(.1) diff --git a/internal_filesystem/lib/drivers/imu_sensor/bma423/git.version b/internal_filesystem/lib/drivers/imu_sensor/bma423/git.version new file mode 100644 index 00000000..bc0e9abd --- /dev/null +++ b/internal_filesystem/lib/drivers/imu_sensor/bma423/git.version @@ -0,0 +1 @@ +9ce483a0e067629a10486a305d9fb91ce5d2bad2 diff --git a/internal_filesystem/lib/mpos/board/lilygo_t_watch_s3_plus.py b/internal_filesystem/lib/mpos/board/lilygo_t_watch_s3_plus.py index ec840850..7dfa6b94 100644 --- a/internal_filesystem/lib/mpos/board/lilygo_t_watch_s3_plus.py +++ b/internal_filesystem/lib/mpos/board/lilygo_t_watch_s3_plus.py @@ -81,7 +81,7 @@ mic_input = AudioManager.add( # Vibrator test -# One extremely strong & fairly long buzz (repeat as needed) +# One strong & fairly long buzz (repeat as needed) write_reg(0x01, 0x00) # internal trigger write_reg(0x03, 0) # Library A write_reg(0x04, 47) # Strong Buzz 100% @@ -90,6 +90,15 @@ import time time.sleep(1) # ~0.8s strong buzz write_reg(0x0C, 0) # stop (optional) +# IMU: +import drivers.imu_sensor.bma423.bma423 as bma423 +from machine import SoftI2C, Pin +i2c = SoftI2C(scl=11,sda=10) +sensor = bma423.BMA423(i2c) +print("temperature: ", sensor.get_temperature()) +print("steps: ", sensor.get_steps()) +print("(x,y,z): ", sensor.get_xyz()) + # TODO: # - battery # - IMU diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index 14a65a95..ad37f1f6 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -107,7 +107,7 @@ def detect_board(): print("lilygo_t_watch_s3_plus ?") if i2c0 := fail_save_i2c(sda=10, scl=11): - if single_address_i2c_scan(i2c0, 0x19): # IMU on 32? but scan shows: [25, 52, 81, 90] + if single_address_i2c_scan(i2c0, 0x19): # IMU on 0x19, scan shows: [25, 52, 81, 90] return "lilygo_t_watch_s3_plus" # example MAC address: D0:CF:13:33:36:306 # Then do I2C-based board detection