Перевод статьи Precision Digital Micro Current Meter with STM32Jobit Joseph, Circuit Digest.

Следить за очень малыми токами часто необходимо при разработке и тестировании маломощных устройств — особенно во встраиваемых системах и электронике с батарейным питанием. В этом подробном руководстве показано, как собрать прецизионный цифровой микроамперметр на микроконтроллере STM32 и аналоговых компонентах. В основе — микроконтроллер STM32 и OLED-дисплей для показаний в реальном времени. Аналоговый фронтенд спроектирован так, чтобы точно улавливать малые токи и переводить их в уровни напряжения, которые микроконтроллер обрабатывает с высоким разрешением. Измеряете ли вы ток сна в сенсорном узле или характеризуете энергоэффективную схему — этот микроамперметр даёт наглядную и мгновенную картину потребления.

Прибор рассчитан на удобство работы на столе: упор сделан на читаемость, стабильность и портативность. OLED-дисплей даёт чёткое изображение, различимое при большинстве условий освещения, а микроконтроллер занимается обработкой сигнала и автоматическим выбором диапазона. Процедуры калибровки обеспечивают согласованность измерений при разных нагрузках. Никаких внешних инструментов и сложной настройки — устройство работает по принципу «включил и пользуйся». Оно измеряет ток в реальном времени на OLED-дисплее с высокой точностью в нескольких диапазонах и выдаёт лабораторного уровня измерения в портативном корпусе с батарейным питанием.

Ключевые особенности#

Этот цифровой микроамперметр вобрал в себя возможности профессионального уровня, что делает его пригодным как для любительской, так и для профессиональной работы с электроникой:

  • построен на микроконтроллере STM32G071KBU для быстрой и точной обработки сигнала;
  • OLED-дисплей 128×64 для чётких и читаемых показаний тока;
  • несколько диапазонов измерения:
    • наноамперный: до 1000 нА;
    • микроамперный: до 1000 мкА;
    • миллиамперный: до 300 мА;
  • обновление показаний в реальном времени;
  • компактный функциональный корпус под 3D-печать;
  • прошивка на базе Arduino — легко дорабатывать;
  • батарейное питание со встроенным зарядным устройством;
  • программный (soft) выключатель питания.

Компоненты для сборки#

Ниже — список компонентов. Точные характеристики есть в подробном BOM и схеме.

  • микроконтроллер STM32G071KBU6;
  • OLED-дисплей 128×64;
  • MAX4239AUT — прецизионный ОУ со сверхнизким смещением/дрейфом и низким шумом;
  • INA333 — микромощный инструментальный усилитель с нулевым дрейфом и rail-to-rail выходом;
  • LM321 — низковольтный ОУ с rail-to-rail выходом;
  • прецизионные шунтирующие резисторы (для измерения тока в диапазонах нА, мкА и мА);
  • пассивные компоненты — резисторы и конденсаторы;
  • переключатель DP3T для выбора диапазона;
  • штыревые разъёмы для программирования и питания;
  • тактовая кнопка для сброса или выбора режима;
  • разъём USB Type-C;
  • диоды и защитные компоненты;
  • LiPo-аккумулятор;
  • заказная печатная плата;
  • корпус под 3D-печать;
  • прочие инструменты и расходники для сборки.

Схема и разбор конструкции#

Полная схема микроамперметра показана ниже. PDF-версию схемы можно скачать из репозитория GitHub по ссылке в конце статьи. Схема разбита на функциональные блоки — так проще разобраться.

Полная схема прецизионного цифрового микроамперметра на STM32

Полная схема прибора, разбитая на функциональные блоки.

Каждый блок разберём отдельно и объясним его назначение.

Блок питания#

