Привет, Хабр! У данного микроконтроллера отсутствует встроенный аналогово-цифровой преобразователь (АЦП), зато есть цифро-аналоговый (ЦАП) и компараторы. Это позволяет использовать PIC16F628A для управления разрядным устройством с довольно продвинутым функционалом.

▍ Постановка задачи


Я занимаюсь свинцово-кислотными аккумуляторами, и однажды мне понадобилось несколько устройств для разряда стабилизированным током до касания определённого уровня напряжения под нагрузкой.

Нагрузки требовались для исследований в области восстановления не сферических аккумуляторных батарей в вакууме, а реальных 12-вольтовых. То есть, состоящих из шести «банок» — свинцово-кислотных ячеек.



Разряд было необходимо производить в двух вариантах: полный до 10.5 вольт и частичный до 12 вольт.

Для электровелосипедных батарей ёмкостью 12 А*ч требовался разрядный ток от 1.2 до 1.4 А, что соответствует мощности тепловыделения до 20 ватт, тогда как автомобильные 60-ампер-часовые аккумуляторы следовало разряжать током 4-6 А. Последнее означало 80 ватт тепловой мощности.



Удовлетворяющие вышеозначенным требованиям электронные нагрузки в продаже имелись, но их стоимость была такой, что у меня отсутствовали как желание, так и возможность приобретения готового варианта.

Зато в закромах валялась кучка радиаторов от микропроцессоров Intel и выходных транзисторов строчной развёртки отечественных телевизоров. Имелись в наличии и опрометчиво купленные микроконтроллеры PIC16F628A без модулей АЦП.



Один из них был успешно использован при создании спидометра с одометром для электромопеда на базе Риги-12. Девять оставшихся ждали своего часа, и он, наконец, пришёл.

▍ Начнём с основного


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

Простейший стабилизатор тока можно собрать на микросхеме LM317 в корпусе TO-220, добавив к ней шунт — резистор сопротивлением 1 Ом, способный рассеивать порядка полутора ватт тепла в продолжительном режиме.



Микросхема поддерживает стабилизированное напряжение 1.25 вольт между выходным и управляющим выводами. 1-омному шунту соответствует разрядный ток 1.25 А, что прекрасно подходит для наших целей.

LM317 содержит встроенную защиту от перегрева, срабатывание которой приведёт к снижению разрядного тока. Как показала практика, в получившейся электронной нагрузке микросхема не перегревается, и заданный разрядный ток остаётся стабильным.

Включать и выключать разрядный ток можно посредством крошечного полевого транзистора IRLML2502 прямо с ножки микроконтроллера. Только необходимо предусмотреть резисторы, подтягивающие затвор к истоку и ограничивающие ток перезарядки затвора.



Последовательный диод служит для защиты схемы от подключения аккумулятора в неверной полярности. А индицирующий о процессе разряда светодиод можно подключить параллельно стабилизатору тока.

▍ Нужно больше мощности


Однако большим автомобильным аккумуляторам требовалось, как минимум, четыре ампера разрядного тока.

В моём распоряжении имелось несколько мощных полевых транзисторов из силовых цепей старых телевизоров. 2 штуки таких прекрасно справятся с 80 ваттами тепла, будучи установленными на один радиатор для Socket 478.



«Телевизионные» транзисторы 3N80 и 9NB50 достались мне в изолированных корпусах TO-220F. Немного работы надфилем, и корпуса стали неизолированными для меньшего теплового сопротивления.

Стабилизатор тока на полевом транзисторе и операционном усилителе — совершенно стандартная схема.



Подтяжка инвертирующих входов операционных усилителей к плюсу питания сделана для гарантированного отключения разрядной нагрузки низким логическим уровнем с выхода микроконтроллера.

Номиналы резисторов выбраны такими, чтобы обеспечить нужный разрядный ток. Микроконтроллер будет питаться от прецизионного стабилизатора напряжения AMS1117-5.0, поэтому опорное напряжение для стабилизатора тока можно брать прямо с цифрового выхода PIC16F628A.

Маленький полевик IRLML2502 отвечает за включение и выключение вентилятора одновременно с нагрузкой. Чтобы уберечь его от пробоя повышенным напряжением на клеммах, добавлен супрессор D2.

▍ Первые сложности


Кроме выхода на включение нагрузки, нам потребуется вход компаратора, трёхразрядный семисегментный индикатор с десятичной точкой и две кнопки. Это 1 + 1 + 8 + 3 + 2 = 15 выводов микроконтроллера.

Их как раз хватит, поскольку у PIC16F628A два полных восьмиразрядных порта, итого 16 сигнальных выводов плюс земля и плюс питания.

