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 шаг тока — вес единицы регистра Currentaveraging (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.
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: шунт 2,5 мкВ на единицу и ±81,92 мВ, шина 1,25 мВ; CAL =
0,00512 / (Current_LSB · R), шаг мощности — 25 шагов тока; без
CALток и мощность нули. - Шунт — под 81,92 мВ: 10 мОм — 8,192 А и 0,671 Вт на полной шкале; шаг
тока — круглый, не меньше Imax / 32768: 250 мкА и
CAL= 2048. - Неудачный шаг виден в
CAL: 51 вместо 51,2 — −0,4 % тока. - Результат раз в AVG × (VBUSCT + VSHCT): от 280 мкс до 16 883 712 мкс; AVG 16 — 35,2 мс и с. к. о. 0,059 мА.
ALERT: порог в единицах регистра; 50 мс перегрузки с AVG 16 замечены через 46,708 мс, с AVG 64 — пропущены; с защёлкой — до чтенияMask/Enable.- Энергия: таймер и усреднение — ±0,04 %, каждое «готово» — 0,00 %; пауза вместо таймера — −0,45 %, пропуск результатов — −53,35 %.
Powerбез знака: знак энергии брать у тока, заряд и разряд копить отдельно.
Полные исходники
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, ®, 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-разрядный АЦП с последовательным выходом и тарирование.