Блок питания рассчитан на эффективную и надёжную работу с поддержкой входа USB Type-C и заряда аккумулятора. Разъём USB Type-C служит основным входом питания; к линиям CC1 и CC2 добавлены подтягивающие к земле резисторы в соответствии со спецификацией USB Type-C, что обеспечивает совместимость со стандартными источниками 5 В. Для управления аккумулятором используется LTC4054ES5 — компактная линейная микросхема зарядки, безопасно и эффективно заряжающая одну ячейку Li-ion. Чтобы подать на остальную схему стабилизированное напряжение, применён LDO-регулятор ADP44002, формирующий стабильные 2,5 В — их хватает и для микроконтроллера STM32G071, и для компонентов аналогового фронтенда.

Ключевая особенность блока — soft-latch выключатель питания на MOSFET-ах AO3401 и AO3400. При нажатии тактовой кнопки затвор P-канального AO3401 подтягивается к низкому уровню, транзистор открывается и подаёт питание на плату. После запуска STM32 настраивает пин PA4 как вход с подтяжкой, удерживая затвор N-канального AO3400 на высоком уровне — это фиксирует AO3401 во включённом состоянии даже после отпускания кнопки. Тот же пин (PA4) следит за состоянием кнопки. При обнаружении долгого нажатия микроконтроллер может переконфигурировать PA4 как цифровой выход и подать низкий уровень: это притягивает затвор AO3401 через AO3400, закрывает его и отключает питание платы. Два диода изолируют цепи управления и фиксации, позволяя реализовать двойную функциональность на одной линии GPIO.

Схема блока питания: USB Type-C, зарядка LTC4054, LDO и soft-latch на MOSFET

Блок питания: вход USB-C, зарядник LTC4054, LDO ADP44002 и soft-latch выключатель.

Интерфейс микроконтроллера STM32#

Сердце схемы — микроконтроллер STM32G071KBU, который управляет всеми основными функциями: сбором данных, дисплеем и питанием. Несколько аналоговых GPIO настроены на работу с аналоговым фронтендом и мониторинг напряжений. В частности, PA0 — основной вход АЦП для измерения напряжения с фронтенда, соответствующего измеряемому току. PA1 следит за виртуальной землёй (около 1,25 В), используемой внутри аналоговой части для симметричного диапазона сигнала. PA2 опционально подключён к прецизионному источнику опорного напряжения (REF3012AIDBZR) для точного расчёта VDDA и калибровки. PA3 измеряет напряжение аккумулятора через делитель, обеспечивая мониторинг заряда в реальном времени.

Подключения микроконтроллера STM32G071KBU: входы АЦП, I2C, SWD, мониторинг батареи

Обвязка STM32G071KBU: входы АЦП, I2C, программирование по SWD и мониторинг батареи.

Дисплей и интерфейсы программирования#

Для связи с дисплеем микроконтроллер использует пины I2C — PA9 (SCL) и PA10 (SDA) — к OLED-дисплею 128×64. Для разработки и отладки прошивки на плату выведены линии SWD (Serial Wire Debug) и UART, что позволяет программировать и диагностировать прибор внутри системы.

Интерфейсы дисплея и программирования: OLED по I2C, SWD и UART

OLED по I2C, разъём SWD для программирования и UART для отладки.

Аналоговый фронтенд (по мотивам µCurrent)#

Аналоговый фронтенд основан на известной конструкции µCurrent Дэйва Джонса (EEVblog), которая славится низким падением напряжения (burden voltage) и высокой точностью. По сути, система работает как прецизионный преобразователь «ток → напряжение» на наборе высокоточных шунтов и усилителей со сверхнизким смещением. Шунты собраны в многодиапазонную схему, охватывающую уровни нА, мкА и мА, и подобраны так, чтобы падение напряжения оставалось пренебрежимо малым. Ток пропускается через один из этих резисторов, а образующееся падение напряжения усиливается автозеро- («чоппер»-) усилителем. Такая архитектура удерживает выходное смещение около нуля и практически исключает шум 1/f — оба свойства критичны для надёжного улавливания крайне малых токов.

Ключевой компонент, обеспечивающий точность, — Maxim MAX4239, автозеро-усилитель с впечатляющим смещением всего до 0,1 мкВ. Автозеро-усилители непрерывно измеряют и компенсируют собственное смещение по двухфазному процессу, управляемому внутренним тактовым генератором. В результате выход динамически скорректирован — компенсируются и постоянное смещение, и низкочастотный шум, что даёт чистый сигнал даже в тяжёлых условиях малых токов.

