AS5600

Форк
0
/
as5600mod.py 
293 строки · 18.0 Кб
1
"""Модуль MicroPython для управления 12-ти битным однооборотным магнитным энкодером AS5600 от AMS OSRAM."""
2
# micropython
3
# mail: goctaprog@gmail.com
4
# MIT license
5
# import struct
6
import time
7
from micropython import const
8
from sensor_pack_2 import bus_service
9
from sensor_pack_2.base_sensor import DeviceEx, Iterator, check_value, all_none
10
import micropython
11
from collections import namedtuple
12
from sensor_pack_2.aliased import get_bf_gen, bitmask
13

14
_mask_12 = bitmask(range(12))
15
_all_round = const(360)
16
raw_per_degrees: float = const((1+_mask_12)/_all_round)
17

18

19
def degrees_to_raw(degrees: int) -> int:
20
    """Преобразует градусы от 0 до 360 в сырые значения для записи в регистры датчика"""
21
    check_value(degrees, range(1+_all_round), f"Значение параметра degrees вне допустимого диапазона: {degrees}!")
22
    val = int(round(raw_per_degrees * degrees))
23
    if val > _mask_12:
24
        return _mask_12
25
    return val
26

27

28
def raw_to_degrees(raw: int) -> float:
29
    """Преобразует сырые значения углов из регистров датчика в градусы от 0 до 360"""
30
    check_value(raw, range(1 + _mask_12), f"Значение параметра raw вне допустимого диапазона: {raw}!")
31
    return raw / raw_per_degrees
32

33

34
# маски для битовых полей регистра CONF
35
#               WatchDog(bool),   fast_filter_threshold(int), slow_filter(int), pwm_freq(int), output_stage(int),
36
#               hysteresis(int), power_mode(int)
37
_conf_masks = range(13, 14), range(10, 13), range(8, 10), range(6, 8), range(4, 6), range(2, 4), range(2)
38

39
# именованный кортеж конфигурации для регистра CONF
40
config_as5600 = namedtuple("config_as5600",
41
                           ("watchdog", "fast_filter_threshold", "slow_filter",
42
                            "pwm_freq", "output_stage", "hysteresis", "power_mode"))
43

44
# Превышение минимального усиления АРУ, слишком сильный магнит (min_gain_ovf)
45
# Превышение максимального усиления АРУ, слишком слабый магнит (max_gain_ovf)
46
# Магнит обнаружен (mag_detected)
47
_status_masks = range(5, 6), range(4, 5), range(3, 4)
48
# именованный кортеж - состояние"""
49
# для метода get_status
50
status_as5600 = namedtuple("status_as5600", "mag_detected max_gain_ovf min_gain_ovf")
51
# для хранения угловых положений: ZPOS, MPOS, MANG
52
angle_positions = namedtuple("angle_positions", "start stop angular_range")
53

54

55
def _check_slow_filter(sf: int):
56
    """Проверяет параметр slow_filter (0..3) на правильность
57
    Соответствие параметр slow_filter и времени интегрирования в мс
58
    0 - 2.2 мс
59
    1 - 1.1 мс
60
    2 - 0.55 мс
61
    3 - 0.286 мс"""
62
    check_value(sf, range(4), f"Неверное значение параметра slow_filter: {sf}")
63

64

65
class AS5600(DeviceEx, Iterator):
66
    """Класс MicroPython для управления 12-ти битным однооборотным магнитным энкодером AS5600 от AMS OSRAM."""
67

68
    def __init__(self, adapter: bus_service.BusAdapter, address=0x36):
69
        """i2c - объект класса I2C; address - адрес датчика на шине. Он не изменяется!"""
70
        super().__init__(adapter, address, True)
71
        self._buf_6 = bytearray((0 for _ in range(6)))  # буфер для угловых положений
72
        # magnet. состояние внешнего магнита энкодера. обновляются методом get_status()
73
        self._mag_detected = self._max_gain_ovf = self._min_gain_ovf = None
74
        # конфигурация/настройки датчика. обновляются методом get_config
75
        # Сторожевой таймер позволяет экономить электроэнергию путем переключения в LMP3, если угол остается в пределах
76
        # сторожевого порога 4 младших разрядов в течение как минимум одной(!) минуты.
77
        self._watchdog = None