Однако коллегам по проекту захотелось использовать кварцевый резонатор, несмотря на прецизионность тактового RC-генератора, встроенного в микроконтроллер. Кварцу требуется две ножки, а 15 уже заняты. Одной не хватает.

Проблема усугубляется тем, что при активации одного встроенного компаратора оба его входа занимают по ножке микроконтроллера.



На неинвертирующий вход я буду подавать опорное напряжение со встроенного ЦАП. Осуществить это без задействования вывода RA3/AN3/CMP1, к сожалению, невозможно. Поэтому первую ножку микроконтроллера придётся оставить висящей в воздухе.

Итак, восьмиразрядный порт B будет отведён под сегменты индикатора с общим катодом. Два вывода порта A забирает кварц, ещё два — компаратор, и ещё один — управление включением нагрузки.

Остаётся три вывода, из которых один — MCLR/Vdd/RA5 — способен работать только на вход. К нему будет подключена одна кнопка.

Вторую кнопку и переключение трёх разрядов светодиодного индикатора надо ухитриться реализовать с использованием всего двух ножек микроконтроллера. К счастью, это вполне возможно.

▍ Разработка принципиальной схемы


Я просто обожаю диодно-транзисторную логику на дискретных компонентах. Транзисторные ключи, зажигающие разряды индикатора, представляют собой инверторы — логические элементы НЕ.



При логической единице на входе инвертора транзистор открывается, и соответствующий разряд индикатора зажигается.

Если открыт транзистор первого или второго разряда, то анод светодиода D7 просаживается до напряжения, недостаточного для открытия D7 и Q5. Третий разряд не светит.

Когда закрыты и Q3, и Q4, светодиод открывается и обеспечивает ток базы Q5. На индикаторе зажигается третий разряд.

Предусматриваем резисторы, ограничивающие токи сегментов.



И, наконец, подключаем микроконтроллер.



Осталось написать программное обеспечение. Для этого я пользуюсь пакетом JAL V.2.

▍ Пишем прошивку


Загружаем библиотеку, соответствующую используемому микроконтроллеру.

include 16f628a

Указываем компилятору, что тактирование будет осуществляться от внешнего 4-мегагерцового кварца. Частота команд получается 1 мегагерц, так как одна команда выполняется за 4 такта.

pragma target clock 4_000_000 -- 4 МГц
pragma target OSC      XT -- внешний пассивный кварц

Настраиваем защиты микроконтроллера.

pragma target BROWNOUT ENABLED
pragma target LVP      DISABLED -- низковольтное программирование запрещено
pragma target CP       DISABLED -- защита от считывания исполняемого кода отключена
pragma target CPD      DISABLED -- защита от считывания EEPROM программатором отключена
pragma target PWRTE    ENABLED -- таймер задержки включения активирован
pragma target MCLR     INTERNAL  -- внутренний сброс

Нашему разрядному устройству потребуется калибровочная константа, учитывающая реальную тактовую частоту и реальный разрядный ток конкретного экземпляра. При прошивке микроконтроллера в EEPROM будет записано значение этой константы по умолчанию.

pragma EEDATA 0xB0, 0x04

▍ Присваиваем имена


Назовём выводы микроконтроллера соответственно возлагаемым на них функциям.

alias digit1_on is   pin_A0 -- зажигает первый разряд индикатора
alias cmp_in    is   pin_A1 -- вход компаратора для контроля напряжения на клеммах АКБ
alias vrefout   is   pin_A2 -- выход источника опорного напряжения (не используется)
alias dscg_on   is   pin_A3 -- включает разрядный ток
alias digit2_on is   pin_A4 -- зажигает второй разряд
alias btn_minus is   pin_A4 -- кнопка «минус»
var bit minus_dir at TRISA:4 -- переключение режима вывода: 1 - вход, 0 - выход
alias btn_plus  is   pin_A5 -- кнопка «плюс»
alias clkout    is   pin_A6 -- выход тактового генератора на кварц
alias clkin     is   pin_A7 -- вход с кварца
alias dp_on     is   pin_B2 -- зажигает десятичную точку

▍ Кодировка символов


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

