PyBMA400/pyBMA400/driver.py
2025-05-24 12:23:08 +01:00

363 lines
12 KiB
Python

# bma400.py - BMA400 Accelerometer Library for Python
import time
import struct
from typing import Tuple, Optional, Union, List
try:
import smbus
except ImportError:
pass # Allow for non-Raspberry Pi environments
class BMA400:
"""
Python driver for the Bosch BMA400 Accelerometer.
This library provides access to the BMA400 accelerometer over I2C,
allowing reading of acceleration data and configuration of sensor parameters.
:param i2c_bus: The I2C bus number (usually 1 on Raspberry Pi 3+)
:param address: The I2C device address, defaults to 0x14
"""
# Register addresses
_REG_WHOAMI = 0x00
_ACC_CONFIG0 = 0x19
_ACC_CONFIG1 = 0x1A
_ACC_CONFIG2 = 0x1B
_ACCEL_DATA = 0x04
_TEMP_DATA = 0x11
# Constants for conversion
_ACC_CONVERSION = 9.80665 # Convert to m/s²
# Power mode values
SLEEP_MODE = 0x00
LOW_POWER_MODE = 0x01
NORMAL_MODE = 0x02
SWITCH_TO_SLEEP = 0x03
# Output data rate values
ACCEL_12_5HZ = 0x05
ACCEL_25HZ = 0x06
ACCEL_50HZ = 0x07
ACCEL_100HZ = 0x08
ACCEL_200HZ = 0x09
ACCEL_400HZ = 0xA4
ACCEL_800HZ = 0xB8
# Filter bandwidth values
ACC_FILT_BW0 = 0x00 # 0.48 x ODR
ACC_FILT_BW1 = 0x01 # 0.24 x ODR
# Oversampling values
OVERSAMPLING_0 = 0x00
OVERSAMPLING_1 = 0x01
OVERSAMPLING_2 = 0x02
OVERSAMPLING_3 = 0x03
# Acceleration range values
ACC_RANGE_2 = 0x00 # ±2g
ACC_RANGE_4 = 0x01 # ±4g
ACC_RANGE_8 = 0x02 # ±8g
ACC_RANGE_16 = 0x03 # ±16g
# Range factors for converting raw values
_acc_range_factor = {
ACC_RANGE_2: 1024,
ACC_RANGE_4: 512,
ACC_RANGE_8: 256,
ACC_RANGE_16: 128
}
# Data source register values
ACC_FILT1 = 0x00
ACC_FILT2 = 0x01
ACC_FILT_LP = 0x02
def __init__(self, i2c_bus: int = 1, address: int = 0x14):
"""
Initialize the BMA400 sensor.
:param i2c_bus: The I2C bus number
:param address: The I2C device address
:raises RuntimeError: If the sensor is not found
"""
self._bus = smbus.SMBus(i2c_bus)
self._address = address
# Check device ID
device_id = self._read_byte(BMA400._REG_WHOAMI)
if device_id != 0x90:
raise RuntimeError(f"Failed to find BMA400, got device ID: 0x{device_id:02X}")
# Initialize with default settings
self._power_mode = self.NORMAL_MODE
self._acc_range = self.ACC_RANGE_2
self._output_data_rate = self.ACCEL_100HZ
self._oversampling_rate = self.OVERSAMPLING_3
self._filter_bandwidth = self.ACC_FILT_BW0
self._source_data_registers = self.ACC_FILT1
# Apply default settings
self._write_register_bits(self._ACC_CONFIG0, 0, 2, self._power_mode)
self._write_register_bits(self._ACC_CONFIG0, 7, 1, self._filter_bandwidth)
self._write_register_bits(self._ACC_CONFIG1, 0, 4, self._output_data_rate)
self._write_register_bits(self._ACC_CONFIG1, 4, 2, self._oversampling_rate)
self._write_register_bits(self._ACC_CONFIG1, 6, 2, self._acc_range)
self._write_register_bits(self._ACC_CONFIG2, 2, 2, self._source_data_registers)
def _read_byte(self, register: int) -> int:
"""Read a single byte from the specified register"""
return self._bus.read_byte_data(self._address, register)
def _read_bytes(self, register: int, length: int) -> List[int]:
"""Read multiple bytes from the specified register"""
return self._bus.read_i2c_block_data(self._address, register, length)
def _write_byte(self, register: int, value: int) -> None:
"""Write a single byte to the specified register"""
self._bus.write_byte_data(self._address, register, value)
def _write_register_bits(self, register: int, pos: int, length: int, value: int) -> None:
"""
Write specific bits in a register
:param register: Register address
:param pos: Position of the LSB (0-indexed)
:param length: Number of bits to modify
:param value: Value to write
"""
current = self._read_byte(register)
mask = (1 << length) - 1
current &= ~(mask << pos) # Clear the bits
current |= (value & mask) << pos # Set the bits
self._write_byte(register, current)
def _read_register_bits(self, register: int, pos: int, length: int) -> int:
"""
Read specific bits from a register
:param register: Register address
:param pos: Position of the LSB (0-indexed)
:param length: Number of bits to read
:return: Value of the specified bits
"""
value = self._read_byte(register)
mask = (1 << length) - 1
return (value >> pos) & mask
@property
def power_mode(self) -> str:
"""
Get the current power mode.
:return: String describing the current power mode
"""
mode = self._read_register_bits(self._ACC_CONFIG0, 0, 2)
modes = ["SLEEP_MODE", "LOW_POWER_MODE", "NORMAL_MODE", "SWITCH_TO_SLEEP"]
return modes[mode]
@power_mode.setter
def power_mode(self, value: int) -> None:
"""
Set the power mode.
:param value: Power mode value (use BMA400.XXX_MODE constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.SLEEP_MODE, self.LOW_POWER_MODE, self.NORMAL_MODE, self.SWITCH_TO_SLEEP]:
raise ValueError("Value must be a valid power mode setting")
self._write_register_bits(self._ACC_CONFIG0, 0, 2, value)
self._power_mode = value
@property
def output_data_rate(self) -> str:
"""
Get the current output data rate.
:return: String describing the current output data rate
"""
odr = self._read_register_bits(self._ACC_CONFIG1, 0, 4)
rates = {
0x05: "ACCEL_12_5HZ",
0x06: "ACCEL_25HZ",
0x07: "ACCEL_50HZ",
0x08: "ACCEL_100HZ",
0x09: "ACCEL_200HZ",
0xA4: "ACCEL_400HZ",
0xB8: "ACCEL_800HZ"
}
return rates.get(odr, f"UNKNOWN (0x{odr:02X})")
@output_data_rate.setter
def output_data_rate(self, value: int) -> None:
"""
Set the output data rate.
:param value: Output data rate value (use BMA400.ACCEL_XXX constants)
:raises ValueError: If the value is invalid
"""
valid_values = [self.ACCEL_12_5HZ, self.ACCEL_25HZ, self.ACCEL_50HZ,
self.ACCEL_100HZ, self.ACCEL_200HZ, self.ACCEL_400HZ,
self.ACCEL_800HZ]
if value not in valid_values:
raise ValueError("Value must be a valid output data rate setting")
self._write_register_bits(self._ACC_CONFIG1, 0, 4, value)
self._output_data_rate = value
@property
def oversampling_rate(self) -> str:
"""
Get the current oversampling rate.
:return: String describing the current oversampling rate
"""
osr = self._read_register_bits(self._ACC_CONFIG1, 4, 2)
rates = ["OVERSAMPLING_0", "OVERSAMPLING_1", "OVERSAMPLING_2", "OVERSAMPLING_3"]
return rates[osr]
@oversampling_rate.setter
def oversampling_rate(self, value: int) -> None:
"""
Set the oversampling rate.
:param value: Oversampling rate value (use BMA400.OVERSAMPLING_X constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.OVERSAMPLING_0, self.OVERSAMPLING_1,
self.OVERSAMPLING_2, self.OVERSAMPLING_3]:
raise ValueError("Value must be a valid oversampling rate setting")
self._write_register_bits(self._ACC_CONFIG1, 4, 2, value)
self._oversampling_rate = value
@property
def acc_range(self) -> str:
"""
Get the current acceleration range.
:return: String describing the current acceleration range
"""
range_val = self._read_register_bits(self._ACC_CONFIG1, 6, 2)
ranges = ["ACC_RANGE_2", "ACC_RANGE_4", "ACC_RANGE_8", "ACC_RANGE_16"]
return ranges[range_val]
@acc_range.setter
def acc_range(self, value: int) -> None:
"""
Set the acceleration range.
:param value: Acceleration range value (use BMA400.ACC_RANGE_X constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.ACC_RANGE_2, self.ACC_RANGE_4,
self.ACC_RANGE_8, self.ACC_RANGE_16]:
raise ValueError("Value must be a valid acceleration range setting")
self._write_register_bits(self._ACC_CONFIG1, 6, 2, value)
self._acc_range = value
@property
def filter_bandwidth(self) -> str:
"""
Get the current filter bandwidth.
:return: String describing the current filter bandwidth
"""
bw = self._read_register_bits(self._ACC_CONFIG0, 7, 1)
bandwidths = ["ACC_FILT_BW0", "ACC_FILT_BW1"]
return bandwidths[bw]
@filter_bandwidth.setter
def filter_bandwidth(self, value: int) -> None:
"""
Set the filter bandwidth.
:param value: Filter bandwidth value (use BMA400.ACC_FILT_BWx constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.ACC_FILT_BW0, self.ACC_FILT_BW1]:
raise ValueError("Value must be a valid filter bandwidth setting")
self._write_register_bits(self._ACC_CONFIG0, 7, 1, value)
self._filter_bandwidth = value
@property
def source_data_registers(self) -> str:
"""
Get the current source data register setting.
:return: String describing the current source data register
"""
src = self._read_register_bits(self._ACC_CONFIG2, 2, 2)
sources = ["ACC_FILT1", "ACC_FILT2", "ACC_FILT_LP"]
return sources[src] if src < len(sources) else f"UNKNOWN (0x{src:02X})"
@source_data_registers.setter
def source_data_registers(self, value: int) -> None:
"""
Set the source data register.
:param value: Source data register value (use BMA400.ACC_FILT_X constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.ACC_FILT1, self.ACC_FILT2, self.ACC_FILT_LP]:
raise ValueError("Value must be a valid source data register setting")
self._write_register_bits(self._ACC_CONFIG2, 2, 2, value)
self._source_data_registers = value
@property
def acceleration(self) -> Tuple[float, float, float]:
"""
Get the current acceleration measurements in m/s².
:return: Tuple of (x, y, z) acceleration values
"""
# Read 6 bytes (2 bytes per axis, x, y, z)
data = self._read_bytes(self._ACCEL_DATA, 6)
# Convert data to 16-bit signed integers
rawx = (data[1] << 8) | data[0]
rawy = (data[3] << 8) | data[2]
rawz = (data[5] << 8) | data[4]
# Apply two's complement if needed
if rawx > 2047:
rawx -= 4096
if rawy > 2047:
rawy -= 4096
if rawz > 2047:
rawz -= 4096
# Convert to m/s² based on range setting
factor = self._acc_range_factor[self._acc_range] * self._ACC_CONVERSION
return (rawx / factor, rawy / factor, rawz / factor)
@property
def temperature(self) -> float:
"""
Get the current temperature in Celsius.
The temperature sensor is calibrated with a precision of ±5°C.
:return: Temperature in Celsius
"""
raw_temp = self._read_byte(self._TEMP_DATA)
# Apply two's complement for 8-bit value
if raw_temp > 127:
raw_temp -= 256
# Convert to Celsius according to datasheet
return (raw_temp * 0.5) + 23.0
def soft_reset(self) -> None:
"""
Perform a soft reset of the sensor.
This resets all registers to their default values.
"""
# Command register and soft reset command
CMD_REG = 0x7E
SOFT_RESET_CMD = 0xB6
self._write_byte(CMD_REG, SOFT_RESET_CMD)
time.sleep(0.2) # Wait for reset to complete