78
        # Для быстрого реагирования на скачки и низкого уровня шума после стабилизации можно включить fast filter.
79
        # Он работает только в том случае, если изменение угла превышает(!) порог fast filter, в противном случае
80
        # выходной отклик определяется только slow filter.
81
        self._fast_filter_threshold = None,  # bit 12..10
82
        # Если быстрый фильтр выключен, переходная характеристика выходного (угла) сигнала контролируется
83
        # медленным линейным фильтром.
84
        self._slow_filter = 0,  # bit 9..8,
85
        # частота ШИМ. 00 = 115 Hz; 01 = 230 Hz; 10 = 460 Hz; 11 = 920 Hz
86
        self._pwm_freq = None,  # bit 7..6,
87
        # 0x00 - аналоговый выход (выходное напряжение в пределах 0..100% VDD)
88
        # 0x01 - аналоговый выход (выходное напряжение в пределах 10..90% VDD)
89
        # 0x02 - на выводе микросхемы датчика ШИМ(!)
90
        self._output_stage = None,  # bit 5..4,
91
        # Чтобы избежать любого переключения выхода, когда магнит не движется, можно включить гистерезис от 1 до 3
92
        # младших разрядов 12-битного разрешения.
93
        self._hysteresis = None,  # bit 3..2,
94
        # Цифровой конечный автомат автоматически управляет режимами пониженного энергопотребления, чтобы снизить
95
        # среднее потребление тока. Доступны и могут быть включены три(!) режима пониженного энергопотребления!
96
        # PM0 - датчик всегда(!) включен
97
        # PM1 - период опроса 5 мс
98
        # PM2 - период опроса 20 мс
99
        # PM3 - период опроса 100 мс
100
        self._power_mode = None,  # bit 1..0
101
        #
102
        self.get_status()  # читаю состояние датчика
103
        self.get_config()  # читаю настройки датчика
104

105
    def get_status(self, noreturn: bool = True) -> [status_as5600, None]:
106
        """Возвращает содержимое регистра состояния(STATUS) в виде именованного кортежа (MD, ML, MH)
107
        MH - минимальное усиления АРУ, слишком сильный магнит(min_gain_ovf).
108
        ML - Превышение максимального усиления АРУ, слишком слабый магнит(max_gain_ovf).
109
        МD - Магнит обнаружен.(mag_detected)"""
110
        val = self.read_reg(0x0B, 1)[0]
111
        # return 0 != (val & 0b10_0000), 0 != (val & 0b01_0000), 0 != (val & 0b00_1000)
112
        #               магнит в норме          магнит слишком слабый  магнит слишком сильный
113
        # return status_t(0 != (val & 0b10_0000), 0 != (val & 0b01_0000), 0 != (val & 0b00_1000))
114
        stat = status_as5600(*get_bf_gen(val, _status_masks))
115
        # сохраняю состояние магнита энкодера
116
        self._mag_detected, self._max_gain_ovf, self._min_gain_ovf = stat
117
        if noreturn:
118
            return
119
        return stat
120

121
    @property
122
    def status(self) -> status_as5600:
123
        return self.get_status(noreturn=False)
124

125
    def _get_gain(self) -> int:
126
        """Возвращает содержимое регистра Automatic Gain Control (ACG)"""
127
        return self.read_reg(0x1A, 1)[0]
128

129
    def _get_magnitude(self) -> int:
130
        """Возвращает содержимое регистра MAGNITUDE"""
131
        return _mask_12 & self.unpack(f"H", self.read_reg(0x1B, 2))[0]
132

133
    def get_raw_angle(self, raw: bool = False) -> int:
134
        """Возвращает сырой(raw) угол поворота магнита относительно микросхемы.
135
        Если raw в Истина, то возвращенное значение не масштабировано (сырое)!
136
        Регистр ANGLE имеет гистерезис 10-LSB на пределе диапазона 360 градусов,
137
        чтобы избежать точек разрыва или переключения выхода в течение одного оборота."""
138
        return _mask_12 & self.unpack(f"H", self.read_reg(0x0C if raw else 0x0E, 2))[0]
139

140
    def get_config(self, noreturn: bool = True) -> [config_as5600, None]:
141
        """Возвращает текущие настройки датчика, регистр CONF, в виде именованного кортежа"""
