1+ #
2+ # This file is part of MicroPython MPU9250 driver
3+ # Copyright (c) 2018 Mika Tuupola
4+ #
5+ # Licensed under the MIT license:
6+ # http://www.opensource.org/licenses/mit-license.php
7+ #
8+ # See:
9+ # https://github.com/tuupola/micropython-mpu9250
10+ # https://www.akm.com/akm/en/file/datasheet/AK8963C.pdf
11+ #
12+
13+ """
14+ MicroPython I2C driver for AK8963 magnetometer
15+ """
16+
17+ __version__ = "0.2.0"
18+
19+ # pylint: disable=import-error
20+ import ustruct
21+ import utime
22+ from machine import I2C , Pin
23+ from micropython import const
24+ # pylint: enable=import-error
25+
26+ _WIA = const (0x00 )
27+ _HXL = const (0x03 )
28+ _HXH = const (0x04 )
29+ _HYL = const (0x05 )
30+ _HYH = const (0x06 )
31+ _HZL = const (0x07 )
32+ _HZH = const (0x08 )
33+ _ST2 = const (0x09 )
34+ _CNTL1 = const (0x0a )
35+ _ASAX = const (0x10 )
36+ _ASAY = const (0x11 )
37+ _ASAZ = const (0x12 )
38+
39+ _MODE_POWER_DOWN = 0b00000000
40+ MODE_SINGLE_MEASURE = 0b00000001
41+ MODE_CONTINOUS_MEASURE_1 = 0b00000010 # 8Hz
42+ MODE_CONTINOUS_MEASURE_2 = 0b00000110 # 100Hz
43+ MODE_EXTERNAL_TRIGGER_MEASURE = 0b00000100
44+ _MODE_SELF_TEST = 0b00001000
45+ _MODE_FUSE_ROM_ACCESS = 0b00001111
46+
47+ OUTPUT_14_BIT = 0b00000000
48+ OUTPUT_16_BIT = 0b00010000
49+
50+ _SO_14BIT = 0.6 # μT per digit when 14bit mode
51+ _SO_16BIT = 0.15 # μT per digit when 16bit mode
52+
53+ class AK8963 :
54+ """Class which provides interface to AK8963 magnetometer."""
55+ def __init__ (
56+ self , i2c , address = 0x0c ,
57+ mode = MODE_CONTINOUS_MEASURE_1 , output = OUTPUT_16_BIT ,
58+ offset = (0 , 0 , 0 ), scale = (1 , 1 , 1 )
59+ ):
60+ self .i2c = i2c
61+ self .address = address
62+ self ._offset = offset
63+ self ._scale = scale
64+
65+ if 0x48 != self .whoami :
66+ raise RuntimeError ("AK8963 not found in I2C bus." )
67+
68+ # Sensitivity adjustement values
69+ self ._register_char (_CNTL1 , _MODE_FUSE_ROM_ACCESS )
70+ asax = self ._register_char (_ASAX )
71+ asay = self ._register_char (_ASAY )
72+ asaz = self ._register_char (_ASAZ )
73+ self ._register_char (_CNTL1 , _MODE_POWER_DOWN )
74+
75+ # Should wait atleast 100us before next mode
76+ self ._adjustement = (
77+ (0.5 * (asax - 128 )) / 128 + 1 ,
78+ (0.5 * (asay - 128 )) / 128 + 1 ,
79+ (0.5 * (asaz - 128 )) / 128 + 1
80+ )
81+
82+ # Power on
83+ self ._register_char (_CNTL1 , (mode | output ))
84+
85+ if output is OUTPUT_16_BIT :
86+ self ._so = _SO_16BIT
87+ else :
88+ self ._so = _SO_14BIT
89+
90+ @property
91+ def magnetic (self ):
92+ """
93+ X, Y, Z axis micro-Tesla (uT) as floats.
94+ """
95+ xyz = list (self ._register_three_shorts (_HXL ))
96+ self ._register_char (_ST2 ) # Enable updating readings again
97+
98+ # Apply factory axial sensitivy adjustements
99+ xyz [0 ] *= self ._adjustement [0 ]
100+ xyz [1 ] *= self ._adjustement [1 ]
101+ xyz [2 ] *= self ._adjustement [2 ]
102+
103+ # Apply output scale determined in constructor
104+ so = self ._so
105+ xyz [0 ] *= so
106+ xyz [1 ] *= so
107+ xyz [2 ] *= so
108+
109+ # Apply hard iron ie. offset bias from calibration
110+ xyz [0 ] -= self ._offset [0 ]
111+ xyz [1 ] -= self ._offset [1 ]
112+ xyz [2 ] -= self ._offset [2 ]
113+
114+ # Apply soft iron ie. scale bias from calibration
115+ xyz [0 ] *= self ._scale [0 ]
116+ xyz [1 ] *= self ._scale [1 ]
117+ xyz [2 ] *= self ._scale [2 ]
118+
119+ return tuple (xyz )
120+
121+ @property
122+ def adjustement (self ):
123+ return self ._adjustement
124+
125+ @property
126+ def whoami (self ):
127+ """ Value of the whoami register. """
128+ return self ._register_char (_WIA )
129+
130+ def calibrate (self , count = 256 , delay = 200 ):
131+ self ._offset = (0 , 0 , 0 )
132+ self ._scale = (1 , 1 , 1 )
133+
134+ reading = self .magnetic
135+ minx = maxx = reading [0 ]
136+ miny = maxy = reading [1 ]
137+ minz = maxz = reading [2 ]
138+
139+ while count :
140+ utime .sleep_ms (delay )
141+ reading = self .magnetic
142+ minx = min (minx , reading [0 ])
143+ maxx = max (maxx , reading [0 ])
144+ miny = min (miny , reading [1 ])
145+ maxy = max (maxy , reading [1 ])
146+ minz = min (minz , reading [2 ])
147+ maxz = max (maxz , reading [2 ])
148+ count -= 1
149+
150+ # Hard iron correction
151+ offset_x = (maxx + minx ) / 2
152+ offset_y = (maxy + miny ) / 2
153+ offset_z = (maxz + minz ) / 2
154+
155+ self ._offset = (offset_x , offset_y , offset_z )
156+
157+ # Soft iron correction
158+ avg_delta_x = (maxx - minx ) / 2
159+ avg_delta_y = (maxy - miny ) / 2
160+ avg_delta_z = (maxz - minz ) / 2
161+
162+ avg_delta = (avg_delta_x + avg_delta_y + avg_delta_z ) / 3
163+
164+ scale_x = avg_delta / avg_delta_x
165+ scale_y = avg_delta / avg_delta_y
166+ scale_z = avg_delta / avg_delta_z
167+
168+ self ._scale = (scale_x , scale_y , scale_z )
169+
170+ return self ._offset , self ._scale
171+
172+ def _register_short (self , register , value = None , buf = bytearray (2 )):
173+ if value is None :
174+ self .i2c .readfrom_mem_into (self .address , register , buf )
175+ return ustruct .unpack ("<h" , buf )[0 ]
176+
177+ ustruct .pack_into ("<h" , buf , 0 , value )
178+ return self .i2c .writeto_mem (self .address , register , buf )
179+
180+ def _register_three_shorts (self , register , buf = bytearray (6 )):
181+ self .i2c .readfrom_mem_into (self .address , register , buf )
182+ return ustruct .unpack ("<hhh" , buf )
183+
184+ def _register_char (self , register , value = None , buf = bytearray (1 )):
185+ if value is None :
186+ self .i2c .readfrom_mem_into (self .address , register , buf )
187+ return buf [0 ]
188+
189+ ustruct .pack_into ("<b" , buf , 0 , value )
190+ return self .i2c .writeto_mem (self .address , register , buf )
191+
192+ def __enter__ (self ):
193+ return self
194+
195+ def __exit__ (self , exception_type , exception_value , traceback ):
196+ pass
0 commit comments