Embedded HandbookПриложения
Все разделы › XI. Датчики: подключение, схемы, драйверы
STM32F103 · уровень средний · 31 мин чтения
Открыть в интерактивном справочнике

101а. Монитор тока и мощности INA226: 16 бит, калибровочный регистр, усреднение, тревога по порогу и счёт энергии

В разделе 101 — INA219: шунт, свой АЦП, калибровочное число и ток с мощностью по I2C. INA226 — следующая ступень того же семейства: шаг шунта вчетверо мельче, свой счёт усреднения внутри микросхемы и вывод ALERT, который сам поднимает тревогу при перегрузке. Здесь — чем она отличается от INA219, как подключить её к F103 и выбрать шунт, как посчитать CAL и перевести регистры целыми, что дают усреднение и время преобразования, как работает тревога, как считать миллиампер-часы и ватт-часы и что делать с током в обе стороны.

Опоры: драйвер ina226 из Zephyr 4.4.2 (drivers/sensor/ti/ina2xx/ina226.c, ina226.h, include/zephyr/dt-bindings/sensor/ina226.h, dts/bindings/sensor/ti,ina226.yaml): номера регистров, 2,5 мкВ на единицу шунта, 1,25 мВ на единицу шины, шаг мощности — 25 шагов тока, формула CAL, поля Config, времена 140…8244 мкс и счёт AVG 1…1024, значения после сброса, код производителя 0x5449. Биты Mask/Enable — из описания родственной INA230 в том же дереве (include/zephyr/dt-bindings/sensor/ina230.h). RM0008 (DocID13902 Rev 17): таблица режимов выводов, с. 160; AFIO_EXTICR2, с. 190; EXTI_FTSR, с. 211; приём двух байт ведущим, с. 764; I2C_CCR, с. 783–784; I2C_TRISE, с. 784. DocID13587 Rev 17: PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT, с. 32. Документа на INA226 в проекте нет: адреса по выводам A1 и A0, предел напряжения шины, бит 14 Config, внутренний расчёт с округлением и поведение флагов и ALERT — по документации производителя, заложены в модель и с микросхемой не сверены. Все числа — прогон драйвера _verify/ina101b/ina226.c на компьютере (gcc 12.2.0) со своей моделью INA226 как ведомого I2C на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

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

Оригинал Перевод
current/power monitor монитор тока и мощности: шунт, АЦП и расчёт в одной микросхеме
shunt voltage, bus voltage напряжение на шунте; напряжение шины относительно земли
calibration register (CAL) калибровочный регистр: связывает код шунта с током
Current_LSB шаг тока — вес единицы регистра Current
averaging (AVG) сколько преобразований складывать в один результат
conversion time (VBUSCT, VSHCT) время одного преобразования шины и шунта
Mask/Enable register что выводить на ALERT и флаги
alert limit порог тревоги
shunt over-voltage (SOL) шунт выше порога — перегрузка по току
conversion ready (CNVR, CVRF) «результат готов»: включение на ALERT и флаг
latch enable (LEN) защёлка: держать тревогу до чтения
math overflow (OVF) переполнение расчёта тока или мощности
Kelvin connection подключение Кельвина: отдельные провода от площадок шунта
coulomb counting счёт кулонов: интеграл тока по времени

101а.1 Чем INA226 отличается от INA219

INA219 (раздел 101) INA226
шунт: единица 10 мкВ 2,5 мкВ
шунт: диапазон ±320 мВ при усилении по умолчанию ±81,92 мВ, один
шина: единица 4 мВ 1,25 мВ
регистры шунта и тока 12 бит в настройке после сброса 16 бит со знаком
калибровка CAL = 0,04096 / (I_LSB · R) CAL = 0,00512 / (Current_LSB · R)
шаг мощности 20 шагов тока 25 шагов тока

Диапазон шунта — 32768 единиц по 2,5 мкВ, то есть ±81,92 мВ: в 3,9 раза уже, чем ±320 мВ у INA219, зато единица вчетверо мельче. Своё усреднение INA226 задаёт полем AVG — от 1 до 1024 преобразований по 140…8244 мкс, а вывод ALERT с порогом и защёлкой поднимает тревогу без опроса. Поля усиления в Config у INA226 нет — только AVG, VBUSCT, VSHCT и MODE, поэтому шунт выбирают под этот один диапазон (101а.2). Предел напряжения шины 36 В — по документации производителя, на диске его нет: не проверено.

Внутри микросхема считает так же, как INA219, только с другими числами: Current = Shunt × CAL / 2048, Power = Current × Bus / 20000. Это следует из весов Zephyr: 0,00512 / (2,5 мкВ × 2048) = 1, то есть код тока — это ток шунта в шагах Current_LSB, а 1,25 мВ / 25 = 1/20000.

Вверху — подключение INA226 к STM32F103: PB6 — SCL, PB7 — SDA на I2C1 на 400 кГц, PB5 — ALERT на EXTI5 по спаду, выводы A1 и A0 на GND задают адрес 0x40; шунт 10 мОм в плюсовом проводе +12 В перед нагрузкой, IN+ и IN− идут отдельными проводами от площадок шунта, VBUS — со стороны нагрузки. Посередине — что считает микросхема после каждого результата: АЦП шунта 2,5 мкВ на единицу и ±81,92 мВ, затем Current = Shunt × CAL / 2048, Power = Current × Bus / 20000 с АЦП шины 1,25 мВ на единицу и сравнение с Alert Limit на выводе ALERT; CAL для 10 мОм и 250 мкА — 2048, шаг мощности 6,25 мВт. Внизу — поля регистра Config с номерами бит: RST, три бита со значением 100, AVG, VBUSCT, VSHCT, MODE; после сброса 0x4127 — AVG 1, по 1100 мкс, непрерывно, результат раз в 2200 мкс, а с AVG 16 — раз в 35,2 мс


101а.2 Подключение к F103 и выбор шунта