142
        _conf = self._conf()
143
        (self._watchdog, self._fast_filter_threshold, self._slow_filter, self._pwm_freq, self._output_stage,
144
         self._hysteresis, self._power_mode) = _conf
145
        if noreturn:
146
            return
147
        return _conf
148

149
    def get_angle_pos(self, raw: bool = True) -> angle_positions:
150
        """Возвращает сырые(raw is True) или в градусах(raw is False) значения:
151
        начального углового положения(ZPOS/start)
152
        конечного углового положения(MPOS/stop)
153
        размера углового диапазона(MANG/angular_range)"""
154
        buf = self._buf_6
155
        self.read_buf_from_mem(address=0x01, buf=buf)
156
        if raw:
157
            itr = map(lambda x: _mask_12 & x, self.unpack(fmt_char="HHH", source=buf))
158
        else:
159
            itr = map(lambda x: raw_to_degrees(_mask_12 & x), self.unpack(fmt_char="HHH", source=buf))
160
        # print(f"DBG:get_angle_pos {list(itr)}")
161
        return angle_positions(*itr)
162

163
    def set_angle_pos(self, start: int = 0, stop: [int, None] = 360, angular_range: [int, None] = None):
164
        """Записывает в регистры датчика начальное угловое положение(start), конечное угловое положение(stop).
165
        Угловой диапазон вычисляется автоматически!
166
        Задавайте пару start и stop, или пару start и angular_range.
167
        Задавать start, stop и angular_range бессмысленно! Датчик самостоятельно обнулит stop,
168
        если вы зададите angular_range. Вот такие пошли датчики умные!"""
169
        err_str = "Неверное значение параметра"
170
        if stop is None and angular_range is None:
171
            raise ValueError("Один из параметров 'stop' или 'angular_range' должен быть задан!")
172
        rng = range(1 + _all_round)
173
        check_value(start, rng, err_str)
174
        check_value(stop, rng, err_str)
175
        check_value(angular_range, rng, err_str)
176
        # минимальный угловой диапазон у меня 19 градусов. В документации на датчик это значение равно 18 градусам!
177
        if angular_range is not None and angular_range < 20:
178
            raise ValueError("'angular_range' не может быть меньше 20!")
179
        #
180
        # vals = degrees_to_raw(start), degrees_to_raw(stop)      # , degrees_to_raw(angle_range)
181
        self.write_reg(0x01, degrees_to_raw(start), 2)
182
        time.sleep_ms(1)  # ожидаю 1 мс
183
        # Диапазон задается путем программирования начальной позиции (ZPOS) и либо конечной позиции (MPOS),
184
        # либо(!) размера углового диапазона (MANG). То есть, либо пара ZPOS, MPOS, либо пара ZPOS, MANG!
185
        # я выбрал пару ZPOS, MPOS. Если после MPOS записать MANG, то MPOS обнулится!!!
186
        if stop is not None:
187
            self.write_reg(0x03, degrees_to_raw(stop), 2)
188
            return
189
        if angular_range is not None:
190
            self.write_reg(0x05, degrees_to_raw(angular_range), 2)
191

192
    def burn(self, angle_or_settings: bool = True):
193
        """Сохраняет настройки углов(angle_or_settings is True) или настройки(angle_or_settings is False) из
194
        регистров датчика в энергонезависимую память (OTP).
195
        Эту команду (BURN_ANGLE) можно выполнить только при наличии
196
        бита "магнит обнаружен" (MD = 1/status_as5600.mag_detected == True)"""
197
        self.write_reg(0xFF, 0x80 if angle_or_settings else 0x40, 1)
198

199
    def get_counter(self) -> int:
200
        """Возвращает кол-во операций записи в энергонезависимую память. В датчике на это отведено ДВА бита (ZMCO(1:0)),
201
        то есть результат будет в диапазоне 0..3"""
202
        return 0b11 & self.read_reg(0x00, 1)[0]
203

204
    @micropython.native
205
    def get_conversion_cycle_time(self) -> int:
206
        """Возвращает время преобразования в [мкc] датчиком данных цвета. В микросекундах!"""
207
        k = self._slow_filter
208
        _check_slow_filter(k)