const byte segments [22] = 
-- Здесь должна быть открывающая фигурная скобка, но она делает текст ниже серым.
--  -A-
-- F   B
--  -G-
-- E   C
--  -D-
--FADECpGB -- адреса выводов, зажигающих сегменты
0b11111001, -- 0  ABCDEF
0b00001001, -- 1  BC
0b01110011, -- 2  ABDEG
0b01101011, -- 3  ABCDG
0b10001011, -- 4  BCFG
0b11101010, -- 5  ACDFG
0b11111010, -- 6  ACDEFG
0b01001001, -- 7  ABC
0b11111011, -- 8  ABCDEFG
0b11101011, -- 9  ABCDFG
0b01111010, -- 10 ACDEG
0b10011001, -- 11 BCFE
0b01110000, -- 12 ADE
0b01111000, -- 13 ACDE
0b00011010, -- 14 буква "n" CEG
0b11010010, -- 15 - буква "f" AEFG
0b00000000, -- 16 - пробел
0b11010000, -- 17 - буква "r" AEF
0b00111011, -- 18 - буква "d" BCDEG
0b10101011, -- 19 - буква "y" BCDFG
0b10110010, -- 20 - буква "t" DЕFG
0b11110000  -- 21 - буква "c" ADEF
}

Для удобства программирования создадим константы, соответствующие адресам символов.

const letter_o = 0
const letter_s = 5
const letter_n = 14
const letter_f = 15
const space    = 16
const letter_r = 17
const letter_d = 18
const letter_y = 19
const letter_t = 20
const letter_c = 21

▍ Объявляем переменные


var volatile byte sec_100 = 0 -- сотни секунд
var volatile byte sec_10  = 0 -- десятки секунд
var volatile byte sec_1   = 0 -- единицы секунд
var volatile byte ah_1    = 0 -- единицы А*ч
var volatile byte ah_10   = 0 -- десятые доли А*ч
var volatile byte ah_100  = 0 -- сотые доли А*ч
var volatile byte ind1 -- что выводится на первый разряд индикатора
var volatile byte ind2 -- на второй разряд
var volatile byte ind3 -- на третий разряд
var volatile word current -- ток разряда в миллиамперах
var byte current_byte[2] at current
var volatile word current_old -- старое значение
var volatile word mas = 0 -- счётчик миллиампер*секунд
var volatile byte scaler = 0 -- делитель 250 герц на 125
var volatile byte idle = 0    -- счётчик времени между событиями
var volatile byte minus_count = 0 -- счётчик времени удержания кнопки «минус»
var volatile byte threshold      -- порог завершения разряда
var volatile byte threshold_old  -- старый порог завершения разряда
var volatile byte thr_ind       -- индикация порога завершения разряда
var volatile byte ind_off_count = 0 -- Счётчик секунд до отключения индикатора
var bit ind_off_count_64 at ind_off_count:6 -- 64 секунды
var volatile byte cont_count = 0 -- Счётчик секунд до возобновления разряда
var bit cont_count_64 at cont_count:6 -- 64 секунды

▍ Флаги


Эти булевы переменные отражают состояние асинхронного автомата. В однобайтовой переменной может храниться восемь флагов.

var volatile byte flags = 0 
var bit digit1_next   at flags:0 -- индикация будет передана старшему разряду
var bit digit2_next   at flags:1 -- индикация будет передана среднему разряду
var bit dp_1          at flags:2 -- десятичная точка после старшего разряда
var bit dp_2          at flags:3 -- десятичная точка после среднего разряда
var bit second        at flags:4 -- прошла одна секунда
var bit minus_pressed at flags:5 -- нажата кнопка «минус»
var bit plus_pressed  at flags:6 -- нажата кнопка «плюс»
var bit reset_ok      at flags:7 -- ёмкость сброшена

var volatile byte flags1 = 0
var bit clock_stop    at flags1:0 -- секундомер остановлен
var bit calibr_mode   at flags1:1 -- режим калибровки
var bit minus_detect  at flags1:2 -- подавление дребезга кнопки «-»
var bit plus_detect   at flags1:3 -- антидребезг кнопки «+»
var bit even          at flags1:4 -- делитель 2 герц на 2
var bit minus_hold    at flags1:5 -- удерживалась кнопка «-»
var bit threshold_set at flags1:6 -- режим переключения порогов
var bit dscg_finished at flags1:7 -- разряд завершён

var volatile byte flags2 = 0
var bit ind_off       at flags2:0 -- индикатор выключен
var bit cmp_rdy       at flags2:1 -- компаратор сработал
var bit continue      at flags2:2 -- продолжать разряд
var bit cmp_state     at flags2:3 -- отображать состояние компаратора на индикаторе
var bit no_ind        at flags2:4 -- работать без индикации

▍ Выбор порога завершения разряда


Данное разрядное устройство реализует восемь предустановок напряжения завершения разряда.

Встроенный цифро-аналоговый преобразователь позволяет устанавливать опорное напряжение компаратора в виде определённой доли напряжения питания микроконтроллера. За это отвечает управляющий регистр VRCON.