В этой реализации между аналоговым фронтендом и АЦП STM32 добавлен ещё один каскад — прецизионный инструментальный усилитель INA333AIDGKR. Это важное усовершенствование из-за использования виртуальной земли (примерно 1,25 В) в аналоговом тракте. Виртуальная земля позволяет фронтенду работать с двуполярными сигналами относительно середины питания, но означает, что выход нельзя подать напрямую на однополярный АЦП STM32. INA333 решает это, усиливая дифференциальный сигнал с высоким подавлением синфазной составляющей и привязывая свой выход к той же виртуальной земле. Выходное усиление фронтенда задано тщательно подобранными прецизионными резисторами; применяются резисторы с малым ppm и допуском 0,1%, задающие усиление обычно 100×, чтобы даже токи наноамперного уровня давали измеримое напряжение.

Аналоговый фронтенд: MAX4239, шунты и INA333 для трёх диапазонов тока

Аналоговый фронтенд: автозеро-усилитель MAX4239, прецизионные шунты и инструментальный INA333.

Заказная печатная плата#

Для проекта разработана отдельная печатная плата, чтобы прибор получился компактным и надёжным. Разводка сделана в KiCad; все файлы проекта, включая схему и Gerber, есть в репозитории GitHub по ссылке в конце статьи. Плата размером примерно 45×65 мм подходит и для настольного использования, и для портативного.

Ниже — верхний и нижний слои платы:

Верхний и нижний слои печатной платы микроамперметра

Верхний и нижний слои платы.

После проверки и финализации файлы отправили на изготовление.

Изготовленные печатные платы микроамперметра

Готовые изготовленные платы.

Пайка и сборка платы#

Сборка требует внимательности, особенно при работе с чувствительными аналоговыми компонентами. Начали с сортировки всех деталей из BOM. Каждую деталь вручную размещали и паяли на плату. Для более быстрого и стабильного результата можно использовать SMD-трафарет для нанесения паяльной пасты, затем расстановку компонентов и оплавление феном или в печи.

Ниже — полностью собранная плата (обратная сторона):

Собранная плата микроамперметра, обратная сторона

Собранная плата, обратная сторона.

Возле входов видны навесные (bodge) перемычки — на первой ревизии платы не хватало одной связи. Не переживайте: в предоставленных файлах платы это уже исправлено. Готовая сборка с OLED-дисплеем:

Собранная плата микроамперметра с OLED-дисплеем, лицевая сторона

Собранная плата с OLED-дисплеем.

Корпус под 3D-печать#

Чтобы аккуратно разместить схему и упростить обращение с прибором, разработан корпус под 3D-печать. STL-файлы корпуса лежат в том же репозитории GitHub, что и код с файлами платы.

Вот 3D-модель корпуса:

3D-модель корпуса микроамперметра

3D-модель корпуса.

А это напечатанные детали:

Напечатанные на 3D-принтере детали корпуса

Напечатанные детали корпуса.

Финальная сборка#

Когда все детали готовы, можно собирать прибор. Сначала вставьте колпачок тактовой кнопки в напечатанную рамку, затем — полностью собранную плату. Прикрутите плату к передней рамке винтами M2 длиной 4 мм. Подключите аккумулятор и закройте заднюю панель — она защёлкивается в корпусе. После этого установите спереди колпачок движкового переключателя на селектор диапазона.

Плата, закреплённая в корпусе:

Плата, закреплённая внутри напечатанного корпуса

Плата, закреплённая в корпусе.

Лицевая сторона с установленными колпачками переключателей:

Лицевая сторона корпуса с OLED и колпачками переключателей

Лицевая сторона собранного корпуса.

И вот полностью собранный прецизионный цифровой микроамперметр:

Полностью собранный прецизионный цифровой микроамперметр на STM32

Готовый прибор.

