Embedded HandbookПриложения
Все разделы › VIII. Электроника вокруг микроконтроллера
STM32F103 · уровень средний · 31 мин чтения
Открыть в интерактивном справочнике

57и. Магнитный энкодер AS5600: угол по I2C и ШИМ, нулевая точка, счёт оборотов и скорость

В 57з скорость двигателя мерил инкрементальный энкодер на TIM2: 400 отсчётов на оборот, счётчик CNT от включения питания. Он знает, на сколько повернулся вал, но не знает, где вал стоит: после сброса ноль потерян, и его ищут концевиком или меткой. Магнитный датчик AS5600 — другой род: под валом микросхема, на валу магнит, и угол в пределах оборота известен сразу после включения, 12 бит.

Что внутри: как датчик Холла видит угол и почему ему не мешает однородное постороннее поле; как подключить AS5600 к STM32F103 и прочитать угол по I2C; как проверить, что магнит на месте и зазор правильный; как взять угол с ШИМ-выхода захватом на таймере, не зная частоты генератора датчика; где поставить ноль — у себя в программе или в регистре ZPOS — и почему прожигать его не нужно; как считать обороты и до какой скорости это верно; откуда скорость по разности углов и чем её фильтровать; что дают гистерезис, смещение магнита с оси и таблица поправок.

Опоры: RM0008 (DocID13902 Rev 17): режим ШИМ-входа, с. 384; поля TS и SMS (101 — TI1FP1, 100 — сброс), с. 407; приём одного и двух байт ведущим I2C, с. 764. DocID13587 Rev 17 (STM32F103x8/xB): PA0 — TIM2_CH1_ETR без допуска 5 В, с. 29; PA8 — TIM1_CH1 с допуском 5 В, с. 31; PB6, PB7 — I2C1_SCL, I2C1_SDA с допуском 5 В, с. 32. Документа на AS5600 в проекте нет. Часть сведений сверена с двумя открытыми проектами на диске: драйвер Zephyr 4.4.2 drivers/sensor/ams/ams_as5600/ams_as5600.c — регистры RAW ANGLE 0x0C, ANGLE 0x0E, STATUS 0x0B, биты MD, ML, MH — 5, 4, 3, 4096 отсчётов на оборот; SimpleFOC 2.4.0 MagneticSensorI2C.cpp — адрес 0x36, угол в 0x0C, 12 бит, маска старшего байта 0x0F; MagneticSensorPWM.h — ШИМ 115, 230, 460 или 920 Гц, рамка 4351 такт, ноль — 128 тактов, наибольшее — 4223. Остальное — ZPOS 0x01, CONF 0x07, AGC 0x1A, MAGNITUDE 0x1B, BURN 0xFF и поля CONF — по документации производителя, здесь не проверено. Все числа ниже — прогон драйвера _verify/mag57j/mag57j.c на компьютере (gcc 12.2.0) со своей моделью AS5600 как ведомого I2C на Python 3.11: магнит, поле, шум, смещение, ШИМ-выход и TIM2 в ней условные; прошивка собрана armclang 6.24 под Cortex-M3.

Как это называется в документации

Оригинал Перевод
contactless magnetic rotary position sensor бесконтактный магнитный датчик угла
diametrically magnetized magnet магнит, намагниченный по диаметру
Hall element датчик Холла
raw angle, angle угол без поправок, угол от нулевой точки
zero position (ZPOS), maximum position (MPOS) нулевая точка, конечная точка
magnet detected (MD), too weak (ML), too strong (MH) магнит есть, поле слабое, поле сильное
automatic gain control (AGC) автоподстройка усиления
hysteresis (HYST) гистерезис выхода
slow filter, fast filter threshold (SF, FTH) медленный фильтр, порог быстрого
output stage (OUTS), PWM frequency (PWMF) вид выхода, частота ШИМ
permanent programming (BURN) прожиг энергонезависимой памяти, необратимо
PWM input mode режим ШИМ-входа таймера
absolute, incremental encoder абсолютный, инкрементальный энкодер
eccentricity, misalignment смещение магнита с оси датчика

57и.1 Принцип: магнит, датчики Холла, 12 бит на оборот

На торце вала — небольшой круглый магнит, намагниченный по диаметру: одна половина — север, другая — юг. Под ним, на оси, — AS5600. Внутри микросхемы датчики Холла меряют поле, перпендикулярное кристаллу, в нескольких точках по кругу. Разность противоположных точек даёт две составляющие X и Y, угол — atan2(Y, X), и это 12 бит: 4096 отсчётов на оборот, 0,0879° на отсчёт.

Слева — вид сбоку: на валу магнит с диаметральной намагниченностью, N и S по разные стороны оси, под ним AS5600 с четырьмя точками Холла по кругу, угол = atan2(Y, X), 4096 отсчётов на оборот; в модели магнит 6 × 2,5 мм, 1,2 Тл, при зазоре 2 мм амплитуда 40,1 мТл, смещение датчика от оси на 1 мм даёт ошибку ±1,563°, две волны за оборот. Справа — подключение к STM32F103 при питании 3,3 В: SDA — PB7 I2C1_SDA, SCL — PB6 I2C1_SCL, подтяжки 4,7 кОм к 3,3 В, OUT — PA0 TIM2_CH1, ШИМ 920 Гц, DIR — на GND. В середине — рамка ШИМ-выхода 4351 такт: «1» — 128 + угол, угол = «1» × 4351 / период − 128, рамка 1,087 мс. Внизу — счёт оборотов: RAW ANGLE по I2C раз в 1 кГц, разность по кругу −2048…+2047, pos += Δ, скорость с фильтром; верно до 30 000 об/мин, правило SimpleFOC с порогом 0,8 оборота — до 12 000 об/мин