const byte vrc [8] = 
-- Здесь должна быть открывающая фигурная скобка. 
0b11100100, --  6 вольт
0b11100101, --  7.5 В
0b11100110, --  9 В
0b11000001, -- 10.125 В
0b11100111, -- 10.5 В
0b11000010, -- 11.25 В
0b11101000, -- 12 В
0b11000010  -- 12.375 В
}
VRCON    =   0b11100100

При выборе порога завершения разряда с помощью кнопки на индикатор должно выводиться цифровое значение задаваемого напряжения в вольтах.

const byte thr [8] = { 
60,75,90,101,105,0,120,124
}

procedure indicate_thr is -- отображение порога 
thr_ind = thr[threshold]
dp_1 = off
dp_2 = on
ind1 = thr_ind / 100
ind2 = (thr_ind % 100) / 10 -- остаток от деления на 100, делённый на 10
ind3 = thr_ind % 10 -- остаток от деления на 10
if (thr_ind ==0) then
  ind1 = 11
  ind2 = 2
  ind3 = 5
  dp_1 = on
  dp_2 = off
end if
end procedure

▍ Настраиваем периферию


Биты в управляющих регистрах TRISA и TRISB задают режим работы выводов соответствующих портов. Единица означает вход, ноль — выход.

Запись в регистры PORTA и PORTB приводит к изменению состояния тех ножек портов, что настроены на выход.

TRISA    =   0b11110110
TRISB    =   0b00000000
PORTA    =   0b00000000
PORTB    =   0b00000000

Содержимое регистра CMCON определяет конфигурацию компараторов. Нам требуется один независимый компаратор.

CMCON    =   0b00000101
alias cmp_out is CMCON_C2OUT -- выход компаратора

▍ Динамическая индикация


Благодаря инерции зрения, быстро переключающиеся разряды индикатора воспринимаются как светящиеся постоянно. Это называется динамической индикацией.

В данном разрядном устройстве переключение разрядов осуществляется с частотой 250 герц. Эта же частота используется для подсчёта времени разряда.

T2CON    =   0b00000110 -- предделитель 16, постделитель 1
PR2      =   250 - период таймера

Предделитель делит тактовую частоту 1 мегагерц на 16. Получается приращение счётчика таймера с частотой 62500 герц. Период 250 вызовет переполнение таймера 250 раз в секунду.

▍ Прерывания


Разрешаем прерывания только от TIMER2 и обнуляем регистр с флагами прерываний.

PIE1     =   0b00000010
PIR1     =   0b00000000 
INTCON   =   0b11000000

Пишем процедуру обработчика прерывания, которое будет вызываться таймером 250 раз в секунду.


procedure interrupt is
pragma interrupt
if PIR1_TMR2IF then  
  PIR1_TMR2IF = off -- сбрасываем флаг прерывания от таймера
  if cmp_state then -- выводим на индикатор состояние компаратора
   ind1 = letter_o 
   if cmp_out then  
    ind2 = letter_f 
    ind3 = letter_f 
   else
    ind2 = letter_n
    ind3 = space
   end if
   dp_1 = off
   dp_2 = off
  end if
  no_ind = (ind_off|(continue & even)) -- если индикация отключена или мигает
  if digit1_next then    -- зажигаем первый разряд индикатора
   digit1_next = off
   digit2_next = on
   if no_ind then
    portb = 0
   else
    portb = segments[ind1]
    dp_on = dp_1
   end if
   digit1_on = on
   digit2_on = off
  elsif digit2_next then -- зажигаем второй разряд
   digit1_next = off
   digit2_next = off
   if no_ind then
    portb = 0
   else
    portb = segments[ind2]
    dp_on = dp_2
   end if
   minus_dir = 1
   digit2_on = on
   digit1_on = off
  else                   -- зажигаем третий разряд
   if !btn_minus then -- но сначала проверим, нажата ли кнопка «минус»
    minus_detect = on -- устанавливаем флаг короткого нажатия
   else
    if (minus_count > 3) then
     minus_hold = on -- флаг длинного нажатия кнопки
    end if
    minus_count = 0
   end if
   digit1_next = on
   digit2_next = off
   if no_ind then
    portb = 0
   else
    portb = segments[ind3]
    dp_on = !(dp_2|dp_1)
   end if
    digit1_on = off
    minus_dir = 0 -- переводим ножку в режим выхода
    digit2_on = off
  end if
end if
if !btn_plus then -- если нажата кнопка «плюс»
  plus_detect = on