Прошивка на Arduino для STM32#

Прошивка написана на C++, совместимом с Arduino, и загружается в STM32G071KBU через Arduino IDE с поддержкой плат STM32. Для OLED-дисплея 128×64 используется библиотека U8g2 для вывода текста и графики. Код занимается сбором данных с АЦП, вычислением напряжений, обновлением дисплея и программным выключением по кнопке.

Основной скетч начинается с настройки АЦП для чтения напряжений в ключевых точках фронтенда и перевода их в измерения тока. Он также снимает напряжение аккумулятора и в реальном времени выводит на OLED ток и уровень заряда. Дисплей обновляется каждые 500 мс, а показания АЦП передискретизируются для лучшего разрешения.

#include <Wire.h>
#include <HardwareSerial.h>
#include <U8g2lib.h>
// Define our custom HardwareSerial
HardwareSerial MySerial(PB6, PB7);  // TX, RX
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
#define LATCH_PIN PA4
#define NUM_SAMPLES 2
#define SAMPLE_DELAY 10
#define REF_VOLTAGE 1249.3f  // float for precision
#define EMA_ALPHA 0.2f  // Change this to adjust smoothing strength (e.g., 0.1 to 0.3)
volatile bool buttonPressed = false;
volatile bool buttonReleased = false;
volatile unsigned long lastInterruptTime = 0;
unsigned long releaseTime = 0;
const unsigned long debounceDelay = 50;
const unsigned long shutdownDelay = 1000;
unsigned long lastBatteryUpdate = 0;
float batteryVoltage = 0;
int AmpV = 0;
float AmpVF = 0;
int batteryPercent = 0;

Сначала подключаются нужные библиотеки для I2C, логирования в порт и вывода на OLED. Пользовательский аппаратный последовательный порт инициализируется на пинах PB6 (TX) и PB7 (RX), а OLED настраивается через U8g2 в режиме полного буфера по I2C. Заданы несколько глобальных констант: опорное напряжение (REF_VOLTAGE), пин фиксации и параметры передискретизации. Глобальные переменные хранят состояние кнопки, уровень заряда и вычисленные показания тока.

void buttonISR() {
unsigned long now = millis();
if (now - lastInterruptTime < debounceDelay) return;
if (digitalRead(LATCH_PIN) == LOW) {
  buttonPressed = true;
  buttonReleased = false;
} else {
  if (buttonPressed) {
    buttonReleased = true;
    releaseTime = now;
    delay(500);
    pinMode(LATCH_PIN, OUTPUT);
    digitalWrite(LATCH_PIN, LOW);
    while (1)
      ;
  }
  buttonPressed = false;
}
lastInterruptTime = now;

Обработчик прерывания buttonISR() реализует soft-latch выключение. Он срабатывает при любом изменении на пине фиксации. Если кнопка нажата (уровень LOW), выставляется флаг buttonPressed. Когда кнопку отпускают (HIGH) и флаг нажатия был установлен, подтверждается отпускание и начинается выключение. Затем пин фиксации переключается в выход и подаёт низкий уровень, обрубая питание.

void setup() {
pinMode(LATCH_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(LATCH_PIN), buttonISR, CHANGE);
MySerial.begin(115200);
MySerial.println("STM32G071 ADC with External Reference (PA2)");
analogReadResolution(12);
u8g2.begin();
}

В setup() пин фиксации настраивается как вход с подтяжкой, к нему привязывается прерывание. Последовательный вывод стартует на 115200 бод. Разрешение АЦП задано 12 бит — под нативное разрешение АЦП STM32. Наконец, инициализируется OLED вызовом u8g2.begin().

void loop() {
unsigned long now = millis();
if (now - lastBatteryUpdate > 500) {
  float vdda = measureVDDA();
  batteryVoltage = measureInputVoltage(PA3, vdda) * 2.0f;
  float Vref = measureInputVoltage(PA1, vdda);
  float ampVoltage = measureInputVoltage(PA0, vdda);
  //AmpV = ((int)((ampVoltage - Vref) / 2.0f)) - 3;
  //float error_percent = 4.77f;
  //AmpV = (int)(((ampVoltage - Vref) / 2.0f) * (1.0f - error_percent / 100.0f)) - 3;
  float raw = ((ampVoltage - Vref) / 1.0f);
  float offset = 6.0f;
  float error_percent = 0.0f;
  //AmpV = (int)((raw - offset) * (1.0f - error_percent / 100.0f));
  AmpVF = (raw  * (1.0f - error_percent / 100.0f) - offset);
  AmpV = (int)AmpVF;
  batteryPercent = batteryLevelPercent((int)batteryVoltage);
  MySerial.print("VDDA: "); MySerial.print(vdda);
  MySerial.print(" mV, Vref: "); MySerial.print(Vref);
  MySerial.print(" mV, PA2: "); MySerial.print(measureInputVoltage(PA2, vdda));
  MySerial.print(" mV, Amp: "); MySerial.println(AmpV);
  lastBatteryUpdate = now;
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_5x8_tr);
  int iconX = 96, iconY = 0, iconW = 28, iconH = 12;
  char batteryPercentStr[6];
  itoa(batteryPercent, batteryPercentStr, 10);
  strcat(batteryPercentStr, "%");
  u8g2.drawStr(iconX + 20, iconY + 10, batteryPercentStr);
  drawBatteryIconWithBars(iconX, iconY, iconW, iconH, batteryPercent);
  char AmpStr[7];
  if (AmpV < 0) {
    sprintf(AmpStr, " %04d", abs(AmpV));
  } else {
    sprintf(AmpStr, " %04d", AmpV);
  }
  u8g2.setFont(u8g2_font_inb33_mn);
  u8g2.drawStr( 10, 62, AmpStr);
  u8g2.sendBuffer();
}
}