Разность точек — не украшение. Однородное постороннее поле (Земли, соседнего двигателя вдалеке) добавляется ко всем точкам одинаково и вычитается. В модели раздела 5 мТл однородного поля сдвинули угол ровно на 0 отсчётов. Неоднородное поле — стальной винт рядом, магнит соседнего вала — так не вычитается; его модель не считает.

Модель: магнит 6 × 2,5 мм с остаточной индукцией 1,2 Тл, четыре точки на окружности радиусом 1 мм, поле — интегралом по магнитным зарядам на поверхности магнита. Пороги состояния и закон AGC в модели условные: документа нет, и настоящие пороги — свои. Как меняется картина с зазором:

Зазор Амплитуда STATUS AGC MAGNITUDE Шум угла
0,5 мм 65,3 мТл MD, MH 0 2137 0,31 отсчёта
1,0 мм 60,3 мТл MD, MH 0 1973 0,33 отсчёта
1,5 мм 50,5 мТл MD 14 1800 0,40 отсчёта
2,0 мм 40,1 мТл MD 53 1800 0,50 отсчёта
3,0 мм 23,5 мТл MD 142 1800 0,85 отсчёта
4,0 мм 13,7 мТл MD 233 1800 1,46 отсчёта
6,0 мм 5,1 мТл MD, ML 255 764 3,93 отсчёта
8,0 мм 2,2 мТл ML 255 331 9,07 отсчёта

Поле падает с зазором быстро: от 1 до 4 мм — в 4,4 раза. Автоподстройка держит MAGNITUDE постоянным, пока хватает усиления, а AGC показывает, сколько его ушло: число растёт с зазором. Шум угла в модели обратно пропорционален полю — при зазоре 6 мм он в 7,9 раза больше, чем при 2 мм.

Чем AS5600 отличается от энкодера из 57з:

Инкрементальный энкодер (57з) AS5600
что знает приращение от включения угол в пределах оборота сразу
ноль искать концевиком или меткой есть всегда, сдвигается программой или ZPOS
разрешение 400 отсчётов на оборот 4096 отсчётов на оборот
где считает CNT таймера, без процессора процессор читает угол сам, обороты считает сам
предел скорости частота фронтов и фильтр входа полоборота за период опроса (57и.5)
механика диск или штрихи магнит на торце вала, без контакта

57и.2 Подключение к F103 и угол по I2C

I2C1 — PB6 (SCL) и PB7 (SDA), оба с допуском 5 В (DocID13587, с. 32), открытый сток, 400 кГц, подтяжки 4,7 кОм к питанию датчика. Адрес — 0x36 (как в SimpleFOC), и он один: два AS5600 на одной шине не поставить — второй нужен на I2C2 или через коммутатор шины. Вывод DIR задаёт, в какую сторону растёт угол, — его соединяют с землёй или с питанием, не оставляют висеть. Вывод OUT — аналоговый или ШИМ-выход (57и.3). Питание — 3,3 или 5 В; как соединять выводы питания для каждого случая, сказано в документе производителя, здесь не проверено.

Угол — два байта, старший первым, значащих 12 бит. Драйвер пишет номер регистра, повторный старт, читает два байта:

/* Двухбайтовый регистр: старший байт первым, значащих бит 12. */
int as_read12(uint8_t reg, uint16_t *v)
{
    uint8_t b[2];
    if (as_port_xfer(AS_ADDR, &reg, 1u, b, 2u))
        return AS_E_BUS;
    *v = (uint16_t)((b[0] & 0x0Fu) << 8 | b[1]);
    return AS_OK;
}

Маска 0x0F — как в SimpleFOC; Zephyr берёт два байта целиком (sys_get_be16), без маски. Порт as_port_xfer в прошивке — I2C1 по RM0008, с. 764: для двух байт POS и ACK ставятся до адреса, для одного ACK снимается до сброса ADDR (17.4). На 400 кГц чтение угла занимает 120,0 мкс шины, один байт — 97,5 мкс; опрос раз в 1 мс берёт 12,0 % шины.

Перевод в градусы — целыми, в сотых долях градуса с округлением: (raw × 36000 + 2048) / 4096.

RAW ANGLE 0 1 2 3 512 1024 2048 3071 4095
сотые градуса 0 9 18 26 4500 9000 18000 26991 35991

Модель поворачивает вал через 30° и читает RAW ANGLE: вал 0° — 2470 отсчётов (217,09°), вал 90° — 3493 (307,00°). Постоянный сдвиг 217° — как магнит приклеен к валу (в модели 37°) плюс 180°, на которые развёрнута диаметральная пара; разброс от расчётного угла — от −1 до 1 отсчёта. Отсюда и нужна нулевая точка (57и.4).

Магнит на месте? Перед работой драйвер читает STATUS, AGC и MAGNITUDE — три обмена, 315,0 мкс:

/* Магнит на месте? STATUS, AGC и MAGNITUDE; ошибка — если MD = 0 или усиление в пределе. */
int as_magnet(as_mag_t *m)
{
    if (as_read8(AS_STATUS, &m->status) || as_read8(AS_AGC, &m->agc) || as_read12(AS_MAGNITUDE, &m->magnitude))
        return AS_E_BUS;
    if (!(m->status & AS_MD))
        return AS_E_NOMAG;
    if (m->status & AS_ML)
        return AS_E_WEAK;
    if (m->status & AS_MH)
        return AS_E_STRONG;
    return AS_OK;
}

Zephyr делает ту же проверку перед каждым чтением угла и при MD = 0, ML или MH угол не обновляет. В модели без магнита — ML без MD, AGC 255, MAGNITUDE 0. Осевой магнит вместо диаметрального при смещении 0,25 мм даёт поле 5,0 мТл, MD и ML, и, главное, угол на повороте вала на 360° ходит всего в пределах 18 отсчётов: поле осевого магнита от поворота не меняется, датчик видит шум и смещение, а не вал. В прошивке, пока as_magnet не вернёт AS_OK, горит светодиод платы.