Вывод INA226 Куда Назначение
SCL, SDA PB6, PB7 I2C1
ALERT PB5 тревога или «готово»; EXTI5 по спаду
A1, A0 GND адрес 0x40
VS, GND 3,3 В и земля общая земля с F103
IN+, IN− площадки шунта напряжение на шунте
VBUS шина на стороне нагрузки напряжение для мощности

I2C1 — как в 15б.2: 400 кГц, CCR = F/S | 30, TRISE = 11; PB6 и PB7 — FT (DocID13587, с. 32), PB5 — вход с подтяжкой (код 1000, с. 160). ALERT идёт на EXTI5: AFIO_EXTICR2 — порт B (с. 190), спад включает EXTI_FTSR (с. 211). Чтение регистра — запись указателя, повторный старт и два байта старшим вперёд, как в 17.4; на 400 кГц в модели это 120 мкс, четыре регистра ina_measure — 480 мкс.

Адрес задают два вывода, и у каждого четыре положения, — 16 адресов:

A1 \ A0 GND VS SDA SCL
GND 0x40 0x41 0x42 0x43
VS 0x44 0x45 0x46 0x47
SDA 0x48 0x49 0x4A 0x4B
SCL 0x4C 0x4D 0x4E 0x4F

Шунт. Ток полной шкалы — 81,92 мВ / R, рассеяние на нём — I² · R. Шаг тока берут не меньше Imax / 32768 и округляют вверх до «круглого»:

Шунт Полная шкала Рассеяние на ней Imax Current_LSB CAL Единица шунта в токе
100 мОм 0,819 А 0,067 Вт 0,8 А 25 мкА 2048 0,025 мА
10 мОм 8,192 А 0,671 Вт 8 А 250 мкА 2048 0,25 мА
5 мОм 16,384 А 1,342 Вт 15 А 500 мкА 2048 0,5 мА
2 мОм 40,96 А 3,355 Вт 40 А 2000 мкА 1280 1,25 мА
1 мОм 81,92 А 6,711 Вт 80 А 2500 мкА 2048 2,5 мА

Меньше шунт — меньше греется, но крупнее единица в токе: на 1 мОм единица шунта — уже 2,5 мА. Для прибора на 8 А годится 10 мОм: 0,671 Вт на полной шкале, резистор на 1 Вт.


101а.3 Калибровка: CAL, шаг тока и перевод регистров

CAL = 0,00512 / (Current_LSB · R). В драйвере шаг тока — в наноамперах, шунт — в микроомах, и формула становится целой: 5,12 · 10¹² / (lsb · R) с округлением. Так же считает Zephyr, только в микроамперах:

/* шаг тока: не меньше Imax / 32768 и «круглый» — 1, 2, 2,5 или 5 × 10^n нА (от 10 нА) */
uint32_t ina_lsb_na(uint32_t imax_ma)
{
    static const uint32_t nice[4] = {10, 20, 25, 50};
    uint32_t need = (uint32_t)(((uint64_t)imax_ma * 1000000u + 32767u) / 32768u);   /* нА, вверх */
    for (uint32_t dec = 1; dec <= 10000000u; dec *= 10u)
        for (uint32_t i = 0; i < 4u; i++)
            if (nice[i] * dec >= need)
                return nice[i] * dec;
    return 0;
}

/* CAL = 0,00512 / (Current_LSB · Rshunt); в нА и мкОм — 5,12 · 10^12 / (lsb · R), с округлением */
uint32_t ina_cal(uint32_t lsb_na, uint32_t rshunt_uohm)
{
    uint64_t den = (uint64_t)lsb_na * rshunt_uohm;
    uint64_t cal = (5120000000000ull + den / 2u) / den;
    return cal == 0u || cal > 0x7FFFu ? 0u : (uint32_t)cal;   /* 0 — такой CAL записать нельзя */
}

Зачем «круглый» шаг — видно по ошибке округления CAL. В модели через шунт шёл известный ток, драйвер его читал:

Шунт Current_LSB CAL точно CAL Ошибка CAL Ток Прочитано
100 мОм 25 мкА 2048 2048 0 0,5 А 500 000 мкА
100 мОм 24,414 мкА 2097,157 2097 −75 ppm 0,5 А 499 949 мкА
100 мОм 1000 мкА 51,2 51 −3906 ppm 0,5 А 498 000 мкА
10 мОм 250 мкА 2048 2048 0 5 А 5 000 000 мкА
3 мОм 500 мкА 3413,333 3413 −98 ppm 10 А 9 999 000 мкА

Шаг ровно Imax / 32768 (24,414 мкА) не делает CAL целым и даёт −0,01 %. Крупный шаг (1 мА на 100 мОм) делает CAL маленьким — 51 вместо 51,2, и ток врёт на −0,4 %. А шаг 0,1 мкА на 1 мОм требует CAL = 51 200 000 — в регистр не помещается, и ina_cal возвращает 0.

Инициализация: сброс битом 15 Config, проверка кода производителя, Config, CAL и чтение CAL обратно — 5 обменов, 525 мкс:

int ina_init(ina_t *d, uint8_t addr7, uint32_t lsb_na, uint32_t rshunt_uohm, uint16_t cfg)
{
    uint16_t id, back;
    int rc;
    d->addr = addr7;
    d->xfers = d->errors = 0;
    d->lsb_na = lsb_na;
    d->rshunt_uohm = rshunt_uohm;
    d->cfg = cfg;
    d->cal = (uint16_t)ina_cal(lsb_na, rshunt_uohm);
    if (d->cal == 0u)
        return INA_E_RANGE;                             /* шаг тока и шунт не дают допустимого CAL */
    if ((rc = ina_write(d, INA_CONFIG, INA_CFG_RST)) != INA_OK)
        return rc;
    if ((rc = ina_read(d, INA_MFG_ID, &id)) != INA_OK)
        return rc;
    if (id != INA_MFG_TI)
        return INA_E_ID;
    if ((rc = ina_write(d, INA_CONFIG, cfg)) != INA_OK || (rc = ina_write(d, INA_CAL, d->cal)) != INA_OK
        || (rc = ina_read(d, INA_CAL, &back)) != INA_OK)
        return rc;
    return back == d->cal ? INA_OK : INA_E_CAL;
}

