Skip to content

Commit d78202c

Browse files
Charly-sketchnedseb
authored andcommitted
lis2mdl: add : driver function + exemple
1 parent 54752af commit d78202c

3 files changed

Lines changed: 184 additions & 65 deletions

File tree

lib/lis2mdl/README.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,68 @@
1-
MAgnetometer
1+
# LIS2MDL Magnetometer Driver
2+
3+
This is a MicroPython driver for the LIS2MDL 3-axis magnetometer. The LIS2MDL is a high-performance magnetic sensor designed for applications such as electronic compasses, motion tracking, and orientation detection. This driver provides methods for initialization, calibration, and reading magnetic field data.
4+
5+
## Features
6+
- Soft reset and initialization of the LIS2MDL sensor.
7+
- Configurable output data rate (ODR) and low-power mode.
8+
- 3D calibration for offsets and scales.
9+
- Heading calculation with and without tilt compensation.
10+
- Direction labeling (e.g., N, NE, E, etc.).
11+
12+
## Requirements
13+
- MicroPython-compatible board with I2C support.
14+
- LIS2MDL sensor module.
15+
16+
## Installation
17+
1. Copy the `lis2mdl` folder to your MicroPython device.
18+
2. Import the driver in your script:
19+
```python
20+
from lis2mdl.device import LIS2MDL
21+
```
22+
23+
## Usage
24+
### Basic Example
25+
```python
26+
from time import sleep_ms
27+
from machine import I2C
28+
from lis2mdl.device import LIS2MDL
29+
30+
# Initialize I2C and LIS2MDL
31+
i2c = I2C(1)
32+
mag = LIS2MDL(i2c)
33+
34+
# Perform calibration
35+
mag.calibrate_step()
36+
calibration_values = mag.get_calibration()
37+
print("Calibration values:", calibration_values)
38+
39+
# Continuous heading reading
40+
print("Continuous heading readings:")
41+
while True:
42+
angle = mag.heading_flat_only()
43+
direction = mag.direction_label(angle)
44+
print(f"{direction} | angle={angle:.2f}°")
45+
sleep_ms(100)
46+
```
47+
48+
### Tilt Compensation Example
49+
To use tilt compensation, you need an accelerometer to provide roll and pitch data. Pass a function that reads accelerometer data to the `heading_with_tilt_compensation` method.
50+
51+
```python
52+
def read_accel():
53+
# Replace with your accelerometer reading logic
54+
return ax, ay, az
55+
56+
angle = mag.heading_with_tilt_compensation(read_accel)
57+
print(f"Tilt-compensated angle: {angle:.2f}°")
58+
```
59+
60+
## Calibration
61+
The `calibrate_step` method performs a quick 3D calibration. Rotate the sensor flat over 360° to capture the minimum and maximum magnetic field values. The offsets and scales are calculated automatically.
62+
63+
## Datasheet
64+
For detailed information about the LIS2MDL sensor, refer to the [datasheet](https://www.st.com/resource/en/datasheet/lis2mdl.pdf).
65+
66+
## Notes
67+
- The `heading_with_tilt_compensation` method has not been tested due to the lack of an accelerometer during development.
68+
- Ensure the I2C pins are correctly connected to the LIS2MDL module.

lib/lis2mdl/exemple/magnet.py

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,19 @@
11
from time import sleep_ms
22
from machine import I2C
33
from lis2mdl.device import LIS2MDL
4-
import math
54

65
i2c = I2C(1)
76
mag = LIS2MDL(i2c)
87

9-
print("Quick calibration: rotate the board FLAT over 360°...")
10-
xmin = ymin = 1e9
11-
xmax = ymax = -1e9
8+
mag.calibrate_step()
9+
calibration_values = mag.get_calibration()
10+
print("x_off, y_off, z_off, x_scale, y_scale, z_scale :")
11+
print(calibration_values)
1212

13-
# capture ~3 seconds of data for min/max
14-
for _ in range(150):
15-
x, y, z = mag.read_magnet()
16-
xmin = min(xmin, x); xmax = max(xmax, x)
17-
ymin = min(ymin, y); ymax = max(ymax, y)
18-
sleep_ms(20)
19-
20-
# offsets (hard-iron) and scales (simple 2D soft-iron)
21-
x_off = (xmax + xmin) / 2.0
22-
y_off = (ymax + ymin) / 2.0
23-
x_scale = (xmax - xmin) / 2.0
24-
y_scale = (ymax - ymin) / 2.0
25-
# normalize the scale to have a circle (not essential but better)
26-
scale = (x_scale + y_scale) / 2.0
27-
28-
print("Offsets:", x_off, y_off, " Scales:", x_scale, y_scale)
29-
30-
print("\nContinuous reading (compass):")
13+
print("Lecture continue (boussole):")
3114
while True:
32-
x, y, z = mag.read_magnet()
33-
34-
# recentering + normalization
35-
x_c = (x - x_off) / scale
36-
y_c = (y - y_off) / scale
37-
38-
# heading (adjust the sign according to your reference if needed)
39-
angle = math.degrees(math.atan2(x_c,y_c))
40-
if angle < 0:
41-
angle += 360
15+
angle = mag.heading_flat_only()
16+
dir8 = mag.direction_label(angle)
4217

43-
direction = ""
44-
if angle >= 337.5 or angle < 22.5:
45-
direction = "N"
46-
elif angle >= 22.5 and angle < 67.5:
47-
direction = "NE"
48-
elif angle >= 67.5 and angle < 112.5:
49-
direction = "E"
50-
elif angle >= 112.5 and angle < 157.5:
51-
direction = "SE"
52-
elif angle >= 157.5 and angle < 202.5:
53-
direction = "S"
54-
elif angle >= 202.5 and angle < 247.5:
55-
direction = "SW"
56-
elif angle >= 247.5 and angle < 292.5:
57-
direction = "W"
58-
elif angle >= 292.5 and angle < 337.5:
59-
direction = "NW"
60-
61-
print("{} | {:.2f},{:.2f},{:.2f} | angle={:.2f}°".format(direction, x, y, z, angle))
62-
sleep_ms(100)
18+
print(f"{dir8} | angle={angle:.2f}°")
19+
sleep_ms(100)

lib/lis2mdl/lis2mdl/device.py

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,156 @@
11
# device.py
2+
# This driver is for the LIS2MDL magnetometer sensor. It provides methods for initialization, calibration, and reading magnetic field data.
3+
24
from machine import I2C
35
from lis2mdl.const import *
6+
import time
7+
import math
48

59
class LIS2MDL(object):
10+
# Default calibration offsets and scales for the magnetometer
11+
x_off = -132.0
12+
y_off = -521.5
13+
z_off = -891.0
14+
15+
x_scale = 273.0
16+
y_scale = 313.5
17+
z_scale = 295.0
18+
619
def __init__(self, i2c, address=LIS2MDL_I2C_ADDR, odr_hz=10, temp_comp=True, low_power=False, drdy_pin=False):
20+
# Initialize the LIS2MDL sensor with the given I2C interface and settings.
721
self.i2c = i2c
822
self.address = address
923
self.writebuffer = bytearray(1)
1024
self.readbuffer = bytearray(1)
1125

12-
# Soft reset property
26+
# Perform a soft reset to ensure the sensor starts in a known state.
1327
self.setReg(0x20, LIS2MDL_CFG_REG_A) # SOFT_RST=1 (not 0x10)
14-
# small delay (if needed on the MicroPython side)
1528
try:
16-
import time; time.sleep_ms(10)
29+
import time; time.sleep_ms(10) # Small delay for reset to complete
1730
except:
1831
pass
1932

20-
# CFG_REG_A: COMP_TEMP, LP, ODR, MD=00 (continuous)
33+
# Configure the sensor's operating mode, output data rate, and other settings.
2134
odr_bits = {10:0b00, 20:0b01, 50:0b10, 100:0b11}.get(odr_hz, 0b00)
2235
comp = 1 if temp_comp else 0
2336
lp = 1 if low_power else 0
2437
cfg_a = (comp<<7) | (lp<<4) | (odr_bits<<2) | 0b00
25-
self.setReg(cfg_a, LIS2MDL_CFG_REG_A) # <- essential to exit IDLE
38+
self.setReg(cfg_a, LIS2MDL_CFG_REG_A) # Essential to exit IDLE mode
2639

27-
# CFG_REG_B: default 0x00 (LPF/off_canc off); you can enable LPF if desired
28-
self.setReg(0x00, LIS2MDL_CFG_REG_B)
40+
# Configure low-pass filter and other optional settings.
41+
self.setReg(0x00, LIS2MDL_CFG_REG_B) # Default: LPF and offset cancellation off
2942

30-
# CFG_REG_C: BDU=1 (+ DRDY_on_PIN if requested)
43+
# Enable block data update and optionally configure the DRDY pin.
3144
cfg_c = 0x10 | (0x01 if drdy_pin else 0x00)
3245
self.setReg(cfg_c, LIS2MDL_CFG_REG_C)
3346

34-
# --- identical low-level ---
47+
# --- Low-level I2C communication methods ---
3548
def setReg(self, data, reg):
49+
# Write a byte to a specific register.
3650
self.writebuffer[0] = data
3751
self.i2c.writeto_mem(self.address, reg, self.writebuffer)
3852

3953
def getReg(self, reg):
54+
# Read a byte from a specific register.
4055
self.i2c.readfrom_mem_into(self.address, reg, self.readbuffer)
4156
return self.readbuffer[0]
4257

43-
# signed helper
4458
@staticmethod
4559
def _to_int16(v):
60+
# Convert an unsigned 16-bit value to a signed 16-bit value.
4661
return v - 0x10000 if v & 0x8000 else v
4762

48-
# burst read + signed
4963
def read_magnet(self):
50-
# auto-increment: reg | 0x80 (cf. datasheet I2C op)
64+
# Read the raw magnetic field data (X, Y, Z) from the sensor.
5165
buf = self.i2c.readfrom_mem(self.address, LIS2MDL_OUTX_L_REG | 0x80, 6)
5266
x = self._to_int16((buf[1] << 8) | buf[0])
5367
y = self._to_int16((buf[3] << 8) | buf[2])
5468
z = self._to_int16((buf[5] << 8) | buf[4])
5569
return (x, y, z)
5670

5771
def whoAmI(self):
72+
# Return the WHO_AM_I register value to verify the sensor's identity.
5873
return self.getReg(LIS2MDL_WHO_AM_I)
5974

6075
def status(self):
76+
# Return the STATUS_REG value to check the sensor's status.
6177
return self.getReg(LIS2MDL_STATUS_REG)
78+
79+
def set_calibrate_step(self, xoff, yoff, zoff, xscale, yscale, zscale):
80+
# Set the calibration offsets and scales manually.
81+
self.x_off = xoff
82+
self.y_off = yoff
83+
self.z_off = zoff
84+
self.x_scale = xscale
85+
self.y_scale = yscale
86+
self.z_scale = zscale
87+
88+
def calibrate_step(self):
89+
# Perform a quick 3D calibration by rotating the board flat over 360°.
90+
print("Quick 3D calibration: rotate the board FLAT over 360°...")
91+
92+
xmin = ymin = zmin = 1e9
93+
xmax = ymax = zmax = -1e9
94+
95+
for _ in range(150):
96+
x, y, z = self.read_magnet()
97+
xmin = min(xmin, x); xmax = max(xmax, x)
98+
ymin = min(ymin, y); ymax = max(ymax, y)
99+
zmin = min(zmin, z); zmax = max(zmax, z)
100+
time.sleep_ms(20)
101+
102+
# Calculate offsets and scales based on the min/max values.
103+
self.x_off = (xmax + xmin) / 2.0
104+
self.y_off = (ymax + ymin) / 2.0
105+
self.z_off = (zmax + zmin) / 2.0
106+
107+
self.x_scale = (xmax - xmin) / 2.0
108+
self.y_scale = (ymax - ymin) / 2.0
109+
self.z_scale = (zmax - zmin) / 2.0
110+
111+
def get_calibration(self):
112+
# Return the current calibration offsets and scales.
113+
return (self.x_off, self.y_off, self.z_off, self.x_scale, self.y_scale, self.z_scale)
114+
115+
def heading_flat_only(self):
116+
# Calculate the heading (angle) assuming the sensor is flat.
117+
x, y, z = self.read_magnet()
118+
x = (x - self.x_off) / self.x_scale
119+
y = (y - self.y_off) / self.y_scale
120+
angle = math.degrees(math.atan2(x, y))
121+
if angle < 0:
122+
angle += 360
123+
return angle
124+
125+
def heading_with_tilt_compensation(self, read_accel):
126+
# Calculate the heading with tilt compensation using accelerometer data.
127+
# Note: This method has not been tested as no accelerometer was available during development.
128+
129+
# Read magnetometer and accelerometer data.
130+
x, y, z = self.read_magnet()
131+
ax, ay, az = read_accel()
132+
133+
# Apply 3D calibration (offsets and scales for each axis).
134+
x = (x - self.x_off) / self.x_scale
135+
y = (y - self.y_off) / self.y_scale
136+
z = (z - self.z_off) / self.z_scale
137+
138+
# Calculate roll and pitch from accelerometer data.
139+
roll = math.atan2(ay, az)
140+
pitch = math.atan2(-ax, math.sqrt(ay*ay + az*az))
141+
142+
# Adjust the magnetic vector to account for tilt.
143+
Xh = x * math.cos(pitch) + z * math.sin(pitch)
144+
Yh = x * math.sin(roll) * math.sin(pitch) + y * math.cos(roll) - z * math.sin(roll) * math.cos(pitch)
145+
146+
# Calculate the heading (0 to 360°).
147+
angle = math.degrees(math.atan2(Xh, Yh))
148+
if angle < 0:
149+
angle += 360
150+
return angle
151+
152+
def direction_label(self, angle):
153+
# Return a compass direction label (e.g., N, NE, E, etc.) based on the angle.
154+
dirs = ["N","NE","E","SE","S","SW","W","NW","N"]
155+
idx = int((angle + 22.5)//45)
156+
return dirs[idx]

0 commit comments

Comments
 (0)