57и.3 ШИМ-выход: угол захватом на таймере

Если провода I2C длинные или шина занята, угол берут с вывода OUT в режиме ШИМ. Рамка — 4351 такт генератора датчика: 128 тактов «1», затем угол — 0…4095 тактов «1», остаток — «0», последние 128 тактов — всегда «0». Частота рамки — 115, 230, 460 или 920 Гц по полю PWMF; генератор у датчика свой, и точной частоты прошивка не знает.

Отсюда способ: мерить и период, и длительность «1», а угол брать из их отношения — частота сокращается. TIM2 в режиме ШИМ-входа делает оба замера сам (RM0008, с. 384): канал 1 ловит фронт и сбрасывает счётчик, канал 2 — спад:

static void pwm_in_init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x4u;           /* PA0 — плавающий вход: выход датчика двухтактный */
    TIM2->PSC = 1u;
    TIM2->ARR = 0xFFFFu;
    TIM2->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_1;  /* CC1S = 01: TI1; CC2S = 10: тоже TI1 */
    TIM2->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC2P;   /* IC2 — по спаду */
    TIM2->SMCR = TIM_SMCR_TS_2 | TIM_SMCR_TS_0          /* TS = 101: TI1FP1 */
               | TIM_SMCR_SMS_2;                        /* SMS = 100: сброс счётчика по фронту */
    TIM2->DIER = TIM_DIER_CC1IE;
    TIM2->EGR = TIM_EGR_UG;
    TIM2->CR1 = TIM_CR1_CEN;
    NVIC_EnableIRQ(TIM2_IRQn);
}

Перевод в угол — целыми, с округлением, и с пределами 0 и 4095:

uint16_t as_pwm_raw(uint32_t period, uint32_t high)
{
    if (period == 0u)
        return 0u;
    uint32_t clk = (uint32_t)(((uint64_t)high * AS_PWM_FRAME + period / 2u) / period);
    if (clk <= AS_PWM_HEAD)
        return 0u;
    clk -= AS_PWM_HEAD;
    return (uint16_t)(clk > AS_CPR - 1 ? AS_CPR - 1 : clk);
}

PSC подбирают так, чтобы период уложился в 16 бит CCR1 и на такт рамки пришлось несколько тиков. Модель берёт 52 угла через 7°, случайную фазу тиков, и сравнивает с ANGLE датчика; «уход» — генератор датчика быстрее на 3 %:

ШИМ PSC Рамка Тиков на такт Ошибка Уход 3 %: ошибка Уход 3 %, период принят за известный
115 Гц 15 8,696 мс 8,99 0 0 122 отсчёта
230 Гц 7 4,348 мс 8,99 0 0 122 отсчёта
460 Гц 3 2,174 мс 8,99 0 0 122 отсчёта
920 Гц 1 1,087 мс 8,99 0 0 122 отсчёта

Девяти тиков на такт хватает, чтобы ни один угол не разошёлся с ANGLE. Если же мерить только длительность «1» и делить на расчётный такт, уход генератора на 3 % даёт до 122 отсчётов ошибки — 10,7°. Так устроен MagneticSensorPWM в SimpleFOC: длительность импульса в микросекундах и частота, заданная числом при создании. На краях: 128 тактов «1» и меньше — 0, 129 — 1, 4223 и больше — 4095.

Цена ШИМ-выхода — скорость обновления: одно значение за рамку, 1,087 мс при 920 Гц и 8,696 мс при 115. Прошивка ставит 920 Гц и PSC = 1. Выход датчика — его питания: при 3,3 В — на PA0 (TIM2_CH1, без допуска 5 В, DocID13587, с. 29), при 5 В — на PA8 (TIM1_CH1, с допуском 5 В, с. 31) или через делитель.


57и.4 Нулевая точка: программно или через ZPOS, без прожига

Ноль ставят двумя способами. Программно: прошивка помнит показание RAW ANGLE в нулевой точке и вычитает его: (raw − zero) & 4095. В датчике: записать это показание в ZPOS, и регистр ANGLE (0x0E) сам отдаёт угол от нуля, и ШИМ-выход тоже.

/* Ноль там, где вал стоит сейчас: ZPOS = RAW ANGLE. Только запись в регистр — без BURN:
 * после выключения питания ZPOS снова 0, и прошивка пишет его при каждом старте. */
int as_set_zpos(uint16_t *zpos)
{
    uint16_t raw, back;
    if (as_read12(AS_RAW_ANGLE, &raw) || as_write12(AS_ZPOS, raw) || as_read12(AS_ZPOS, &back))
        return AS_E_BUS;
    *zpos = back;
    return back == raw ? AS_OK : AS_E_BUS;
}

Прогон: вал на 100°, RAW ANGLE 3608, as_set_zpos записала 3606 (шум между чтениями), ANGLE пять раз подряд — 1, 1, 0, 0, 0. Вал на 190°: ANGLE 1024 — ровно 90,00°, программный ноль на том же чтении — 1023. Оба способа дают одно и то же; ZPOS нужен, когда угол читают с ШИМ-выхода или когда вычитать некому.

После выключения питания ZPOS в модели снова 0, и ANGLE равен RAW ANGLE (534 и 535). Чтобы ноль пережил выключение, в документе есть команда BURN (запись в 0xFF): она прожигает ZPOS и MPOS в энергонезависимую память. Это необратимо и разрешено ограниченное число раз — по документации производителя. Делать этого не нужно: ноль из своей памяти прошивка пишет в ZPOS при каждом старте (в fw57j.c — ZERO_RAW), и испорченный прожиг невозможен. В драйвере функции прожига нет, и за весь прогон модель не получила ни одной записи в 0xFF.


57и.5 Обороты и скорость