После сброса в модели Config = 0x4127, CAL = 0x0000, Mask/Enable и Alert Limit — 0, код производителя — 0x5449. Пока CAL = 0, ток и мощность — нули: при 0,5 А через 10 мОм шунт показал 5 000 000 нВ, а Current и Power — 0.

Перевод целыми. Ток — код со знаком × шаг, мощность — код без знака × 25 шагов; произведения 64-битные:

int32_t ina_current_ua(int16_t raw, uint32_t lsb_na)
{
    return (int32_t)((int64_t)raw * lsb_na / 1000);
}

uint32_t ina_power_uw(uint16_t raw, uint32_t lsb_na)
{
    return (uint32_t)((uint64_t)raw * 25u * lsb_na / 1000u);   /* шаг мощности — 25 шагов тока */
}

Шунт 10 мОм, шаг 250 мкА, CAL = 2048, шина 12 В. Ток 2,5 А: Shunt = 0x2710 (10000 единиц, 25 мВ), Bus = 0x2580 (9600), Current = 10000 × 2048 / 2048 = 10000, Power = 10000 × 9600 / 20000 = 4800 (0x12C0). Драйвер: 2 500 000 мкА, 30 000 000 мкВт, 12 000 000 мкВ. Ток −1,2 А: Shunt и Current = 0xED40 (−4800), а Power = 0x0900 (2304) — 14 400 000 мкВт, без знака (101а.6).


101а.4 Усреднение и время преобразования

Результат — AVG пар преобразований «шунт, затем шина», каждое длится VSHCT и VBUSCT. В непрерывном режиме новый результат готов раз в AVG × (VBUSCT + VSHCT):

/* время одного результата в непрерывном режиме «шунт и шина»: AVG × (VBUSCT + VSHCT) */
uint32_t ina_conv_us(uint16_t cfg)
{
    return avg_n[cfg >> 9 & 7u] * (uint32_t)(ct_us[cfg >> 6 & 7u] + ct_us[cfg >> 3 & 7u]);
}

После сброса — AVG 1 и по 1100 мкс: результат раз в 2200 мкс. Шум в модели задан так: 7,5 мкВ на шунте при 140 мкс и падает как 1/√t — это допущение, не данные документа. Ток 0,5 А через 10 мОм, шаг 250 мкА, результаты подряд по «готово»:

AVG Время Config Результат раз в С. к. о. тока Разных кодов Размах
1 140 мкс 0x4007 280 мкс 0,774 мА 16 3,75 мА
1 1100 мкс 0x4127 2200 мкс 0,29 мА 7 1,5 мА
4 1100 мкс 0x4327 8800 мкс 0,129 мА 3 0,5 мА
16 1100 мкс 0x4527 35 200 мкс 0,059 мА 3 0,5 мА
64 1100 мкс 0x4727 140 800 мкс 0 1 0
1024 8244 мкс 0x4FFF 16 883 712 мкс 0 1 0

С AVG 64 шум модели меньше единицы шунта, и все 200 результатов одинаковы — дальше складывать нечего (14а.6). Зато результат становится медленным: с AVG 1024 и 8244 мкс — почти 17 с. За это платят реакцией: короткую перегрузку длинное усреднение размазывает (101а.5), а мгновенные значения тока при счёте энергии теряются (101а.6).


101а.5 Тревога ALERT: перегрузка, мощность, «готово» и защёлка

Порог пишут в Alert Limit в единицах того регистра, с которым сравнивают, а в Mask/Enable выбирают одну функцию: SOL — шунт выше порога, SUL — ниже, BOL, BUL — шина, POL — мощность выше, и к ней можно добавить CNVR — «готово». Бит LEN — защёлка, APOL — полярность вывода.

/* порог по току — в единицах регистра шунта: I · R / 2,5 мкВ */
uint16_t ina_shunt_limit(int32_t current_ma, uint32_t rshunt_uohm)
{
    int64_t x = (int64_t)current_ma * rshunt_uohm;      /* мА · мкОм = 10^-9 В */
    int64_t code = (x >= 0 ? x + 1250 : x - 1250) / 2500;
    return (uint16_t)(int16_t)(code > 32767 ? 32767 : code < -32768 ? -32768 : code);
}

Перегрузка 3 А на 10 мОм — 30 мВ, Alert Limit = 12000 (0x2EE0); мощность 40 Вт при шаге 250 мкА — 6400 (0x1900). В модели ток 1 А, на 200-й мс скачок до 4 А на 50 мс:

AVG и время Результат раз в SOL 3 А сработал Через
1, 140 мкс 280 мкс да 0,507 мс
16, 1100 мкс 35,2 мс да 46,708 мс
64, 1100 мкс 140,8 мс нет —
128, 1100 мкс 281,6 мс нет —

Сравнивается результат, а не мгновенный ток: с AVG 64 50 мс по 4 А усредняются с 1 А не больше чем до 2,07 А — ниже порога, и тревоги нет. Импульс в 500 мс с AVG 128 тревогу дал, но через 363,507 мс. Для защиты берут короткое усреднение, для показаний — длинное; одной микросхеме нужно выбрать.

Без защёлки вывод вернулся вверх, как только ток упал. С LEN он остался внизу и после импульса; чтение Mask/Enable вернуло 0x8019 (SOL, AFF, CVRF, LEN) и отпустило вывод. POL на 40 Вт при 12 В сработал так же, через 46,708 мс. В прошивке прерывание по спаду сразу выключает нагрузку:

/* ALERT: спад на PB5 — перегрузка; нагрузку выключить сразу, в прерывании */
void EXTI9_5_IRQHandler(void)
{
    EXTI->PR = 1u << 5;
    GPIOA->BRR = 1u << 0;
    alert = 1;
}