Функция loop() проверяет по millis(), прошло ли 500 мс с последнего обновления. Если да — начинается новый набор измерений. Сначала measureVDDA() определяет фактическое VDDA по известному опорному напряжению. Затем measureInputVoltage(PA3, vdda) снимает напряжение батареи (умножается на 2 из-за делителя). Опорное напряжение датчика тока читается с PA1, измеряемый сигнал — с PA0. Сырая разность масштабируется и корректируется смещением для компенсации дрейфа нуля. Процент заряда вычисляется функцией batteryLevelPercent(). Дисплей очищается и обновляется текущим показанием и статусом батареи.

float measureVDDA() {
uint32_t sum = 0;
for (int i = 0; i < NUM_SAMPLES; i++) {
  sum += oversampledADC(PA1);
  delayMicroseconds(SAMPLE_DELAY);
}
float adcAvg = (float)sum / NUM_SAMPLES;
float vdda = (REF_VOLTAGE * 65535.0f) / adcAvg;
return vdda;
}

measureVDDA() читает значение АЦП с внешнего прецизионного опорника на PA1, усредняет NUM_SAMPLES передискретизированных отсчётов и вычисляет фактическое VDDA по известному опорному значению (1,2493 В). Это корректирует колебания напряжения питания, напрямую влияющие на показания АЦП.

float measureInputVoltage(uint32_t pin, float vdda) {
uint32_t sum = 0;
for (int i = 0; i < NUM_SAMPLES; i++) {
  sum += oversampledADC(pin); 
  delayMicroseconds(SAMPLE_DELAY);
}
float adcAvg = (float)sum / NUM_SAMPLES;
float voltage = (adcAvg * vdda) / 65535.0f;
return voltage;
}

measureInputVoltage() берёт любой аналоговый пин и текущее VDDA, выполняет NUM_SAMPLES передискретизированных чтений и возвращает фактическое напряжение на пине. Она используется для батареи, внутреннего опорника и сигнала с фронтенда, опираясь на oversampledADC() для высокого разрешения.