209
        return 11 + (2200 // (1 << k))
210

211
    def _conf(self,
212
              watchdog: [bool, None] = None,  # bit 13,
213
              fast_filter_threshold: [int, None] = None,  # bit 12..10
214
              slow_filter: [int, None] = None,  # bit 9..8,
215
              pwm_freq: [int, None] = None,  # bit 7..6,
216
              output_stage: [int, None] = None,  # bit 5..4,
217
              hysteresis: [int, None] = None,  # bit 3..2,
218
              power_mode: [int, None] = None,  # bit 1..0
219
              ) -> config_as5600:
220
        """Регистр CONF. Если все параметры в None, возвращает содержимое регистра"""
221
        val = self.unpack(f"H", self.read_reg(0x07, 2))[0]
222
        if all_none(watchdog, fast_filter_threshold, slow_filter, pwm_freq,
223
                    output_stage, hysteresis, power_mode):
224
            # print(f"DBG _conf before: 0x{val:x}")
225
            return config_as5600(*get_bf_gen(val, _conf_masks))
226
        #
227
        if watchdog is not None:
228
            _mask = _conf_masks[0]
229
            val &= ~bitmask(_mask)  # mask
230
            val |= watchdog << _mask.start
231
        if fast_filter_threshold is not None:
232
            _mask = _conf_masks[1]
233
            val &= ~bitmask(_mask)  # mask
234
            val |= fast_filter_threshold << _mask.start
235
        if slow_filter is not None:
236
            _mask = _conf_masks[2]
237
            val &= ~bitmask(_mask)  # mask
238
            val |= slow_filter << _mask.start
239
        if pwm_freq is not None:
240
            _mask = _conf_masks[3]
241
            val &= ~bitmask(_mask)  # mask
242
            val |= pwm_freq << _mask.start
243
        if output_stage is not None:
244
            _mask = _conf_masks[4]
245
            val &= ~bitmask(_mask)  # mask
246
            val |= output_stage << _mask.start
247
        if hysteresis is not None:
248
            _mask = _conf_masks[5]
249
            val &= ~bitmask(_mask)  # mask
250
            val |= hysteresis << _mask.start
251
        if power_mode is not None:
252
            _mask = _conf_masks[6]
253
            val &= ~bitmask(_mask)  # mask
254
            val |= power_mode << _mask.start
255
        # print(f"DBG _settings after: 0x{val:x}")
256
        self.write_reg(0x07, val, 2)
257

258
    @property
259
    def magnitude(self) -> int:
260
        """Возвращает """
261
        return self._get_magnitude()
262

263
    @property
264
    def gain(self) -> int:
265
        """Возвращает """
266
        return self._get_gain()
267

268
    @property
269
    def angle(self) -> float:
270
        """Возвращает угол поворота магнита относительно корпуса микросхемы"""
271
        return raw_to_degrees(self.get_raw_angle(raw=False))
272

273
    def start_measurement(self, slow_filter: int = 1, fast_filter_threshold: int = 0, watchdog: bool = False,
274
                          pwm_freq: int = 2, output_stage: int = 2, hysteresis: int = 1, power_mode: int = 1):
275
        """Настраивает параметры датчика"""
276
        def get_err_str(val_name: str, val: int, rng: range) -> str:
277
            """Возвращает подробное сообщение об ошибке"""
278
            return f"Значение {val} параметра {val_name} вне диапазона [{rng.start}..{rng.stop-1}]!"
279
        r4 = range(4)
280
        check_value(slow_filter, r4, get_err_str("slow filter", slow_filter, r4))
281
        check_value(fast_filter_threshold, range(8), get_err_str("fast filter threshold",
282
                                                                 fast_filter_threshold, range(8)))
283
        check_value(pwm_freq, r4, get_err_str("pwm freq", pwm_freq, r4))
284
        check_value(output_stage, range(3), get_err_str("output stage", output_stage, range(3)))
285
        check_value(hysteresis, r4, get_err_str("hysteresis", hysteresis, r4))
286
        check_value(power_mode, r4, get_err_str("power_mode", power_mode, r4))
287
        #
288
        self._conf(watchdog, fast_filter_threshold, slow_filter, pwm_freq, output_stage, hysteresis, power_mode)
289

290
    # Iterator
291
    def __next__(self) -> [int, None]:
292
        """Часть протокола итератора"""
293
        return self.angle
294

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.