«Готово». С CNVR вывод опускается в конце каждого результата и держится, пока не прочитан Mask/Enable. За 1 с с AVG 16 модель дала 29 результатов: с чтением Mask/Enable после каждого — 29 спадов, без чтения — 1.


101а.6 Счёт энергии и ток в обе стороны

Миллиампер-часы — интеграл тока, ватт-часы — интеграл мощности (счёт кулонов, 173б.5). Драйвер копит µА·мкс в 64 битах — прямоугольниками: последний результат × время:

/* прямоугольник: последний результат × время с прошлого вызова; знак мощности — по знаку тока */
void ina_energy_add(ina_energy_t *e, const ina_meas_t *m, uint32_t dt_us)
{
    e->ua_us += (int64_t)m->current_ua * dt_us;
    if (m->current_ua >= 0)
        e->uw_us_in += (int64_t)m->power_uw * dt_us;
    else
        e->uw_us_out += (int64_t)m->power_uw * dt_us;
}

При 2 А такой счётчик переполнится через 53,4 суток, а 32-битный в µА·мс — через 1,074 с. В модели нагрузка 0,3 А и импульсы ещё 1,7 А по 20 % времени с периодом 37 мс — в среднем 0,64 А, шина 12 В, шунт 10 мОм, 30 с:

Как считали Точно Насчитано Ошибка
таймер 100 мс, AVG 1, 140 мкс 5333 мкА·ч 5379 мкА·ч +0,86 %
таймер 100 мс, AVG 16, 1100 мкс 5334 мкА·ч 5336 мкА·ч +0,04 %
таймер 100 мс, AVG 64, 1100 мкс 5334 мкА·ч 5332 мкА·ч −0,04 %
таймер 1 с, AVG 16, 1100 мкс 5334 мкА·ч 5362 мкА·ч +0,52 %
пауза 100 мс вместо таймера, AVG 16 5360 мкА·ч 5336 мкА·ч −0,45 %
каждое «готово», AVG 16, 1100 мкс 5333,1 мкА·ч 5333 мкА·ч 0,00 %
каждое «готово», AVG 1, 140 мкс 3200,7 мкА·ч 1493 мкА·ч −53,35 %

Что видно. Мгновенный снимок (AVG 1, 140 мкс) раз в 100 мс ловит импульсы как попало: +0,86 % по току, а ватт-часы — 66 570 мкВт·ч вместо 63 996,6, на 4,0 % больше. Результат готов раз в 280 мкс, а четыре регистра читаются 480 мкс: Current и Power попадают в разные результаты. Усреднение, сравнимое с периодом опроса, снимает ошибку до сотых процента. Пауза вместо таймера теряет время самого чтения: 300 раз по 480 мкс, и 30 с превращаются в 30,144 с, а считано 30 — −0,45 %. Период должен задавать таймер (13.2), а не пауза в цикле. Каждое «готово» точнее всего, пока чтение успевает: с AVG 1 и 140 мкс результат каждые 280 мкс, чтение дольше, и больше половины результатов пропущено — −53,35 %.

Ток в обе стороны. Аккумулятор 12 В: заряд +0,8 А 20 с, затем разряд −1,2 А 20 с. При заряде Shunt = 0x0C80, ток +800 000 мкА, при разряде Shunt = 0xED40, ток −1 200 000 мкА, а Power в обоих случаях положительный: 9 600 000 и 14 400 000 мкВт. Поэтому знак энергии драйвер берёт у тока, а заряд и разряд копит отдельно. По каждому «готово» с AVG 16: итог −2217 мкА·ч (точно −2218,3), в аккумулятор — 53 316 мкВт·ч (точно 53 333,3), из него — 79 930 (точно 79 952,4). Сложи Power без знака — вышло бы 133 246 мкВт·ч «расхода» вместо разницы. Шунт, впаянный наоборот (IN+ и IN− местами), даёт +2217 мкА·ч: заряд и разряд меняются местами, и это видно только по знаку.


101а.7 Грабли

Ток врёт на четверть, а CAL верный. IN+ и IN− взяты не с площадок шунта, а с дорожек: в цепь измерения попала медь. Шунт 2 мОм и 0,5 мОм меди — 12 500 000 мкА вместо 10 А, +25 %. С отводами Кельвина — ровно 10 000 000 мкА.

Мощность меньше, чем «должна быть». VBUS подключён к стороне нагрузки — и это правильно: при 2 А через 10 мОм шина 11,98 В вместо 12 В, мощность 23 956 250 мкВт вместо 24 000 000. Кому нужна мощность источника, а не нагрузки, — VBUS до шунта. А шунт в минусовом проводе сдвигает землю нагрузки (101.5).

Ток и мощность вдруг нули. Микросхема перезапустилась — провал питания, — и CAL снова 0. В модели после сброса 500 000 мкА превратились в 0. ina_check_cal раз в цикл сверяет CAL с записанным: вернула INA_E_CAL (−5), записала Config и CAL заново, следующий результат — 500 000 мкА.

/* микросхема могла перезапуститься (провал питания): после сброса CAL = 0 — записать заново */
int ina_check_cal(ina_t *d)
{
    uint16_t v;
    int rc = ina_read(d, INA_CAL, &v);
    if (rc != INA_OK || v == d->cal)
        return rc;
    if ((rc = ina_write(d, INA_CONFIG, d->cfg)) != INA_OK || (rc = ina_write(d, INA_CAL, d->cal)) != INA_OK)
        return rc;
    return INA_E_CAL;                                   /* было потеряно и восстановлено */
}

Ток упёрся в 3,2767 А. Шаг тока выбран под 2 А (100 мкА, CAL = 5120), а течёт 5 А: Current = 0x7FFF, 3 276 700 мкА, мощность 78 640 000 мкВт вместо 120 Вт, во флагах — 0x000C, OVF вместе с CVRF. С шагом 250 мкА — 5 000 000 мкА и 120 000 000 мкВт, флаги 0x0008. Power — 16 бит без знака: с шагом 100 мкА больше 163,838 Вт он не покажет, с шагом 250 мкА — 409,594 Вт. Шаг выбирают по наибольшему току, который может случиться, а не по рабочему.