end if
scaler = scaler + 1 -- счётчик срабатываний прерывания
if (scaler >= 125) then -- прошло полсекунды
  scaler = 0 -- обнуляем
  if plus_detect then -- антидребезг
   plus_detect = off
   plus_pressed = on
  end if
  if minus_detect then
   minus_detect = off
   minus_pressed = on
   minus_count = minus_count + 1
  end if
  even = !even
  if !even then -- прошла целая секунда
   second = on
  end if
end if
end procedure

▍ Индикация ампер*часов


Если вы внимательно изучали таблицу кодировки символов, то могли заметить, что в ней присутствует «цифры» 10 и 11. Благодаря ей, в диапазоне от 10.00 до 11.99 мы можем наблюдать не три, а четыре значащих цифры.

Также в таблице имеются знаки для «цифр» 12 и 13, но в силу трудночитаемости они применены только в режиме калибровки, речь о котором пойдёт ниже.

procedure ah_indicate is
if (ah_1 < 12) then    -- от 0.00 до 11.99 А*ч
  ind1 = ah_1
  ind2 = ah_10
  ind3 = ah_100
  dp_1 = on -- десятичная точка после старшего разряда
  dp_2 = off
elsif (ah_1 < 99) then -- от 12.0 до 99.9
  ind1 = ah_1 / 10
  ind2 = ah_1 % 10 -- остаток от деления на 10
  ind3 = ah_10
  dp_1 = off
  dp_2 = on -- десятичная точка после среднего разряда
else                   -- от 100 до 255
  ind1 = ah_1 / 100
  ind2 = (ah_1 % 100) / 10
  ind3 = ah_1 % 10
  dp_1 = off -- десятичная точка после младшего разряда
  dp_2 = off
end if
end procedure

▍ Сохранение А*ч в EEPROM


Отданная аккумулятором ёмкость запоминается в энергонезависимой памяти. То есть, если вы прервали разряд и отключили питание нагрузки, разряд можно будет возобновить с того же места после переподключения аккумулятора.

procedure save_1 is -- единицы
INTCON_GIE = 0 -- запрещаем прерывания перед записью
EEADR = 3
EEDATA = ah_1
EECON1_WREN = 1
EECON2 = 0x55
EECON2 = 0xAA
EECON1_WR = 1
while EECON1_WR loop
end loop
EECON1_WREN = 0
INTCON_GIE = 1
end procedure

procedure save_10 is -- десятые доли 
INTCON_GIE = 0
EEADR = 4
EEDATA = ah_10
EECON1_WREN = 1
EECON2 = 0x55
EECON2 = 0xAA
EECON1_WR = 1
while EECON1_WR loop
end loop
EECON1_WREN = 0
INTCON_GIE = 1
end procedure

procedure save_100 is -- сотые доли 
INTCON_GIE = 0
EEADR = 5
EEDATA = ah_100
EECON1_WREN = 1
EECON2 = 0x55
EECON2 = 0xAA
EECON1_WR = 1
while EECON1_WR loop
end loop
EECON1_WREN = 0
INTCON_GIE = 1
end procedure

▍ Подсчёт ампер*часов


Один А*ч равняется 60*60 = 3600 ампер*секундам, то есть кулонам. Квантом измерения полезной ёмкости аккумулятора в данной электронной нагрузке является сотая доля ампер*часа. Она равна 36 тысячам миллиампер*секунд.

procedure dscg_count is
pragma inline
mas = mas + current
if (mas >= 36000) then
  mas = mas - 36000
  ah_100 = ah_100 + 1
  if (ah_100 > 9) then
   ah_100 = 0
   ah_10 = ah_10 + 1
   if (ah_10 > 9) then
    ah_10 = 0
    ah_1 = ah_1 + 1
    save_1
   end if
   save_10
  end if
  save_100
end if
end procedure

▍ Режим секундомера


Данный режим активируется подачей питания при зажатой кнопке «минус» и служит для калибровки нашей электронной нагрузки.

procedure clock_indicate is -- отображение секунд
pragma inline               
ind1 = sec_100
ind2 = sec_10
ind3 = sec_1
dp_1 = off
dp_2 = off
end procedure

procedure clock_count is -- подсчёт секунд
pragma inline
sec_1 = sec_1 + 1
if (sec_1 > 9) then
  sec_1 = 0
  sec_10 = sec_10 + 1
  if (sec_10 > 9) then
   sec_10 = 0
   sec_100 = sec_100 + 1
   if (sec_100 > 12) then
    sec_100 = 0
   end if
  end if
end if
end procedure

Нажатие кнопки «плюс» запускает и останавливает секундомер, а нажатие «минуса» сбрасывает его на ноль.