AS5600 знает угол в пределах оборота; обороты считает прошивка. Между двумя опросами она берёт разность углов по кругу — в пределах −2048…+2047 — и прибавляет к полному углу:

/* Разность двух углов по кругу: −2048…+2047. */
int16_t as_wrap(int32_t d)
{
    d &= AS_CPR - 1;
    return (int16_t)(d >= AS_CPR / 2 ? d - AS_CPR : d);
}
/* Развёртка: верно, пока за период опроса вал проходит меньше половины оборота. */
int32_t as_turns_step(as_turns_t *t, uint16_t raw)
{
    int16_t d = as_wrap((int32_t)raw - t->last);
    t->last = raw;
    t->pos += d;
    return d;
}

Предел скорости опроса. Разность по кругу не отличает поворот на Δ от поворота на Δ − 4096: она выбирает меньший по модулю. Значит, счёт верен, пока за период опроса вал проходит меньше половины оборота: при частоте опроса f — до 30·f об/мин. При 1 кГц — 30 000 об/мин, при 100 Гц — 3000. Прогон — 1 с вращения, драйвер читает RAW ANGLE по I2C каждый тик:

Опрос Вал Шаг за опрос Должно быть Насчитано
1 кГц 600 об/мин 41,0 отсчёта 10,00 об 10,00 об
1 кГц −6000 об/мин −409,6 отсчёта −100,00 об −100,00 об
1 кГц 29 000 об/мин 1979,7 отсчёта 483,33 об 483,33 об
1 кГц 29 900 об/мин 2041,2 отсчёта 498,33 об 498,33 об
1 кГц 30 100 об/мин 2054,8 отсчёта 501,67 об −498,33 об
1 кГц 31 000 об/мин 2116,3 отсчёта 516,67 об −483,33 об
100 Гц 2900 об/мин 1979,7 отсчёта 48,33 об 48,33 об
100 Гц 3100 об/мин 2116,3 отсчёта 51,67 об −48,33 об

За пределом счёт не сбивается на чуть-чуть — он идёт назад: каждый шаг больше 2048 отсчётов разность принимает за шаг в обратную сторону. Модель не учитывает, что сам датчик на таких скоростях отстаёт своим фильтром, — это ещё одна причина держать запас. SimpleFOC (Sensor::update) считает переход через ноль, если скачок больше 0,8 оборота; тот же разбор даёт предел 0,2 оборота за опрос: при 1 кГц — 11 900 об/мин счёт верен (198,33 об), 12 100 — нет (0,67 об вместо 201,67).

Полный угол pos — int32_t в отсчётах: его хватает на 524 288 оборотов, на 6000 об/мин — на 87 минут; дальше — переполнение, и долго крутящемуся валу нужен 64-битный счёт или сброс. DIR, поднятый к питанию, меняет знак: 600 об/мин насчитаны как −10,00 об.

Скорость — разность за период опроса, в отсчётах в секунду, с фильтром первого порядка:

/* Скорость из приращения за период опроса, фильтр первого порядка; отсчётов/с. */
int32_t as_speed_step(as_speed_t *s, int32_t delta)
{
    int32_t x = delta * (int32_t)s->hz * 256;
    s->y += (x - s->y) / (1 << s->k);
    return s->y / 256;
}

Без фильтра ступень скорости — один отсчёт за 1 мс: 14,65 об/мин против 150 у энкодера на 400 отсчётов в 57з. Но у AS5600 шумит каждый отсчёт, и этот шум делится на период опроса. Модель: зазор 2 мм (шум 0,5 отсчёта), опрос 1 кГц; вал стоит, затем на 0,1 с скорость скачком 600 об/мин; «разброс» — от наименьшего до наибольшего за последние 0,5 с:

Фильтр k Стоит: разброс 600 об/мин: среднее 600 об/мин: разброс До 90 %
0 (без фильтра) 73,2 об/мин 600,0 73,2 об/мин 1 мс
3 6,0 об/мин 600,0 6,7 об/мин 18 мс
5 1,5 об/мин 600,0 1,7 об/мин 73 мс
7 0,3 об/мин 593,5 25,4 об/мин 294 мс

Каждый шаг k вдвое удлиняет фильтр: шум падает, запаздывание растёт. При k = 7 за 0,4 с после скачка скорость ещё не дошла до 600 — отсюда и среднее 593,5, и большой разброс. В прошивке k = 5.


57и.6 Шум, гистерезис, смещение магнита и таблица поправок

Шум. При зазоре 2 мм модель шумит с с. к. о. 0,497 отсчёта — 0,0437°. Неподвижный вал между двумя отсчётами даёт RAW ANGLE, который прыгает. Гистерезис HYST в CONF (по документации — 0…3 отсчёта; модель повторяет это, а не проверяет) держит ANGLE и ШИМ-выход на месте, пока новый угол не уйдёт дальше порога. Вал стоит ровно посередине между двумя отсчётами, 1000 чтений:

HYST ANGLE: размах ANGLE: смен значения RAW ANGLE: размах
0 3 559 3
1 3 63 3
2 0 0 3
3 0 0 3

RAW ANGLE гистерезиса не знает — он для счёта оборотов и скорости. Цена гистерезиса — мёртвая зона: поворот меньше порога выход не покажет. Фильтры датчика SF и FTH (медленный фильтр и порог, после которого включается быстрый) модель не повторяет: их времена — в документе производителя.

Смещение магнита с оси. Если ось вала проходит мимо центра датчика, поле в точках Холла меняется уже не только поворотом. В модели смещение датчика от оси вала даёт ошибку, похожую на синусоиду с двумя периодами за оборот (первой и третьей гармоник в ней нет):

Зазор Смещение Ошибка, наибольшая
2 мм 0 0,000°
2 мм 0,25 мм 0,075°
2 мм 0,5 мм 0,317°
2 мм 1,0 мм 1,563°
3 мм 1,0 мм 2,370°