Не отвечает по адресу. A1 или A0 подключены не туда: на 0x41 вместо 0x40 ina_init вернула INA_E_NACK (−1). И A1 на SDA даёт 0x48–0x4B — те же адреса, что у ADS1115 (15б.2): на одной шине их не совместить.

«Готово» пришло один раз. Mask/Enable не прочитан — CVRF держит вывод внизу, нового спада нет (101а.5).

Энергия меньше на десятки процентов. Результаты чаще, чем их успевают читать, а каждый считают за полный интервал (101а.6).


101а.8 Итоги

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

ina226.h — регистры, поля Config, биты Mask/Enable, адреса, порт и функции драйвера.

/* ina226.h — раздел 101а: драйвер монитора тока и мощности INA226 по I2C без регистров микроконтроллера.
 * Драйвер говорит с микросхемой только через функции порта (ina_port_*): в прошивке fw101b.c их делают
 * I2C1 и EXTI5 STM32F103, в стенде host101b.c — модель model101b.py.
 * Карта регистров, веса младших разрядов (2,5 мкВ, 1,25 мВ, мощность — 25 шагов тока), поля Config и
 * формула CAL сверены с драйвером ina226 из Zephyr 4.4.2; биты Mask/Enable — с его описанием INA230.
 * Адреса A1/A0, бит 14 Config и поведение флагов — по документации производителя, с микросхемой не сверено. */
#ifndef INA226_H
#define INA226_H
#include <stdint.h>

/* ── регистры: номер пишется первым байтом записи в регистр указателя ── */
#define INA_CONFIG        0x00u
#define INA_SHUNT         0x01u         /* напряжение шунта, со знаком, 2,5 мкВ на единицу */
#define INA_BUS           0x02u         /* напряжение шины, без знака, 1,25 мВ на единицу */
#define INA_POWER         0x03u         /* мощность, без знака, 25 шагов тока × 1 В */
#define INA_CURRENT       0x04u         /* ток, со знаком, шаг — Current_LSB */
#define INA_CAL           0x05u         /* калибровка: после сброса 0 — ток и мощность не считаются */
#define INA_MASK          0x06u         /* Mask/Enable: что выводить на ALERT и флаги */
#define INA_LIMIT         0x07u         /* порог для ALERT, в единицах выбранного регистра */
#define INA_MFG_ID        0xFEu
#define INA_MFG_TI        0x5449u       /* «TI» */

/* ── Config: AVG 11:9, VBUSCT 8:6, VSHCT 5:3, MODE 2:0; бит 15 — сброс ──── */
#define INA_CFG_RST       0x8000u
#define INA_CFG(avg, vbusct, vshct, mode) \
    ((uint16_t)(0x4000u | (avg) << 9 | (vbusct) << 6 | (vshct) << 3 | (mode)))
enum { INA_AVG_1, INA_AVG_4, INA_AVG_16, INA_AVG_64, INA_AVG_128, INA_AVG_256, INA_AVG_512, INA_AVG_1024 };
enum { INA_CT_140, INA_CT_204, INA_CT_332, INA_CT_588, INA_CT_1100, INA_CT_2116, INA_CT_4156, INA_CT_8244 };
#define INA_MODE_CONT     7u            /* шунт и шина, непрерывно */
#define INA_CFG_DEFAULT   INA_CFG(INA_AVG_1, INA_CT_1100, INA_CT_1100, INA_MODE_CONT)   /* 0x4127 */

/* ── Mask/Enable: старшие биты выбирают, что выводить на ALERT; младшие — флаги и режим вывода ── */
#define INA_SOL           0x8000u       /* шунт выше порога: перегрузка по току */
#define INA_SUL           0x4000u       /* шунт ниже порога */
#define INA_BOL           0x2000u       /* шина выше порога */
#define INA_BUL           0x1000u       /* шина ниже порога */
#define INA_POL           0x0800u       /* мощность выше порога */
#define INA_CNVR          0x0400u       /* «готово к чтению» */
#define INA_AFF           0x0010u       /* флаг: сработала тревога */
#define INA_CVRF          0x0008u       /* флаг: результат готов */
#define INA_OVF           0x0004u       /* флаг: переполнение расчёта тока или мощности */
#define INA_APOL          0x0002u       /* 1 — ALERT активен единицей; по умолчанию нулём */
#define INA_LEN           0x0001u       /* 1 — защёлка: держать ALERT до чтения Mask/Enable */

#define INA_ADDR(a1, a0)  ((uint8_t)(0x40u | (a1) << 2 | (a0)))   /* выводы: 0 — GND, 1 — VS, 2 — SDA, 3 — SCL */

enum { INA_OK = 0, INA_E_NACK = -1, INA_E_BUS = -2, INA_E_TIMEOUT = -3, INA_E_ID = -4, INA_E_CAL = -5,
       INA_E_RANGE = -6 };

typedef struct {
    uint8_t addr;
    uint16_t cfg, cal;                  /* что записано в Config и CAL — чтобы заметить сброс микросхемы */
    uint32_t lsb_na;                    /* шаг тока, нА */
    uint32_t rshunt_uohm;               /* шунт, мкОм */
    uint32_t xfers, errors;
} ina_t;

typedef struct {
    int32_t shunt_nv;                   /* напряжение шунта, нВ */
    uint32_t bus_uv;                    /* напряжение шины, мкВ */
    int32_t current_ua;                 /* ток со знаком, мкА */
    uint32_t power_uw;                  /* мощность из регистра Power, без знака, мкВт */
    uint16_t raw_current, raw_power;
} ina_meas_t;