procedure stopwatch is 
pragma inline
forever loop
  if second then -- прошла секунда
   second = off
   if !clock_stop then
    clock_count
   end if
   if plus_pressed then
    plus_pressed = off
    minus_pressed = off
    clock_stop = !clock_stop
   end if -- plus_pressed
   if minus_pressed then
    minus_pressed = off 
    sec_1   = 0
    sec_10  = 0
    sec_100 = 0
   end if -- minus_pressed
   clock_indicate
  end if -- second
end loop
end procedure

▍ Калибровка тока и времени


Чтобы рассчитать калибровочную константу, нужно засечь время по эталонным часам и записать число секунд, которое насчитает нагрузка в режиме секундомера.

Также потребуется измерить фактический средний ток данного экземпляра нагрузки в миллиамперах. Его следует умножить на фактическое время и разделить на показание секундомера нагрузки.

Получится калибровочная константа, которую нужно ввести в энергонезависимую память прибора, переведя его в режим калибровки. Для этого подаём питание с зажатой кнопкой «плюс» и увеличиваем или уменьшаем число на индикаторе соответствующими кнопками.

Когда калибровочная константа введена, следует подождать 10 секунд, не отключая питания и не нажимая кнопок. Произойдёт сохранение в EEPROM, по окончании которого число на индикаторе сменится буквами «rdy» от английского ready — «готово».

procedure ma_indicate_1 is --  до 1399 мА для нагрузки на LM317
  ind1 = current / 100
  ind2 = (current % 100) / 10
  ind3 = current % 10
  dp_1 = off
  dp_2 = off
end procedure

procedure ma_indicate_5 is --  до 9.99 мА для нагрузки на MOSFET
  ind1 = current / 1000
  ind2 = (current % 1000) / 100
  ind3 = (current % 100) / 10
  dp_1 = on
  dp_2 = off
end procedure

alias ma_indicate is ma_indicate_5

procedure calibrate is
pragma inline
calibr_mode = on
current_old = current
ma_indicate
while !btn_plus loop
end loop -- ожидание отпускания кнопки «плюс»
plus_pressed = off
second = off
while !second loop
end loop
second = off
while !second loop
end loop
second = off
while !(plus_pressed|minus_pressed) loop
end loop -- ожидание нажатия любой кнопки
plus_pressed = off
minus_pressed = off
while (idle < 11) loop
  if second then -- прошла секунда
   second = off
   idle = idle + 1
  end if
  if plus_pressed then
   plus_pressed = off
   idle = 0
--   if (current < 1398) then
   if (current < 6000) then
    current = current + 1
   else
--    current = 400
    current = 4000
   end if
   ma_indicate
  end if
  if minus_pressed then
   minus_pressed = off
   idle = 0
--   if (current > 401) then
   if (current > 4001) then
    current = current - 1
   else
--    current = 1399
    current = 5999
   end if
   ma_indicate
  end if
end loop
if (current_old != current) then
  INTCON_GIE = 0
  EEADR = 0
  EEDATA = current_byte[0]
  EECON1_WREN = 1
  EECON2 = 0x55
  EECON2 = 0xAA
  EECON1_WR = 1
  while EECON1_WR loop
  end loop
  EEADR = 1
  EEDATA = current_byte[1]
  EECON2 = 0x55
  EECON2 = 0xAA
  EECON1_WR = 1
  while EECON1_WR loop
  end loop
  EECON1_WREN = 0
  INTCON_GIE = 1
end if
ind1 = letter_r -- индикация «rdy»
ind2 = letter_d
ind3 = letter_y
dp_1 = off
dp_2 = off
forever loop
end loop
end procedure

▍ Рабочий режим


Нагрузка запускается в нормальном режиме работы, если при подаче питания не была зажата ни одна из кнопок.

В данном режиме нажатие кнопки «плюс» включает и выключает разрядный ток, тогда как кнопка «минус» переключает пороги завершения разряда.

procedure switch_thr is -- выбор порога
pragma inline
threshold = threshold + 1
if (threshold > 7) then
  threshold = 0
end if
VRCON = vrc[threshold]
indicate_thr
end procedure

Длительное нажатие кнопки «минус» сбрасывает ампер*часы вместе с десятыми и сотыми долями. Успешный сброс индицируется символами «rst», от английского «reset».

procedure reset_ah is
pragma inline
mas    = 0
ah_1   = 0
ah_10  = 0
ah_100 = 0
save_1
save_10
save_100
ind1 = letter_r -- вывод «rst»
ind2 = letter_s
ind3 = letter_t
dp_1 = off
dp_2 = off
reset_ok = on
end procedure

Порог отключения разрядного тока запоминается в EEPROM в момент ручного запуска разряда.

