
Привет, Хабр! У данного микроконтроллера отсутствует встроенный аналогово-цифровой преобразователь (АЦП), зато есть цифро-аналоговый (ЦАП) и компараторы. Это позволяет использовать 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)
Gudd-Head
04.06.2025 11:27Простейший стабилизатор тока можно собрать на микросхеме LM317 в корпусе TO-220, добавив к ней шунт — резистор сопротивлением 1 Ом
Складывается ощущение, что LM317 в другом корпусе и резистор не 1 Ом не дадут стабилизатор тока или он будет не простейший (надо будет что-то добавить).
Подтяжка инвертирующих входов операционных усилителей к плюсу питания
Долго искал на схеме второй операционник. Не нашёл.
Чтобы уберечь его от пробоя повышенным напряжением на клеммах, добавлен супрессор D2.
Я бы больше боялся выбросов от мотора вентилятора - впараллель даже ёмкости никакой нет.
Осуществить это без задействования вывода RA3/AN3/CMP1, к сожалению, невозможно. Поэтому первую ножку микроконтроллера придётся оставить висящей в воздухе.
Там опорное напряжение не поддаётся на вывод? Обычно так делают чтобы можно было подключить фильтрующий конденсатор.
zatim
04.06.2025 11:27Долго искал на схеме второй операционник. Не нашёл.
Нумерация контактов микросхемы и обозначение "U2A" намекает на то, что использован корпус микросхемы с двумя ОУ. Второй просто не используется.
Я бы больше боялся выбросов от мотора вентилятора
Стандартные вентиляторы обычно маломощные, 0,12 А. Какие уж там от них выбросы можно ждать.
Igrek_L
Есть самодельный разрядник автомобильных и мощных ИБП-шных аккумуляторов. Стабилизацию тока не делал, в качестве нагрузки несколько автомобильных ламп параллельно. Микроконтроллер ESP32 с делителем раз в секунду замеряет напряжение на клеммах и сразу после этого напряжение на ACS712, 30-амперном датчике тока. Измеренное отправляется по MQTT на сервер, где Node Red пишет напряжение и ток в базу Postgres. Сервер не специально под этот проект, это универсальная домашняя система мониторинга/учёта чего угодно. Параллельно идёт накопительный подсчёт отданных ватт-часов, эти значения вместе с током и напряжением также через BLE транслируются. По итогу можно графики строить какие угодно по данным из базы или просто посмотреть, что там с текущей отдачей, любым BLE-сканером в телефоне. Нагрузка подключена через ключ на мощном MOSFET, при снижении напряжения ниже заданного порога нагрузка отключается. Автономно, самим контроллером. Была идея ШИМировать этот ключ и получать тем самым стабилизацию/установку тока, но понял, что разряд стабильным током не даёт преимуществ, точности для оценки, сколько в аккумуляторе осталось «жизни», мне и так хватает, а ток и банальным подключением/отключением лампочек выставляется.