typedef struct {
    int64_t ua_us;                      /* ∫ I dt, мкА·мкс, со знаком: заряд минус разряд */
    int64_t uw_us_in, uw_us_out;        /* ∫ P dt, мкВт·мкс, отдельно в каждую сторону */
} ina_energy_t;

/* ── порт: в прошивке — I2C1 и EXTI5, в стенде — строки к модели ─────── */
int ina_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx);
void ina_port_delay_us(uint32_t us);
/* ждать активного уровня на ALERT (флаг EXTI) не дольше timeout_us; 1 — был, флаг снят */
int ina_port_wait_alert(uint32_t timeout_us);

/* ── драйвер ──────────────────────────────────────────────────────────── */
uint32_t ina_lsb_na(uint32_t imax_ma);
uint32_t ina_cal(uint32_t lsb_na, uint32_t rshunt_uohm);
uint32_t ina_conv_us(uint16_t cfg);
int ina_write(ina_t *d, uint8_t reg, uint16_t v);
int ina_read(ina_t *d, uint8_t reg, uint16_t *v);
int ina_init(ina_t *d, uint8_t addr7, uint32_t lsb_na, uint32_t rshunt_uohm, uint16_t cfg);
int ina_check_cal(ina_t *d);
int ina_measure(ina_t *d, ina_meas_t *m);
int32_t ina_current_ua(int16_t raw, uint32_t lsb_na);
uint32_t ina_power_uw(uint16_t raw, uint32_t lsb_na);
uint16_t ina_shunt_limit(int32_t current_ma, uint32_t rshunt_uohm);
uint16_t ina_power_limit(uint32_t power_mw, uint32_t lsb_na);
int ina_alert(ina_t *d, uint16_t mask, uint16_t limit);
int ina_flags(ina_t *d, uint16_t *mask);
void ina_energy_add(ina_energy_t *e, const ina_meas_t *m, uint32_t dt_us);
int32_t ina_energy_uah(const ina_energy_t *e);
int32_t ina_energy_uwh(const ina_energy_t *e, int dir);

#endif

ina226.c — драйвер: CAL и шаг тока, инициализация, перевод регистров, тревога, счёт энергии.

/* ina226.c — раздел 101а: драйвер INA226 по I2C. Один и тот же файл собирается в прошивку (armclang,
 * Cortex-M3) и в стенд на компьютере (gcc): микроконтроллера он не знает, только порт ina_port_*. */
#include "ina226.h"

static const uint16_t ct_us[8] = {140, 204, 332, 588, 1100, 2116, 4156, 8244};   /* VBUSCT, VSHCT */
static const uint16_t avg_n[8] = {1, 4, 16, 64, 128, 256, 512, 1024};            /* AVG */

/* шаг тока: не меньше Imax / 32768 и «круглый» — 1, 2, 2,5 или 5 × 10^n нА (от 10 нА) */
uint32_t ina_lsb_na(uint32_t imax_ma)
{
    static const uint32_t nice[4] = {10, 20, 25, 50};
    uint32_t need = (uint32_t)(((uint64_t)imax_ma * 1000000u + 32767u) / 32768u);   /* нА, вверх */
    for (uint32_t dec = 1; dec <= 10000000u; dec *= 10u)
        for (uint32_t i = 0; i < 4u; i++)
            if (nice[i] * dec >= need)
                return nice[i] * dec;
    return 0;
}

/* CAL = 0,00512 / (Current_LSB · Rshunt); в нА и мкОм — 5,12 · 10^12 / (lsb · R), с округлением */
uint32_t ina_cal(uint32_t lsb_na, uint32_t rshunt_uohm)
{
    uint64_t den = (uint64_t)lsb_na * rshunt_uohm;
    uint64_t cal = (5120000000000ull + den / 2u) / den;
    return cal == 0u || cal > 0x7FFFu ? 0u : (uint32_t)cal;   /* 0 — такой CAL записать нельзя */
}

/* время одного результата в непрерывном режиме «шунт и шина»: AVG × (VBUSCT + VSHCT) */
uint32_t ina_conv_us(uint16_t cfg)
{
    return avg_n[cfg >> 9 & 7u] * (uint32_t)(ct_us[cfg >> 6 & 7u] + ct_us[cfg >> 3 & 7u]);
}

static int xfer(ina_t *d, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx)
{
    int rc = ina_port_xfer(d->addr, tx, ntx, rx, nrx);
    d->xfers++;
    if (rc)
        d->errors++;
    return rc;
}

int ina_write(ina_t *d, uint8_t reg, uint16_t v)
{
    uint8_t b[3] = {reg, (uint8_t)(v >> 8), (uint8_t)v};   /* старший байт первым */
    return xfer(d, b, 3, 0, 0);
}

int ina_read(ina_t *d, uint8_t reg, uint16_t *v)
{
    uint8_t b[2];
    int rc = xfer(d, &reg, 1, b, 2);                    /* указатель, повторный старт, два байта */
    if (rc == INA_OK)
        *v = (uint16_t)(b[0] << 8 | b[1]);
    return rc;
}

int ina_init(ina_t *d, uint8_t addr7, uint32_t lsb_na, uint32_t rshunt_uohm, uint16_t cfg)
{
    uint16_t id, back;
    int rc;
    d->addr = addr7;
    d->xfers = d->errors = 0;
    d->lsb_na = lsb_na;
    d->rshunt_uohm = rshunt_uohm;
    d->cfg = cfg;
    d->cal = (uint16_t)ina_cal(lsb_na, rshunt_uohm);
    if (d->cal == 0u)
        return INA_E_RANGE;                             /* шаг тока и шунт не дают допустимого CAL */
    if ((rc = ina_write(d, INA_CONFIG, INA_CFG_RST)) != INA_OK)
        return rc;
    if ((rc = ina_read(d, INA_MFG_ID, &id)) != INA_OK)
        return rc;
    if (id != INA_MFG_TI)
        return INA_E_ID;
    if ((rc = ina_write(d, INA_CONFIG, cfg)) != INA_OK || (rc = ina_write(d, INA_CAL, d->cal)) != INA_OK
        || (rc = ina_read(d, INA_CAL, &back)) != INA_OK)
        return rc;
    return back == d->cal ? INA_OK : INA_E_CAL;
}