procedure save_thr is
pragma inline
if threshold_old != threshold then
  INTCON_GIE = 0
  EEADR = 2
  EEDATA = threshold
  EECON1_WREN = 1
  EECON2 = 0x55
  EECON2 = 0xAA
  EECON1_WR = 1
  while EECON1_WR loop
  end loop
  EECON1_WREN = 0
  INTCON_GIE = 1
  threshold_old = threshold
end if
end procedure

▍ Калибровка напряжения


При подаче питания с нулевыми значениями слитой ёмкости в EEPROM индикатор показывает «On» или «Off» в зависимости от состояния компаратора, пока не была нажата ни одна из кнопок.

Это нужно для калибровки напряжения. Запитываем нагрузку от лабораторного блока питания и крутим десятиоборотный подстроечный резистор на плате нагрузки до тех пор, пока фактическое напряжение срабатывания компаратора не совпадёт с установленным.

▍ Обработчики кнопок


Спустя минуту после завершения разряда нагрузка автоматически отключает семисегментный индикатор. Чтобы зажечь его снова и наблюдать значение отданных батареей ампер*часов, требуется просто нажать любую кнопку.

procedure plus_btn is  -- кнопка «плюс»
pragma inline
ind_off_count = 0
if ind_off then 
  ind_off = off  
elsif !(cmp_state|threshold_set|reset_ok) then
  if dscg_on then
   dscg_on = off
   dscg_finished = on
  else
   dscg_on = !cmp_out
   if dscg_on then
    dscg_finished = off
    save_thr      -- сохранение порога
   end if -- dscg_on
  end if -- dscg_on
end if -- !ind_off
threshold_set = off
reset_ok = off
cmp_state = off
end procedure

procedure minus_btn is -- кнопка «минус»
pragma inline
reset_ok = off
cmp_state = off
ind_off_count = 0
if ind_off then
  ind_off = off  
else 
  if minus_hold  then   -- долгое нажатие
   minus_hold = off     -- сброс ёмкости
   threshold_set = off
   reset_ah
   reset_ok = on
  elsif (minus_count < 3) then -- короткое нажатие
   indicate_thr              -- переключение порогов
   if threshold_set then
    switch_thr
   end if
   threshold_set = on
  end if
end if -- !ind_off
end procedure

▍ Инициализация


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

EEADR = 0 
EECON1_RD = 1
current_byte[0] = EEDATA
EEADR = 1
EECON1_RD = 1
current_byte[1] = EEDATA

procedure prepare is
pragma inline
EEADR = 2 
EECON1_RD = 1
threshold = EEDATA
threshold_old = threshold
VRCON = vrc[threshold]
cmp_rdy = off

EEADR = 3
EECON1_RD = 1
ah_1 = EEDATA
EEADR = 4
EECON1_RD = 1
ah_10 = EEDATA
EEADR = 5
EECON1_RD = 1
ah_100 = EEDATA

Если в момент подачи питания энергонезависимая память нагрузки содержит ненулевое число отданной аккумулятором ёмкости, то это число будет мигать на индикаторе в течение минуты.

Далее разрядный ток включится автоматически. Нажатие любой кнопки прерывает обратный отсчёт и отменяет автозапуск.

if (ah_1 != 0) then 
  continue = on      
end if
if (ah_10 != 0) then
  continue = on
end if
if (ah_100 != 0) then
  continue = on
end if
cmp_state = !continue
end procedure

▍ Главный цикл


procedure main_cycle is
pragma inline
forever loop
  if second then -- прошла секунда
   second = off
   if !(cmp_state|threshold_set|reset_ok) then
    ah_indicate -- индикация ёмкости
   end if
   if continue then -- разряд был прерван
    cont_count = cont_count + 1 -- подсчёт секунд
    if cont_count_64 then -- по прошествии 64 секунд
     continue = off       -- перезапуск разряда
     dscg_finished = cmp_out
     ind_off_count = 0
     dscg_on = !dscg_finished
    end if
   end if -- continue
   if dscg_finished then -- после завершения разряда
    ind_off_count = ind_off_count + 1 -- 
    if ind_off_count_64 then -- по прошествии 64 секунд
     ind_off_count = 0
     ind_off = on            -- отключение индикации
    end if
   end if -- dscg_finished
   if cmp_rdy then
    if cmp_out then -- завершение разряда по достижении порога
     if dscg_on then
      dscg_on = off   -- отключение разрядного тока
      dscg_finished = on
      ind_off_count = 0
     end if
    end if
   end if -- cmp_rdy
   cmp_rdy = cmp_out
   if dscg_on then
    dscg_count -- подсчёт ёмкости
    ah_indicate -- индикация ёмкости
   end if
   if plus_pressed then
    plus_pressed = off
    continue = off
    minus_pressed = off
    plus_btn
   end if -- plus_pressed
   if minus_pressed then
    minus_pressed = off
    continue = off
    minus_btn
   end if -- minus_pressed
  end if -- second