Ошибка растёт быстрее смещения: вдвое больше смещение — в 4,2…4,9 раза больше ошибка. При 1 мм это ±17,8 отсчёта: разрешение в один отсчёт съедает механика. Магнит, приклеенный со смещением на самом валу, даёт ошибку другой формы; это модель не считала.

Таблица поправок. Если механику не исправить, ошибку снимают таблицей: вал ставят по эталону (шаговый двигатель, делительная головка) в 32 точки через 11,25°, в каждой — среднее 16 чтений, и as_cal_build записывает постоянный сдвиг и форму ошибки. Поправка — линейно между узлами, узел ищется дважды:

/* Показание → угол вала: снять сдвиг, затем форму; узел ищется дважды — по грубому и по уточнённому углу. */
uint16_t as_cal_apply(const as_cal_t *c, uint16_t raw)
{
    int32_t x = (int32_t)raw - c->mean;
    int32_t y = x - cal_at(c, x);
    y = x - cal_at(c, y);
    return (uint16_t)(y & (AS_CPR - 1));
}

Проверка — 360 углов через 1°, вне узлов таблицы:

Смещение Шум Поправки в таблице До таблицы После
0,5 мм есть −4…4 отсчёта 0,427° 0,237°
1,0 мм есть −18…18 отсчётов 1,692° 0,237°
1,0 мм нет −18…18 отсчётов 1,601° 0,212°

Таблица заодно снимает постоянный сдвиг: mean = −1627 отсчётов, это те же 217°. Остаток 0,212° без шума — 2,4 отсчёта: округления до целого отсчёта в показании, в узлах таблицы и в интерполяции складываются. С шумом добавляется одно чтение без усреднения. При смещении 1 мм таблица уменьшила ошибку в 7,1 раза, при 0,5 мм — в 1,8: там она уже близка к своему остатку.


57и.7 Грабли

Угол стоит на месте или ходит на десяток отсчётов, хотя вал крутится. Магнит намагничен по оси, а не по диаметру. Поле осевого магнита от поворота не меняется: в модели 18 отсчётов размаха на полный оборот и MD вместе с ML. Нужен магнит с диаметральной намагниченностью.

MH или ML в STATUS, угол шумит. Зазор не тот: в модели при 0,5–1 мм — MH, при 6 мм — ML и шум 3,93 отсчёта против 0,50 при 2 мм. Подбирают зазор по STATUS и AGC, а не на глаз.

Ошибка угла дважды за оборот. Датчик не на оси вала: 1 мм смещения — ±1,563° в модели. Центровка или таблица поправок.

Угол уводит рядом со сталью. Однородное поле датчик вычитает (5 мТл — 0 отсчётов в модели), но стальной винт, шайба или подшипник рядом с магнитом искажают само поле магнита неоднородно. Крепёж у магнита — из немагнитных материалов.

Счёт оборотов пошёл назад на высокой скорости. Вал проходит за период опроса больше половины оборота: 30 100 об/мин при опросе 1 кГц насчитаны как −498,33 оборота за секунду. Опрашивать чаще или ограничить скорость.

Направление меняется само. DIR висит в воздухе. Его соединяют с землёй или с питанием; с поднятым DIR 600 об/мин считаются как −10 оборотов в секунду.

После выключения ноль пропал. ZPOS записан, но не прожжён — так и должно быть: прошивка пишет его при каждом старте. Прожигать BURN ради этого не нужно, это необратимо.

Угол с ШИМ-выхода уходит на градусы. Мерят только длительность «1» и делят на расчётный такт: уход генератора датчика на 3 % — до 122 отсчётов. Мерить и период, и длительность — ошибки нет.

ШИМ-выход при питании 5 В на PA0. У PA0 нет допуска 5 В (DocID13587, с. 29): выход датчика от 5 В — на PA8 с допуском (с. 31) или через делитель. I2C на PB6, PB7 допуск имеет (с. 32), но подтяжки — к питанию датчика.

Длинный I2C сбоит. Ёмкость провода затягивает фронты, и шина ловит наводки двигателя. Короткий провод, подтяжки ближе к номиналу, меньшая частота шины или ШИМ-выход; залипшую шину оживляют, как в 17.6.

Второй AS5600 на ту же шину не отвечает. Адрес 0x36 у них одинаковый: вторая шина или коммутатор.


57и.8 Итоги

Полные исходники

mag57j.h — адрес, регистры и биты AS5600, типы и прототипы драйвера.

/* mag57j.h — раздел 57и: магнитный энкодер AS5600, без регистров микроконтроллера.
 * Адрес, регистры и биты — по документации производителя (в проекте её нет, не проверено).
 * Обмен по I2C — через порт as_port_xfer: в прошивке fw57j.c это I2C1, на стенде host57j.c — модель.
 * Код один для прошивки и стенда. */
#ifndef MAG57J_H
#define MAG57J_H
#include <stdint.h>

#define AS_ADDR      0x36u      /* 7-битный адрес */
#define AS_ZPOS      0x01u      /* нулевая точка, 12 бит, старший байт первым */
#define AS_CONF      0x07u      /* настройка: HYST, OUTS, PWMF, SF, FTH */
#define AS_STATUS    0x0Bu      /* MD, ML, MH */
#define AS_RAW_ANGLE 0x0Cu      /* угол без нуля и без гистерезиса */
#define AS_ANGLE     0x0Eu      /* угол от ZPOS, после гистерезиса */
#define AS_AGC       0x1Au      /* усиление автоподстройки */
#define AS_MAGNITUDE 0x1Bu      /* амплитуда сигнала после усиления */

#define AS_MD        0x20u      /* магнит есть */
#define AS_ML        0x10u      /* поле слабое: усиление в верхнем пределе */
#define AS_MH        0x08u      /* поле сильное: усиление в нижнем пределе */