/* микросхема могла перезапуститься (провал питания): после сброса CAL = 0 — записать заново */
int ina_check_cal(ina_t *d)
{
    uint16_t v;
    int rc = ina_read(d, INA_CAL, &v);
    if (rc != INA_OK || v == d->cal)
        return rc;
    if ((rc = ina_write(d, INA_CONFIG, d->cfg)) != INA_OK || (rc = ina_write(d, INA_CAL, d->cal)) != INA_OK)
        return rc;
    return INA_E_CAL;                                   /* было потеряно и восстановлено */
}

int32_t ina_current_ua(int16_t raw, uint32_t lsb_na)
{
    return (int32_t)((int64_t)raw * lsb_na / 1000);
}

uint32_t ina_power_uw(uint16_t raw, uint32_t lsb_na)
{
    return (uint32_t)((uint64_t)raw * 25u * lsb_na / 1000u);   /* шаг мощности — 25 шагов тока */
}

int ina_measure(ina_t *d, ina_meas_t *m)
{
    uint16_t s, b, c, p;
    int rc;
    if ((rc = ina_read(d, INA_SHUNT, &s)) || (rc = ina_read(d, INA_BUS, &b))
        || (rc = ina_read(d, INA_CURRENT, &c)) || (rc = ina_read(d, INA_POWER, &p)))
        return rc;
    m->shunt_nv = (int16_t)s * 2500;                    /* 2,5 мкВ на единицу */
    m->bus_uv = b * 1250u;                              /* 1,25 мВ на единицу */
    m->raw_current = c;
    m->raw_power = p;
    m->current_ua = ina_current_ua((int16_t)c, d->lsb_na);
    m->power_uw = ina_power_uw(p, d->lsb_na);
    return INA_OK;
}

/* порог по току — в единицах регистра шунта: I · R / 2,5 мкВ */
uint16_t ina_shunt_limit(int32_t current_ma, uint32_t rshunt_uohm)
{
    int64_t x = (int64_t)current_ma * rshunt_uohm;      /* мА · мкОм = 10^-9 В */
    int64_t code = (x >= 0 ? x + 1250 : x - 1250) / 2500;
    return (uint16_t)(int16_t)(code > 32767 ? 32767 : code < -32768 ? -32768 : code);
}

/* порог по мощности — в единицах регистра Power: P / (25 · Current_LSB) */
uint16_t ina_power_limit(uint32_t power_mw, uint32_t lsb_na)
{
    uint64_t code = ((uint64_t)power_mw * 1000000u + 12u * lsb_na) / (25u * (uint64_t)lsb_na);
    return (uint16_t)(code > 0xFFFFu ? 0xFFFFu : code);
}

int ina_alert(ina_t *d, uint16_t mask, uint16_t limit)
{
    int rc = ina_write(d, INA_LIMIT, limit);
    return rc ? rc : ina_write(d, INA_MASK, mask);
}

/* чтение Mask/Enable: флаги AFF, CVRF, OVF; оно же снимает защёлку ALERT */
int ina_flags(ina_t *d, uint16_t *mask)
{
    return ina_read(d, INA_MASK, mask);
}

/* прямоугольник: последний результат × время с прошлого вызова; знак мощности — по знаку тока */
void ina_energy_add(ina_energy_t *e, const ina_meas_t *m, uint32_t dt_us)
{
    e->ua_us += (int64_t)m->current_ua * dt_us;
    if (m->current_ua >= 0)
        e->uw_us_in += (int64_t)m->power_uw * dt_us;
    else
        e->uw_us_out += (int64_t)m->power_uw * dt_us;
}

int32_t ina_energy_uah(const ina_energy_t *e)
{
    return (int32_t)(e->ua_us / 3600000000);            /* мкА·мкс → мкА·ч */
}

int32_t ina_energy_uwh(const ina_energy_t *e, int dir)
{
    int64_t x = dir > 0 ? e->uw_us_in : dir < 0 ? e->uw_us_out : e->uw_us_in - e->uw_us_out;   /* 0 — разность */
    return (int32_t)(x / 3600000000);                   /* мкВт·мкс → мкВт·ч */
}

fw101b.c — прошивка STM32F103: I2C1 на 400 кГц, EXTI5 на ALERT, TIM2 раз в 100 мс, счёт энергии.

/* fw101b.c — раздел 101а: INA226 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц, таймеры на APB1 — 72 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 400 кГц); PB5 — ALERT (вход с подтяжкой, EXTI5 по спаду).
 * A1 и A0 микросхемы — на GND: адрес 0x40. Шунт 10 мОм, шаг тока 250 мкА, CAL = 2048; AVG 16 и по 1100 мкс —
 * результат раз в 35,2 мс. TIM2 раз в 100 мс: ток, мощность, счёт энергии. ALERT — перегрузка выше 3 А с
 * защёлкой: прерывание выключает нагрузку (PA0, ключ), чтение Mask/Enable снимает тревогу, нагрузка остаётся
 * выключенной до перезапуска.
 * Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "ina226.h"

#define CPU_HZ          72000000u
#define APB1_MHZ        36u
#define I2C_TIMEOUT_US  1000u
#define TICK_US         100000u         /* период TIM2 */
#define RSHUNT_UOHM     10000u          /* 10 мОм */
#define LSB_NA          250000u         /* 250 мкА: 8 А / 32768 = 244 мкА, вверх до круглого */
#define TRIP_MA         3000

static ina_t ina;
static ina_energy_t en;
static volatile uint8_t tick, alert;
volatile ina_meas_t now;
volatile int32_t charge_uah, in_uwh, out_uwh;
volatile uint32_t trips, bus_errors, cal_lost;

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

void ina_port_delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!expired(t0, us)) {
    }
}