end loop
end procedure

Один из трёх режимов работы нагрузки запускается в зависимости от кнопки, удерживаемой в момент подачи питания.

if !btn_plus then 
plus_pressed = off
minus_pressed = off
calibrate        -- режим калибровки
elsif !btn_minus then 
stopwatch        -- режим секундомера
else              -- рабочий режим
prepare
main_cycle       
end if

▍ Заработало!


Так из того, что было в наличии, получился вполне работоспособный инструмент для работы со свинцово-кислотными аккумуляторами.



А по данной ссылке вы можете посмотреть чудом сохранившееся видео работы двух вариантов электронной нагрузки — с пассивным охлаждением на LM317 и с вентилятором на полевых транзисторах.

Напишите в комментариях, какие самодельные измерительные приборы, на микроконтроллерах и без них, собирали вы или ваши знакомые.

© 2025 ООО «МТ ФИНАНС»

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (23)


  1. Igrek_L
    04.06.2025 11:27

    Есть самодельный разрядник автомобильных и мощных ИБП-шных аккумуляторов. Стабилизацию тока не делал, в качестве нагрузки несколько автомобильных ламп параллельно. Микроконтроллер ESP32 с делителем раз в секунду замеряет напряжение на клеммах и сразу после этого напряжение на ACS712, 30-амперном датчике тока. Измеренное отправляется по MQTT на сервер, где Node Red пишет напряжение и ток в базу Postgres. Сервер не специально под этот проект, это универсальная домашняя система мониторинга/учёта чего угодно. Параллельно идёт накопительный подсчёт отданных ватт-часов, эти значения вместе с током и напряжением также через BLE транслируются. По итогу можно графики строить какие угодно по данным из базы или просто посмотреть, что там с текущей отдачей, любым BLE-сканером в телефоне. Нагрузка подключена через ключ на мощном MOSFET, при снижении напряжения ниже заданного порога нагрузка отключается. Автономно, самим контроллером. Была идея ШИМировать этот ключ и получать тем самым стабилизацию/установку тока, но понял, что разряд стабильным током не даёт преимуществ, точности для оценки, сколько в аккумуляторе осталось «жизни», мне и так хватает, а ток и банальным подключением/отключением лампочек выставляется.


  1. Gudd-Head
    04.06.2025 11:27

    Простейший стабилизатор тока можно собрать на микросхеме LM317 в корпусе TO-220, добавив к ней шунт — резистор сопротивлением 1 Ом

    Складывается ощущение, что LM317 в другом корпусе и резистор не 1 Ом не дадут стабилизатор тока или он будет не простейший (надо будет что-то добавить).

    Подтяжка инвертирующих входов операционных усилителей к плюсу питания

    Долго искал на схеме второй операционник. Не нашёл.

    Чтобы уберечь его от пробоя повышенным напряжением на клеммах, добавлен супрессор D2.

    Я бы больше боялся выбросов от мотора вентилятора - впараллель даже ёмкости никакой нет.

    Осуществить это без задействования вывода RA3/AN3/CMP1, к сожалению, невозможно. Поэтому первую ножку микроконтроллера придётся оставить висящей в воздухе.

    Там опорное напряжение не поддаётся на вывод? Обычно так делают чтобы можно было подключить фильтрующий конденсатор.


    1. zatim
      04.06.2025 11:27

      Долго искал на схеме второй операционник. Не нашёл.

      Нумерация контактов микросхемы и обозначение "U2A" намекает на то, что использован корпус микросхемы с двумя ОУ. Второй просто не используется.

      Я бы больше боялся выбросов от мотора вентилятора

      Стандартные вентиляторы обычно маломощные, 0,12 А. Какие уж там от них выбросы можно ждать.


      1. Va_sil
        04.06.2025 11:27

        Я тоже так думал пока не попытался управлять оборотами вентилятора с помощью мосфета шимом , управлялось плохо , и я подключил ослик. Очень познавательное зрелище , рекомендую.


        1. zatim
          04.06.2025 11:27

          Какая частота шима была?


          1. randomsimplenumber
            04.06.2025 11:27

            Какая частота

            Вангую 500 ардкиновских гц.


        1. randomsimplenumber
          04.06.2025 11:27

          ослик

          Э.. О_о