#define AS_CPR       4096       /* отсчётов на оборот: 12 бит */
#define AS_PWM_FRAME 4351u      /* такт ШИМ-выхода: 128 «1» + 4095 угла + 128 «0» */
#define AS_PWM_HEAD  128u

#define AS_OK        0
#define AS_E_BUS     (-1)       /* нет ответа по I2C */
#define AS_E_NOMAG   (-2)       /* MD = 0 */
#define AS_E_WEAK    (-3)       /* ML = 1 */
#define AS_E_STRONG  (-4)       /* MH = 1 */

#define AS_CAL_N     32         /* точек таблицы поправок на оборот */

int as_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx);

typedef struct {
    uint8_t status, agc;
    uint16_t magnitude;
} as_mag_t;

typedef struct {
    uint16_t last;              /* прошлое показание 0…4095 */
    int32_t pos;                /* полный угол в отсчётах: обороты × 4096 + доля */
} as_turns_t;

typedef struct {
    int32_t y;                  /* скорость в отсчётах/с, Q8 */
    uint8_t k;                  /* фильтр: y += (x − y) / 2^k */
    uint16_t hz;                /* частота опроса */
} as_speed_t;

typedef struct {
    int16_t corr[AS_CAL_N];     /* поправка в отсчётах в точках i × 4096 / AS_CAL_N */
    int16_t mean;               /* средняя поправка: постоянный сдвиг */
} as_cal_t;

int as_read8(uint8_t reg, uint8_t *v);
int as_read12(uint8_t reg, uint16_t *v);
int as_write12(uint8_t reg, uint16_t v);
int as_magnet(as_mag_t *m);
int as_set_zpos(uint16_t *zpos);
int32_t as_cdeg(uint16_t raw);
uint16_t as_pwm_raw(uint32_t period, uint32_t high);
int16_t as_wrap(int32_t d);
void as_turns_init(as_turns_t *t, uint16_t raw);
int32_t as_turns_step(as_turns_t *t, uint16_t raw);
void as_speed_init(as_speed_t *s, uint8_t k, uint16_t hz);
int32_t as_speed_step(as_speed_t *s, int32_t delta);
int as_read_avg(uint8_t reg, uint16_t n, uint16_t *avg);
void as_cal_build(as_cal_t *c, const uint16_t *meas);
uint16_t as_cal_apply(const as_cal_t *c, uint16_t raw);

#endif

mag57j.c — чтение и запись регистров, проверка магнита, ноль, ШИМ, обороты, скорость и таблица поправок — общий для прошивки и стенда.

/* mag57j.c — раздел 57и: AS5600 — угол, магнит, ноль, обороты, скорость и таблица поправок. */
#include "mag57j.h"

/* Регистр в один байт: указатель, повторный старт, чтение. */
int as_read8(uint8_t reg, uint8_t *v)
{
    return as_port_xfer(AS_ADDR, &reg, 1u, v, 1u) ? AS_E_BUS : AS_OK;
}

/* Двухбайтовый регистр: старший байт первым, значащих бит 12. */
int as_read12(uint8_t reg, uint16_t *v)
{
    uint8_t b[2];
    if (as_port_xfer(AS_ADDR, &reg, 1u, b, 2u))
        return AS_E_BUS;
    *v = (uint16_t)((b[0] & 0x0Fu) << 8 | b[1]);
    return AS_OK;
}

int as_write12(uint8_t reg, uint16_t v)
{
    uint8_t b[3] = {reg, (uint8_t)(v >> 8 & 0x0Fu), (uint8_t)v};
    return as_port_xfer(AS_ADDR, b, 3u, 0, 0u) ? AS_E_BUS : AS_OK;
}

/* Магнит на месте? STATUS, AGC и MAGNITUDE; ошибка — если MD = 0 или усиление в пределе. */
int as_magnet(as_mag_t *m)
{
    if (as_read8(AS_STATUS, &m->status) || as_read8(AS_AGC, &m->agc) || as_read12(AS_MAGNITUDE, &m->magnitude))
        return AS_E_BUS;
    if (!(m->status & AS_MD))
        return AS_E_NOMAG;
    if (m->status & AS_ML)
        return AS_E_WEAK;
    if (m->status & AS_MH)
        return AS_E_STRONG;
    return AS_OK;
}

/* Ноль там, где вал стоит сейчас: ZPOS = RAW ANGLE. Только запись в регистр — без BURN:
 * после выключения питания ZPOS снова 0, и прошивка пишет его при каждом старте. */
int as_set_zpos(uint16_t *zpos)
{
    uint16_t raw, back;
    if (as_read12(AS_RAW_ANGLE, &raw) || as_write12(AS_ZPOS, raw) || as_read12(AS_ZPOS, &back))
        return AS_E_BUS;
    *zpos = back;
    return back == raw ? AS_OK : AS_E_BUS;
}

/* Отсчёты в сотые доли градуса с округлением: 4096 → 36000. */
int32_t as_cdeg(uint16_t raw)
{
    return (int32_t)(((uint32_t)raw * 36000u + AS_CPR / 2) / AS_CPR);
}

/* ШИМ-выход: период и длительность «1» в тиках таймера → угол. Рамка — 4351 такт, из них
 * 128 тактов «1» в начале; частота генератора датчика не нужна — берётся отношение. */
uint16_t as_pwm_raw(uint32_t period, uint32_t high)
{
    if (period == 0u)
        return 0u;
    uint32_t clk = (uint32_t)(((uint64_t)high * AS_PWM_FRAME + period / 2u) / period);
    if (clk <= AS_PWM_HEAD)
        return 0u;
    clk -= AS_PWM_HEAD;
    return (uint16_t)(clk > AS_CPR - 1 ? AS_CPR - 1 : clk);
}

/* Разность двух углов по кругу: −2048…+2047. */
int16_t as_wrap(int32_t d)
{
    d &= AS_CPR - 1;
    return (int16_t)(d >= AS_CPR / 2 ? d - AS_CPR : d);
}

