AS5600
/
as5600mod.py
293 строки · 18.0 Кб
1"""Модуль MicroPython для управления 12-ти битным однооборотным магнитным энкодером AS5600 от AMS OSRAM."""
2# micropython
3# mail: goctaprog@gmail.com
4# MIT license
5# import struct
6import time7from micropython import const8from sensor_pack_2 import bus_service9from sensor_pack_2.base_sensor import DeviceEx, Iterator, check_value, all_none10import micropython11from collections import namedtuple12from sensor_pack_2.aliased import get_bf_gen, bitmask13
14_mask_12 = bitmask(range(12))15_all_round = const(360)16raw_per_degrees: float = const((1+_mask_12)/_all_round)17
18
19def degrees_to_raw(degrees: int) -> int:20"""Преобразует градусы от 0 до 360 в сырые значения для записи в регистры датчика"""21check_value(degrees, range(1+_all_round), f"Значение параметра degrees вне допустимого диапазона: {degrees}!")22val = int(round(raw_per_degrees * degrees))23if val > _mask_12:24return _mask_1225return val26
27
28def raw_to_degrees(raw: int) -> float:29"""Преобразует сырые значения углов из регистров датчика в градусы от 0 до 360"""30check_value(raw, range(1 + _mask_12), f"Значение параметра raw вне допустимого диапазона: {raw}!")31return raw / raw_per_degrees32
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
40config_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
50status_as5600 = namedtuple("status_as5600", "mag_detected max_gain_ovf min_gain_ovf")51# для хранения угловых положений: ZPOS, MPOS, MANG
52angle_positions = namedtuple("angle_positions", "start stop angular_range")53
54
55def _check_slow_filter(sf: int):56"""Проверяет параметр slow_filter (0..3) на правильность57Соответствие параметр slow_filter и времени интегрирования в мс
580 - 2.2 мс
591 - 1.1 мс
602 - 0.55 мс
613 - 0.286 мс"""
62check_value(sf, range(4), f"Неверное значение параметра slow_filter: {sf}")63
64
65class AS5600(DeviceEx, Iterator):66"""Класс MicroPython для управления 12-ти битным однооборотным магнитным энкодером AS5600 от AMS OSRAM."""67
68def __init__(self, adapter: bus_service.BusAdapter, address=0x36):69"""i2c - объект класса I2C; address - адрес датчика на шине. Он не изменяется!"""70super().__init__(adapter, address, True)71self._buf_6 = bytearray((0 for _ in range(6))) # буфер для угловых положений72# magnet. состояние внешнего магнита энкодера. обновляются методом get_status()73self._mag_detected = self._max_gain_ovf = self._min_gain_ovf = None74# конфигурация/настройки датчика. обновляются методом get_config75# Сторожевой таймер позволяет экономить электроэнергию путем переключения в LMP3, если угол остается в пределах76# сторожевого порога 4 младших разрядов в течение как минимум одной(!) минуты.77self._watchdog = None78# Для быстрого реагирования на скачки и низкого уровня шума после стабилизации можно включить fast filter.79# Он работает только в том случае, если изменение угла превышает(!) порог fast filter, в противном случае80# выходной отклик определяется только slow filter.81self._fast_filter_threshold = None, # bit 12..1082# Если быстрый фильтр выключен, переходная характеристика выходного (угла) сигнала контролируется83# медленным линейным фильтром.84self._slow_filter = 0, # bit 9..8,85# частота ШИМ. 00 = 115 Hz; 01 = 230 Hz; 10 = 460 Hz; 11 = 920 Hz86self._pwm_freq = None, # bit 7..6,87# 0x00 - аналоговый выход (выходное напряжение в пределах 0..100% VDD)88# 0x01 - аналоговый выход (выходное напряжение в пределах 10..90% VDD)89# 0x02 - на выводе микросхемы датчика ШИМ(!)90self._output_stage = None, # bit 5..4,91# Чтобы избежать любого переключения выхода, когда магнит не движется, можно включить гистерезис от 1 до 392# младших разрядов 12-битного разрешения.93self._hysteresis = None, # bit 3..2,94# Цифровой конечный автомат автоматически управляет режимами пониженного энергопотребления, чтобы снизить95# среднее потребление тока. Доступны и могут быть включены три(!) режима пониженного энергопотребления!96# PM0 - датчик всегда(!) включен97# PM1 - период опроса 5 мс98# PM2 - период опроса 20 мс99# PM3 - период опроса 100 мс100self._power_mode = None, # bit 1..0101#102self.get_status() # читаю состояние датчика103self.get_config() # читаю настройки датчика104
105def get_status(self, noreturn: bool = True) -> [status_as5600, None]:106"""Возвращает содержимое регистра состояния(STATUS) в виде именованного кортежа (MD, ML, MH)107MH - минимальное усиления АРУ, слишком сильный магнит(min_gain_ovf).
108ML - Превышение максимального усиления АРУ, слишком слабый магнит(max_gain_ovf).
109МD - Магнит обнаружен.(mag_detected)"""
110val = 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))114stat = status_as5600(*get_bf_gen(val, _status_masks))115# сохраняю состояние магнита энкодера116self._mag_detected, self._max_gain_ovf, self._min_gain_ovf = stat117if noreturn:118return119return stat120
121@property122def status(self) -> status_as5600:123return self.get_status(noreturn=False)124
125def _get_gain(self) -> int:126"""Возвращает содержимое регистра Automatic Gain Control (ACG)"""127return self.read_reg(0x1A, 1)[0]128
129def _get_magnitude(self) -> int:130"""Возвращает содержимое регистра MAGNITUDE"""131return _mask_12 & self.unpack(f"H", self.read_reg(0x1B, 2))[0]132
133def get_raw_angle(self, raw: bool = False) -> int:134"""Возвращает сырой(raw) угол поворота магнита относительно микросхемы.135Если raw в Истина, то возвращенное значение не масштабировано (сырое)!
136Регистр ANGLE имеет гистерезис 10-LSB на пределе диапазона 360 градусов,
137чтобы избежать точек разрыва или переключения выхода в течение одного оборота."""
138return _mask_12 & self.unpack(f"H", self.read_reg(0x0C if raw else 0x0E, 2))[0]139
140def 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,144self._hysteresis, self._power_mode) = _conf145if noreturn:146return147return _conf148
149def 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)"""
154buf = self._buf_6155self.read_buf_from_mem(address=0x01, buf=buf)156if raw:157itr = map(lambda x: _mask_12 & x, self.unpack(fmt_char="HHH", source=buf))158else:159itr = 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)}")161return angle_positions(*itr)162
163def 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. Вот такие пошли датчики умные!"""
169err_str = "Неверное значение параметра"170if stop is None and angular_range is None:171raise ValueError("Один из параметров 'stop' или 'angular_range' должен быть задан!")172rng = range(1 + _all_round)173check_value(start, rng, err_str)174check_value(stop, rng, err_str)175check_value(angular_range, rng, err_str)176# минимальный угловой диапазон у меня 19 градусов. В документации на датчик это значение равно 18 градусам!177if angular_range is not None and angular_range < 20:178raise ValueError("'angular_range' не может быть меньше 20!")179#180# vals = degrees_to_raw(start), degrees_to_raw(stop) # , degrees_to_raw(angle_range)181self.write_reg(0x01, degrees_to_raw(start), 2)182time.sleep_ms(1) # ожидаю 1 мс183# Диапазон задается путем программирования начальной позиции (ZPOS) и либо конечной позиции (MPOS),184# либо(!) размера углового диапазона (MANG). То есть, либо пара ZPOS, MPOS, либо пара ZPOS, MANG!185# я выбрал пару ZPOS, MPOS. Если после MPOS записать MANG, то MPOS обнулится!!!186if stop is not None:187self.write_reg(0x03, degrees_to_raw(stop), 2)188return189if angular_range is not None:190self.write_reg(0x05, degrees_to_raw(angular_range), 2)191
192def 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)"""
197self.write_reg(0xFF, 0x80 if angle_or_settings else 0x40, 1)198
199def get_counter(self) -> int:200"""Возвращает кол-во операций записи в энергонезависимую память. В датчике на это отведено ДВА бита (ZMCO(1:0)),201то есть результат будет в диапазоне 0..3"""
202return 0b11 & self.read_reg(0x00, 1)[0]203
204@micropython.native205def get_conversion_cycle_time(self) -> int:206"""Возвращает время преобразования в [мкc] датчиком данных цвета. В микросекундах!"""207k = self._slow_filter208_check_slow_filter(k)209return 11 + (2200 // (1 << k))210
211def _conf(self,212watchdog: [bool, None] = None, # bit 13,213fast_filter_threshold: [int, None] = None, # bit 12..10214slow_filter: [int, None] = None, # bit 9..8,215pwm_freq: [int, None] = None, # bit 7..6,216output_stage: [int, None] = None, # bit 5..4,217hysteresis: [int, None] = None, # bit 3..2,218power_mode: [int, None] = None, # bit 1..0219) -> config_as5600:220"""Регистр CONF. Если все параметры в None, возвращает содержимое регистра"""221val = self.unpack(f"H", self.read_reg(0x07, 2))[0]222if all_none(watchdog, fast_filter_threshold, slow_filter, pwm_freq,223output_stage, hysteresis, power_mode):224# print(f"DBG _conf before: 0x{val:x}")225return config_as5600(*get_bf_gen(val, _conf_masks))226#227if watchdog is not None:228_mask = _conf_masks[0]229val &= ~bitmask(_mask) # mask230val |= watchdog << _mask.start231if fast_filter_threshold is not None:232_mask = _conf_masks[1]233val &= ~bitmask(_mask) # mask234val |= fast_filter_threshold << _mask.start235if slow_filter is not None:236_mask = _conf_masks[2]237val &= ~bitmask(_mask) # mask238val |= slow_filter << _mask.start239if pwm_freq is not None:240_mask = _conf_masks[3]241val &= ~bitmask(_mask) # mask242val |= pwm_freq << _mask.start243if output_stage is not None:244_mask = _conf_masks[4]245val &= ~bitmask(_mask) # mask246val |= output_stage << _mask.start247if hysteresis is not None:248_mask = _conf_masks[5]249val &= ~bitmask(_mask) # mask250val |= hysteresis << _mask.start251if power_mode is not None:252_mask = _conf_masks[6]253val &= ~bitmask(_mask) # mask254val |= power_mode << _mask.start255# print(f"DBG _settings after: 0x{val:x}")256self.write_reg(0x07, val, 2)257
258@property259def magnitude(self) -> int:260"""Возвращает """261return self._get_magnitude()262
263@property264def gain(self) -> int:265"""Возвращает """266return self._get_gain()267
268@property269def angle(self) -> float:270"""Возвращает угол поворота магнита относительно корпуса микросхемы"""271return raw_to_degrees(self.get_raw_angle(raw=False))272
273def start_measurement(self, slow_filter: int = 1, fast_filter_threshold: int = 0, watchdog: bool = False,274pwm_freq: int = 2, output_stage: int = 2, hysteresis: int = 1, power_mode: int = 1):275"""Настраивает параметры датчика"""276def get_err_str(val_name: str, val: int, rng: range) -> str:277"""Возвращает подробное сообщение об ошибке"""278return f"Значение {val} параметра {val_name} вне диапазона [{rng.start}..{rng.stop-1}]!"279r4 = range(4)280check_value(slow_filter, r4, get_err_str("slow filter", slow_filter, r4))281check_value(fast_filter_threshold, range(8), get_err_str("fast filter threshold",282fast_filter_threshold, range(8)))283check_value(pwm_freq, r4, get_err_str("pwm freq", pwm_freq, r4))284check_value(output_stage, range(3), get_err_str("output stage", output_stage, range(3)))285check_value(hysteresis, r4, get_err_str("hysteresis", hysteresis, r4))286check_value(power_mode, r4, get_err_str("power_mode", power_mode, r4))287#288self._conf(watchdog, fast_filter_threshold, slow_filter, pwm_freq, output_stage, hysteresis, power_mode)289
290# Iterator291def __next__(self) -> [int, None]:292"""Часть протокола итератора"""293return self.angle294