static void i2c1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111); PB5 — вход с подтяжкой (1000) */
    GPIOB->CRL = (GPIOB->CRL & 0x000FFFFFu) | 0xFF800000u;
    GPIOB->BSRR = 1u << 5;
    I2C1->CR1 = 0;
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = I2C_CCR_FS | 30u;                       /* DUTY = 0: 36 МГц / (3 × 30) = 400 кГц */
    I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u;         /* 300 нс × 36 МГц = 10,8 → 10 + 1 = 11 */
    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 INA_E_NACK;
        if (expired(t0, I2C_TIMEOUT_US))
            return INA_E_BUS;
    }
    return INA_OK;
}

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

int ina_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx)
{
    int rc;
    uint32_t t0 = DWT->CYCCNT;
    if (ntx == 0u || (nrx != 0u && nrx != 2u))
        return INA_E_BUS;                               /* драйвер всегда пишет указатель и читает по два байта */
    while (I2C1->SR2 & I2C_SR2_BUSY) {
        if (expired(t0, I2C_TIMEOUT_US))
            return INA_E_BUS;
    }
    I2C1->CR1 |= I2C_CR1_START;
    if ((rc = sr1_wait(I2C_SR1_SB)) != INA_OK)
        return fail(rc);
    I2C1->DR = (uint8_t)(addr7 << 1);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != INA_OK)
        return fail(rc);
    (void)I2C1->SR2;
    for (uint32_t i = 0; i < ntx; i++) {
        if ((rc = sr1_wait(I2C_SR1_TXE)) != INA_OK)
            return fail(rc);
        I2C1->DR = tx[i];
    }
    if ((rc = sr1_wait(I2C_SR1_BTF)) != INA_OK)
        return fail(rc);
    if (!nrx) {
        I2C1->CR1 |= I2C_CR1_STOP;
        return INA_OK;
    }
    I2C1->CR1 |= I2C_CR1_START;                         /* повторный старт */
    if ((rc = sr1_wait(I2C_SR1_SB)) != INA_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_POS | I2C_CR1_ACK;             /* два байта: RM0008, с. 764 */
    I2C1->DR = (uint8_t)(addr7 << 1 | 1u);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != INA_OK)
        return fail(rc);
    (void)I2C1->SR2;
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    if ((rc = sr1_wait(I2C_SR1_BTF)) != INA_OK)
        return fail(rc);
    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 INA_OK;
}

/* ALERT: спад на PB5 — перегрузка; нагрузку выключить сразу, в прерывании */
void EXTI9_5_IRQHandler(void)
{
    EXTI->PR = 1u << 5;
    GPIOA->BRR = 1u << 0;
    alert = 1;
}

int ina_port_wait_alert(uint32_t timeout_us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!alert) {
        if (expired(t0, timeout_us))
            return 0;
    }
    alert = 0;
    return 1;
}

void TIM2_IRQHandler(void)
{
    TIM2->SR = ~TIM_SR_UIF;
    tick = 1;
}

static void periph_init(void)
{
    GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x2u;           /* PA0 — выход 2 МГц, двухтактный: ключ нагрузки */
    AFIO->EXTICR[1] = (AFIO->EXTICR[1] & ~0xF0u) | 0x10u;    /* EXTI5 ← PB5 */
    EXTI->FTSR |= 1u << 5;
    EXTI->IMR |= 1u << 5;
    NVIC_EnableIRQ(EXTI9_5_IRQn);
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    TIM2->PSC = 7200u - 1u;                             /* 72 МГц / 7200 = 10 кГц */
    TIM2->ARR = TICK_US / 100u - 1u;                    /* 1000 тиков по 100 мкс = 100 мс */
    TIM2->EGR = TIM_EGR_UG;
    TIM2->SR = ~TIM_SR_UIF;
    TIM2->DIER |= TIM_DIER_UIE;
    TIM2->CR1 |= TIM_CR1_CEN;
    NVIC_EnableIRQ(TIM2_IRQn);
}

static int ina_setup(void)
{
    int rc = ina_init(&ina, INA_ADDR(0, 0), LSB_NA, RSHUNT_UOHM, INA_CFG(INA_AVG_16, INA_CT_1100, INA_CT_1100,
                                                                        INA_MODE_CONT));
    return rc ? rc : ina_alert(&ina, INA_SOL | INA_LEN, ina_shunt_limit(TRIP_MA, RSHUNT_UOHM));
}

int main(void)
{
    ina_meas_t m;
    uint16_t f;
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    i2c1_init();
    periph_init();
    while (ina_setup() != INA_OK)
        ina_port_delay_us(100000u);                     /* нет ответа или не та микросхема */
    GPIOA->BSRR = 1u << 0;                              /* нагрузку включить */
    for (;;) {
        if (alert) {
            alert = 0;
            trips++;
            (void)ina_flags(&ina, &f);                  /* снять защёлку; нагрузка остаётся выключенной */
        }
        if (!tick)
            continue;
        tick = 0;
        int rc = ina_check_cal(&ina);                   /* сброс микросхемы — CAL снова 0 */
        if (rc == INA_E_CAL) {
            cal_lost++;
            (void)ina_alert(&ina, INA_SOL | INA_LEN, ina_shunt_limit(TRIP_MA, RSHUNT_UOHM));
        } else if (rc != INA_OK) {
            bus_errors++;
            continue;
        }
        if (ina_measure(&ina, &m) != INA_OK) {
            bus_errors++;
            continue;
        }
        ina_energy_add(&en, &m, TICK_US);               /* dt — период таймера, а не «сколько прошло» */
        now = m;
        charge_uah = ina_energy_uah(&en);
        in_uwh = ina_energy_uwh(&en, 1);
        out_uwh = ina_energy_uwh(&en, -1);
    }
}

Дальше: раздел 102 «Вес: тензодатчик и HX711» — мост из тензорезисторов, 24-разрядный АЦП с последовательным выходом и тарирование.

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