uint32_t oversampledADC(uint32_t pin) {
uint32_t sum = 0;
const int oversample_count = 256;  // 2^(2 × (16 - 12)) = 256
for (int i = 0; i < oversample_count; i++) {
  sum += analogRead(pin);  // 12 bit reading
  delayMicroseconds(SAMPLE_DELAY);   // optional: allow settling
}
// Average and scale to 16 bit range
uint32_t avg = sum / oversample_count;
uint32_t adc16 = avg << 4;  // 12 bit to 16 bit shift (multiply by 16)
// Ignore 3 LSBs
const int ignore_lsb = 4;
adc16 = (adc16 >> ignore_lsb) << ignore_lsb;
return adc16;
}

oversampledADC() реализует 256-кратное передискретизирование, расширяя нативное 12-битное разрешение АЦП STM32 примерно до 16 бит. Она суммирует 256 отсчётов, усредняет их и сдвигает влево на 4 бита до 16-битного диапазона, затем отбрасывает младшие биты для снижения шума. Метод даёт более стабильные показания — что особенно важно для малых сигналов вроде наноамперных токов.

int batteryLevelPercent(int v_bat) {
int percent = (v_bat - 3000) * 100 / (4200 - 3000);
if (percent > 100) percent = 100;
if (percent < 0) percent = 0;
return percent;
}

batteryLevelPercent() переводит напряжение в милливольтах в процент от 0 до 100, линейно отображая диапазон от 3000 мВ (0%) до 4200 мВ (100%) и ограничивая выход этими пределами.

void drawBatteryIconWithBars(int x, int y, int w, int h, int percent) {
u8g2.drawFrame(x, y, w, h);
u8g2.drawBox(x + w, y + h / 3, 2, h / 3);
int numBars = 4;
int barSpacing = 2;
int barWidth = (w - 6 - (numBars - 1) * barSpacing) / numBars;
int barHeight = h - 4;
int barsToDraw = (percent + 24) / 25;
for (int i = 0; i < barsToDraw; i++) {
  int bx = x + 2 + i * (barWidth + barSpacing);
  u8g2.drawBox(bx, y + 2, barWidth, barHeight);
}
}

drawBatteryIconWithBars() рисует прямоугольную иконку батареи в правом верхнем углу OLED. Справа добавляется небольшой «контакт», а внутри заполняется до четырёх полосок в зависимости от процента заряда (округление каждые 25%). Это наглядно дополняет числовое значение.

Устранение неполадок#

ПроблемаВозможная причинаРешение
Устройство не включаетсяАккумулятор разряжен или отключёнЗарядите через USB-C или проверьте подключение батареи
Нестабильные/шумные показанияДлинные измерительные провода или помехи (EMI)Укоротите провода, увеличьте EMA_ALPHA для большей фильтрации
OLED-дисплей пустПроблема с I2C или неисправность дисплеяПроверьте пайку PA9/PA10, работоспособность дисплея
Не загружается прошивкаПроблема с SWD или выбрана не та платаПроверьте подключение SWD, выберите STM32G071 в Arduino IDE
Аккумулятор не заряжаетсяПлохой USB-кабель или проблема с LTC4054Попробуйте другой кабель USB-C, проверьте пайку зарядника

Применения#

  • Разработка IoT-устройств: характеризация токов сна, активных токов и всплесков при передаче у беспроводных сенсорных узлов, проектов на ESP32/ESP8266 и LoRa-устройств.
  • Оценка времени работы от батареи: сбор и анализ профилей потребления для оценки ожидаемого времени работы портативных изделий.
  • Оптимизация энергопотребления: поиск участков кода с наибольшим потреблением и оптимизация прошивки для более долгой работы от батареи.
  • Анализ режимов сна микроконтроллера: проверка корректного перехода в низкопотребляющие режимы и измерение тока удержания в разных состояниях сна.
  • Оценка микро-сборщиков энергии (energy harvesters): достаточно ли тока для непрерывной работы.
  • Поиск неисправностей: аномальный рост тока может указывать на обрыв или короткое замыкание.
  • Учебные проекты: демонстрация расчёта потребляемого тока и создания маломощных изделий.
  • BLE-устройства и беспроводные модули: оценка тока в режимах рекламы, соединения и сна.