void as_turns_init(as_turns_t *t, uint16_t raw)
{
    t->last = raw;
    t->pos = raw;
}

/* Развёртка: верно, пока за период опроса вал проходит меньше половины оборота. */
int32_t as_turns_step(as_turns_t *t, uint16_t raw)
{
    int16_t d = as_wrap((int32_t)raw - t->last);
    t->last = raw;
    t->pos += d;
    return d;
}

void as_speed_init(as_speed_t *s, uint8_t k, uint16_t hz)
{
    s->y = 0;
    s->k = k;
    s->hz = hz;
}

/* Скорость из приращения за период опроса, фильтр первого порядка; отсчётов/с. */
int32_t as_speed_step(as_speed_t *s, int32_t delta)
{
    int32_t x = delta * (int32_t)s->hz * 256;
    s->y += (x - s->y) / (1 << s->k);
    return s->y / 256;
}

/* Среднее n показаний по кругу: каждое — разностью с первым, без скачка через 0/4095. */
int as_read_avg(uint8_t reg, uint16_t n, uint16_t *avg)
{
    uint16_t first, v;
    int32_t sum = 0;
    if (as_read12(reg, &first))
        return AS_E_BUS;
    for (uint16_t i = 1; i < n; i++) {
        if (as_read12(reg, &v))
            return AS_E_BUS;
        sum += as_wrap((int32_t)v - first);
    }
    sum += sum >= 0 ? (int32_t)n / 2 : -(int32_t)n / 2;
    *avg = (uint16_t)(((int32_t)first + sum / (int32_t)(n ? n : 1u)) & (AS_CPR - 1));
    return AS_OK;
}

/* Таблица по эталону: meas[i] — показание, когда вал стоит в точке i × 4096 / AS_CAL_N.
 * Постоянный сдвиг — отдельно (mean), в таблице — только его форма вокруг нуля. */
void as_cal_build(as_cal_t *c, const uint16_t *meas)
{
    int32_t d0 = as_wrap((int32_t)meas[0]), sum = 0;
    for (int i = 0; i < AS_CAL_N; i++) {
        int32_t d = as_wrap((int32_t)meas[i] - i * (AS_CPR / AS_CAL_N));
        c->corr[i] = (int16_t)as_wrap(d - d0);
        sum += c->corr[i];
    }
    int32_t avg = (sum + (sum >= 0 ? AS_CAL_N / 2 : -AS_CAL_N / 2)) / AS_CAL_N;
    for (int i = 0; i < AS_CAL_N; i++)
        c->corr[i] = (int16_t)(c->corr[i] - avg);
    c->mean = as_wrap(d0 + avg);
}

/* Поправка формы в точке x (угол вала, 0…4095): линейно между соседними узлами. */
static int32_t cal_at(const as_cal_t *c, int32_t x)
{
    x &= AS_CPR - 1;
    int32_t i = x / (AS_CPR / AS_CAL_N), f = x % (AS_CPR / AS_CAL_N);
    int32_t a = c->corr[i], b = c->corr[(i + 1) % AS_CAL_N];
    return a + ((b - a) * f + (AS_CPR / AS_CAL_N) / 2) / (AS_CPR / AS_CAL_N);
}

/* Показание → угол вала: снять сдвиг, затем форму; узел ищется дважды — по грубому и по уточнённому углу. */
uint16_t as_cal_apply(const as_cal_t *c, uint16_t raw)
{
    int32_t x = (int32_t)raw - c->mean;
    int32_t y = x - cal_at(c, x);
    y = x - cal_at(c, y);
    return (uint16_t)(y & (AS_CPR - 1));
}

fw57j.c — прошивка STM32F103: I2C1 — AS5600, TIM2 — ШИМ-вход с вывода OUT, опрос угла раз в 1 мс.

/* fw57j.c — раздел 57и: AS5600 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц. Датчик питается от 3,3 В.
 * I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 400 кГц, подтяжки 4,7 кОм к 3,3 В). DIR датчика — на GND.
 * OUT датчика в режиме ШИМ 920 Гц — на PA0: TIM2 в режиме ШИМ-входа, CCR1 — период, CCR2 — длительность «1».
 * Раз в 1 мс — RAW ANGLE по I2C, счёт оборотов и скорость (mag57j.c). Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "mag57j.h"

#define CPU_HZ          72000000u
#define APB1_MHZ        36u
#define POLL_HZ         1000u           /* опрос 1 кГц: развёртка верна до 30 000 об/мин */
#define I2C_TIMEOUT_US  1000u
#define ZERO_RAW        0u              /* ноль вала: RAW ANGLE в нулевой точке, хранится в прошивке */
/* CONF: HYST = 01 (1 отсчёт), OUTS = 10 (ШИМ), PWMF = 11 (920 Гц) — по документации производителя */
#define AS_CONF_VALUE   (1u << 2 | 2u << 4 | 3u << 6)

static as_turns_t turns;
static as_speed_t speed;
static volatile uint8_t tick;
volatile int32_t pos, cps;              /* полный угол в отсчётах, скорость в отсчётах/с */
volatile uint16_t pwm_angle;            /* угол с ШИМ-выхода */
volatile uint32_t bus_errors, mag_rc;

static int expired(uint32_t t0, uint32_t us)
{
    return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}

static void i2c1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;    /* PB6, PB7 — AF, открытый сток (1111) */
    I2C1->CR1 = 0;
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = I2C_CCR_FS | 30u;                       /* 36 МГц / (3 × 30) = 400 кГц */
    I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u;
    I2C1->CR1 = I2C_CR1_PE;
}

static int sr1_wait(uint16_t mask)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!(I2C1->SR1 & mask)) {
        if (I2C1->SR1 & I2C_SR1_AF)
            return AS_E_BUS;
        if (expired(t0, I2C_TIMEOUT_US))
            return AS_E_BUS;
    }
    return AS_OK;
}