Частые вопросы#

Для чего нужен прецизионный цифровой микроамперметр?

Он измеряет очень малые токи от наноампер до миллиампер. Незаменим при тестировании IoT-устройств с батарейным питанием, измерении тока сна микроконтроллеров, характеризации маломощных датчиков, оптимизации энергоэффективных схем и отладке проблем с потреблением.

Насколько точны такие микроамперметры?

Этот прибор достигает точности ±0,1% благодаря автозеро-усилителям вроде MAX4239 (смещение 0,1 мкВ) и прецизионным инструментальным усилителям. Эффективное 16-битное разрешение за счёт передискретизации АЦП позволяет надёжно измерять токи вплоть до 1 нА. Для лучшей точности прибор нужно калибровать, обеспечить стабильное питание и по возможности спокойную (без помех) обстановку.

Можно ли собрать такой прибор дома?

Да, при базовых знаниях электроники. Понадобится умение паять SMD-компоненты, все детали по BOM, а также разводка собственной платы и печать корпуса на 3D-принтере. В репозитории GitHub есть файлы проекта, прошивка и руководство по сборке.

Как калибровать прибор?

Калибровка сводится к измерению известных источников тока и правке смещения в прошивке. Нужны прецизионные источники тока или стабильные комбинации «резистор — напряжение» для каждого диапазона. Показанное значение сравнивают с эталоном, вычисляют ошибку и обновляют смещение в прошивке. Опорник REF3012 позволяет вычислить VDDA для преобразования АЦП.

Каково время работы от батареи?

Зависит от режима использования и ёмкости аккумулятора. Со стандартной LiPo-батареей на 400 мА·ч прибор работает несколько часов. Soft-latch выключатель продлевает время работы, полностью отключая питание, когда прибор не используется. Для более долгой работы «в поле» возьмите аккумулятор большей ёмкости.

Полный код проекта#