static int fail(void)
{
    I2C1->CR1 |= I2C_CR1_STOP;
    I2C1->SR1 = (uint16_t)~I2C_SR1_AF;
    I2C1->CR1 &= (uint16_t)~(I2C_CR1_POS | I2C_CR1_ACK);
    return AS_E_BUS;
}

/* Запись ntx байт; затем, если nrx = 1 или 2, повторный старт и чтение (RM0008, с. 764). */
int as_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx)
{
    uint32_t t0 = DWT->CYCCNT;
    if (nrx > 2u || ntx == 0u)
        return AS_E_BUS;
    while (I2C1->SR2 & I2C_SR2_BUSY) {
        if (expired(t0, I2C_TIMEOUT_US))
            return AS_E_BUS;
    }
    I2C1->CR1 |= I2C_CR1_START;
    if (sr1_wait(I2C_SR1_SB))
        return fail();
    I2C1->DR = (uint8_t)(addr7 << 1);
    if (sr1_wait(I2C_SR1_ADDR))
        return fail();
    (void)I2C1->SR2;
    for (uint32_t i = 0; i < ntx; i++) {
        if (sr1_wait(I2C_SR1_TXE))
            return fail();
        I2C1->DR = tx[i];
    }
    if (sr1_wait(I2C_SR1_BTF))
        return fail();
    if (!nrx) {
        I2C1->CR1 |= I2C_CR1_STOP;
        return AS_OK;
    }
    I2C1->CR1 |= I2C_CR1_START;
    if (sr1_wait(I2C_SR1_SB))
        return fail();
    if (nrx == 2u)
        I2C1->CR1 |= I2C_CR1_POS | I2C_CR1_ACK;         /* два байта: POS и ACK до адреса */
    I2C1->DR = (uint8_t)(addr7 << 1 | 1u);
    if (sr1_wait(I2C_SR1_ADDR))
        return fail();
    if (nrx == 1u) {
        I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;            /* один байт: ACK снять до сброса ADDR */
        (void)I2C1->SR2;
        I2C1->CR1 |= I2C_CR1_STOP;
        if (sr1_wait(I2C_SR1_RXNE))
            return fail();
        rx[0] = (uint8_t)I2C1->DR;
        return AS_OK;
    }
    (void)I2C1->SR2;
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    if (sr1_wait(I2C_SR1_BTF))
        return fail();
    I2C1->CR1 |= I2C_CR1_STOP;
    rx[0] = (uint8_t)I2C1->DR;                          /* старший байт */
    rx[1] = (uint8_t)I2C1->DR;
    I2C1->CR1 &= (uint16_t)~I2C_CR1_POS;
    return AS_OK;
}

/* TIM2 — ШИМ-вход на PA0 (RM0008, с. 384): IC1 — нарастающий фронт и сброс счётчика, IC2 — спадающий.
 * 920 Гц — 1,087 мс; PSC = 1 (36 МГц): период около 39 130 тиков, 9 тиков на такт рамки. */
static void pwm_in_init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x4u;           /* PA0 — плавающий вход: выход датчика двухтактный */
    TIM2->PSC = 1u;
    TIM2->ARR = 0xFFFFu;
    TIM2->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_1;  /* CC1S = 01: TI1; CC2S = 10: тоже TI1 */
    TIM2->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC2P;   /* IC2 — по спаду */
    TIM2->SMCR = TIM_SMCR_TS_2 | TIM_SMCR_TS_0          /* TS = 101: TI1FP1 */
               | TIM_SMCR_SMS_2;                        /* SMS = 100: сброс счётчика по фронту */
    TIM2->DIER = TIM_DIER_CC1IE;
    TIM2->EGR = TIM_EGR_UG;
    TIM2->CR1 = TIM_CR1_CEN;
    NVIC_EnableIRQ(TIM2_IRQn);
}

void TIM2_IRQHandler(void)
{
    uint32_t period = TIM2->CCR1, high = TIM2->CCR2;    /* чтение CCR1 снимает CC1IF */
    pwm_angle = as_pwm_raw(period, high);
}

void SysTick_Handler(void)
{
    tick = 1;
}

int main(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN;
    GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20;    /* PC13 — светодиод платы */
    i2c1_init();
    as_mag_t m;
    while ((mag_rc = (uint32_t)as_magnet(&m)) != AS_OK) {
        GPIOC->BSRR = GPIO_BSRR_BR13;                   /* горит: магнита нет, слабый или сильный */
        uint32_t t0 = DWT->CYCCNT;
        while (!expired(t0, 100000u)) {
        }
    }
    GPIOC->BSRR = GPIO_BSRR_BS13;
    while (as_write12(AS_CONF, AS_CONF_VALUE) != AS_OK || as_write12(AS_ZPOS, ZERO_RAW) != AS_OK)
        bus_errors++;                                   /* ZPOS — только регистр: без BURN, после сброса снова */
    pwm_in_init();

    uint16_t raw = 0;
    while (as_read12(AS_RAW_ANGLE, &raw) != AS_OK)
        bus_errors++;
    as_turns_init(&turns, raw);
    as_speed_init(&speed, 5u, POLL_HZ);
    SysTick_Config(CPU_HZ / POLL_HZ);
    for (;;) {
        if (!tick)
            continue;
        tick = 0;
        int32_t d = 0;
        if (as_read12(AS_RAW_ANGLE, &raw) == AS_OK)
            d = as_turns_step(&turns, raw);
        else
            bus_errors++;                               /* пропуск: скорость считает 0 за этот тик */
        cps = as_speed_step(&speed, d);
        pos = turns.pos - (int32_t)ZERO_RAW;
    }
}

Дальше: раздел 58 «Импульсные преобразователи».

Открыть в интерактивном справочнике