/*
*
* Author: Jobit Joseph
*
*/
#include <Wire.h>
#include <HardwareSerial.h>
#include <U8g2lib.h>
#define LATCH_PIN PA4
#define NUM_SAMPLES 2
#define SAMPLE_DELAY 10
#define REF_VOLTAGE 1249.3f  // float for precision
// Define our custom HardwareSerial
HardwareSerial MySerial(PB6, PB7);  // TX, RX
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
volatile bool buttonPressed = false;
volatile bool buttonReleased = false;
volatile unsigned long lastInterruptTime = 0;
unsigned long releaseTime = 0;
const unsigned long debounceDelay = 50;
const unsigned long shutdownDelay = 1000;
unsigned long lastBatteryUpdate = 0;
float batteryVoltage = 0;
int AmpV = 0;
float AmpVF = 0;
int batteryPercent = 0;
void buttonISR() {
 unsigned long now = millis();
 if (now - lastInterruptTime < debounceDelay) return;
 if (digitalRead(LATCH_PIN) == LOW) {
   buttonPressed = true;
   buttonReleased = false;
 } else {
   if (buttonPressed) {
     buttonReleased = true;
     releaseTime = now;
     delay(500);
     pinMode(LATCH_PIN, OUTPUT);
     digitalWrite(LATCH_PIN, LOW);
     while (1)
       ;
   }
   buttonPressed = false;
 }
 lastInterruptTime = now;
}
void setup() {
 pinMode(LATCH_PIN, INPUT_PULLUP);
 attachInterrupt(digitalPinToInterrupt(LATCH_PIN), buttonISR, CHANGE);
 MySerial.begin(115200);
 MySerial.println("STM32G071 ADC with External Reference (PA2)");
 analogReadResolution(12);
 u8g2.begin();
}
void loop() {
 unsigned long now = millis();
 if (now - lastBatteryUpdate > 500) {
   float vdda = measureVDDA();
   batteryVoltage = measureInputVoltage(PA3, vdda) * 2.0f;
   float Vref = measureInputVoltage(PA1, vdda);
   float ampVoltage = measureInputVoltage(PA0, vdda);
   float raw = ((ampVoltage - Vref) / 1.0f);
   float offset = 6.0f;
   float error_percent = 0.0f;
   AmpVF = (raw  * (1.0f - error_percent / 100.0f)- offset);
   AmpV = (int)AmpVF;
   batteryPercent = batteryLevelPercent((int)batteryVoltage);
   MySerial.print("VDDA: "); MySerial.print(vdda);
   MySerial.print(" mV, Vref: "); MySerial.print(Vref);
   MySerial.print(" mV, PA2: "); MySerial.print(measureInputVoltage(PA2, vdda));
   MySerial.print(" mV, Amp: "); MySerial.println(AmpV);
   lastBatteryUpdate = now;
   u8g2.clearBuffer();
   u8g2.setFont(u8g2_font_5x8_tr);
   int iconX = 96, iconY = 0, iconW = 28, iconH = 12;
   char batteryPercentStr[6];
   itoa(batteryPercent, batteryPercentStr, 10);
   strcat(batteryPercentStr, "%");
   u8g2.drawStr(iconX - 20, iconY + 10, batteryPercentStr);
   drawBatteryIconWithBars(iconX, iconY, iconW, iconH, batteryPercent);
   char AmpStr[7];
   if (AmpV < 0) {
     sprintf(AmpStr, "-%04d", abs(AmpV));
   } else {
     sprintf(AmpStr, " %04d", AmpV);
   }
   u8g2.setFont(u8g2_font_inb33_mn);
   u8g2.drawStr(-10, 62, AmpStr);
   u8g2.sendBuffer();
 }
}
float measureVDDA() {
 uint32_t sum = 0;
 for (int i = 0; i < NUM_SAMPLES; i++) {
   sum += oversampledADC(PA1);
   delayMicroseconds(SAMPLE_DELAY);
 }
 float adcAvg = (float)sum / NUM_SAMPLES;
 float vdda = (REF_VOLTAGE * 65535.0f) / adcAvg;
 return vdda;
}
float measureInputVoltage(uint32_t pin, float vdda) {
 uint32_t sum = 0;
 for (int i = 0; i < NUM_SAMPLES; i++) {
   sum += oversampledADC(pin);  
   delayMicroseconds(SAMPLE_DELAY);
 }
 float adcAvg = (float)sum / NUM_SAMPLES;
 float voltage = (adcAvg * vdda) / 65535.0f;
 return voltage;
}
uint32_t oversampledADC(uint32_t pin) {
 uint32_t sum = 0;
 const int oversample_count = 256;
 for (int i = 0; i < oversample_count; i++) {
   sum += analogRead(pin);
   delayMicroseconds(SAMPLE_DELAY);
 }
 uint32_t avg = sum / oversample_count;
 uint32_t adc16 = avg << 4;
 const int ignore_lsb = 4;
 adc16 = (adc16 >> ignore_lsb) << ignore_lsb;
 return adc16;
}
int batteryLevelPercent(int v_bat) {
 int percent = (v_bat - 3000) * 100 / (4200 - 3000);
 if (percent > 100) percent = 100;
 if (percent < 0) percent = 0;
 return percent;
}
void drawBatteryIconWithBars(int x, int y, int w, int h, int percent) {
 u8g2.drawFrame(x, y, w, h);
 u8g2.drawBox(x + w, y + h / 3, 2, h / 3);
 int numBars = 4;
 int barSpacing = 2;
 int barWidth = (w - 6 - (numBars - 1) * barSpacing) / numBars;
 int barHeight = h - 4;
 int barsToDraw = (percent + 24) / 25;
 for (int i = 0; i < barsToDraw; i++) {
   int bx = x + 2 + i * (barWidth + barSpacing);
   u8g2.drawBox(bx, y + 2, barWidth, barHeight);
 }
}

Все файлы проекта — прошивка, файлы платы (KiCad, Gerber), STL корпуса и BOM — доступны в репозитории GitHub, ссылку на который автор приводит в оригинале статьи.