Прикуривать вредно, или как сохранить заряд автомобильного аккумулятора

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

_pptzvndfwivnln2xsqz7aae224.jpeg
Одно из возможных последствий неконтролируемого разряда.
Покупка первого автомобиля или мотоцикла является значимой вехой в жизни каждого человека, и особенно — инженера. Ведь кто еще кроме очевидных преимуществ своего нового железного коня сразу же обращает внимание и на неочевидные его недостатки? Кто тут же начинает размышлять на предмет всяческих усовершенствований и дополнений к стандартной комплектации? Конечно, если это автомобиль из верхнего сегмента, да еще и «модной» марки, то сначала может показаться, что в нем есть абсолютно все. Но как показывает практика, и в этом случае время опровергает первые впечатления. Если же покупается машина эконом-класса, то тут руки начинают чесаться буквально в первый же день!

Желание по-максимуму «нафаршировать» свой автомобиль разнообразными вспомогательными электронными устройствами вполне естественно. Однако вскоре после реализации всех этих планов, жизнь сталкивает автовладельца с суровой реальностью. Оказывается, что даже самые современные, построенные на новейшей элементарной базе, устройства все равно довольно охочи до электроэнергии. А кажущаяся такой огромной автомобильная АКБ — вовсе не ядерный реактор и легко может «присесть» под тяжестью всех этих безобидных на первый взгляд потребителей за считанные дни.

Чтобы не растекаться далее абстрактными и гипотетическими ситуациями, перейду сразу к своей истории. После покупки автомобиля, первым было желание поставить в него регистратор. Это и было сделано в течение минимально возможного срока, практически полностью продиктованного скоростью доставки посылки с Aliexpress. Понятно, что штатное питание от прикуривателя было крайне неудобным, и регистратор быстро получил стационарное подключение к ближайшей линии бортовой сети через импульсный преобразователь 12/5v. А т.к. дело было, мягко говоря, не вчера, преобразователь этот был не чета современным, на свои собственные нужды, как оказалось впоследствии, он жрал целых 21 mА тока. Теперь прикинем, сколько могла кормить один только этот преобразователь новая и полностью заряженная АКБ емкостью 60 А·ч. Арифметика крайне простая и неутешительная.

ojhsie9i9gzujcnzgkfeq7rxydq.png


Вот так за три месяца не нагруженный ничем преобразователь высадит аккумулятор буквально «в ноль». Если же учесть, что у не совсем свежей АКБ емкость легко может оказаться вдовое меньшей, а заряд после городских покатушек — далеко не 100%, черный день легко наступает уже через один месяц.

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

Идем далее, сам регистратор в режиме записи FHD@30fps потребляет от источника +5v почти 300 mA, что после преобразования с учетом КПД дает около 150 mА тока из бортовой сети. Допустим, что преобразователь заменен на современный, и посчитаем время разряда только этим током.

ufp8fldn4t5bfzpl8u8udb6fd3o.png


Чуть более двух недель, а на практике — дней десять. Теперь перспектива прикуривать (а возможно, и менять АКБ) неиллюзорно маячит после ближайшего отпуска или командировки.

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

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

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

В общем, за несколько недель пассивных раздумий окончательно оформилась идея устройства, которое должно контролировать напряжение бортовой сети и на основе этих данных управлять подачей питания на две группы потребителей: второстепенные (регистратор, USB-розетка) и основные (GPS-трекер и еще кое-что).

Как это могло быть сделано


Первые виртуальные прототипы устройства были «построены» на базе аналоговых компараторов LM393N и умели все, что изначально планировалось получить от устройства. Абстрактная схема получалась примерно такой.

cflaybsmn6m3e6o7mshcfb_1j20.png


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

0m77ewzvozy52h6zr-fg7uaivg0.png


Первичный ключ держится включенным дольше, чем вторичный, поэтому через него запитан сам понижающий преобразователь. Непосредственно к преобразователю подключены первичные нагрузки. Вторичный ключ коммутирует второстепенные нагрузки уже в цепи +5v на выходе преобразователя.

Что вышло в итоге


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

В качестве сердца девайса просто напросился контроллер ATtiny13A, который кроме простоты в обращении и дешевизны, все еще выпускается в приятном для олдфага теплом ламповом DIP-корпусе. Изначально возможности даже такого маленького контроллера казались избыточными по всем фронтам, от количества входов/выходов до объема программной и оперативной памяти, однако аппетит, как известно, приходит во время еды. В результате, забегая вперед, скажу, что в конечном варианте при деле оказались все выводы микросхемы, а свободной программной памяти осталось не более двух десятков байт.

Для измерения напряжения бортовой сети от микроконтроллера требовался всего один вход, имеющий привязку к АЦП. Еще два логических выхода должны были управлять потребителями. В первую очередь после окончательного мысленного перехода на «цифру» возникло желание приспособить к делу два свободных GPIO, и решение не заставило себя долго ждать. Когда в очередной раз на морозе стартер крутил двигатель с плохо скрываемым надрывом, наличие термодатчика в схеме и алгоритме показалось очень кстати. В итоге второй АЦП был использован для измерения температуры. А чтобы терморезистор потреблял ток только тогда, когда это нужно, запитать его было решено от последнего оставшегося логического выхода.

В итоге схема устройства приобрела такой вот окончательный вид.

jhctdt_pol1pk6xum1wwn_n7jzo.png


Тут мы видим самый минимум деталей, и среди них ничего не подлежит какому бы то ни было «подкручиванию». Пробежимся кратко по основным моментам.

Для питания контроллеру нужно стабильное напряжение от 1.8 до 5.5 В, значит, в схеме должен быть стабилизатор, который понизит напряжение бортовой сети до необходимого уровня. С точки зрения экономии энергии может показаться, что тут место исключительно импульсному stepdown-преобразователю, но это только на первый взгляд. Дело в том, что ATtiny13A даже в самом-самом энергоемком режиме работы (частота 8 МГц, активное выполнение кода) потребляет не более 6 mА. В данной схеме же контроллер 99% времени находится в режиме глубокого сна и к тому же работает на частоте 1.2 МГц, в результате чего среднее потребление составляет ориентировочно менее 15 µА. Еще на малую долю секунды активируется питание терморезистора, что добавляет к среднему току около 25 микроампер. И вот ответ на вопрос «стоит ли ради нагрузки с потреблением не более 40 µA городить импульсный преобразователь?» кажется уже не столь однозначным. А если учесть, то мы имеем дело с аналоговыми измерениями, то однозначно не стоит. Поэтому был применен линейный стабилизатор LP2950, функциональный аналог популярного 78L05, но гораздо более экономичный. Этот преобразователь может дать до 100 mA тока на выходе, потребляя при этом на себя любимого не более 75 µA.

Делитель напряжения бортовой сети, защищенный стабилитроном и конденсатором, позволяет измерять напряжения до 15 В.

Я знаю, что сейчас на меня обрушится волна критики за такое решение, но будем объективны. Во-первых, не спутник разрабатываю, а во-вторых, нет такого одиночного фактора, который привел бы к катастрофе. Сопротивления плечей высокие, стабилитрон способен отвести гораздо больший ток, чем тот, который может потечь через делитель даже в самом пессимистическом сценарии. От высокочастотных импульсов, когда стабилитрону не хватает быстродействия, защитит конденсатор C2 (с резистором R7 создает ФНЧ с частотой среза всего 7 Гц). D1 и R6 в какой-то мере страхуют схему от отвала друг друга. Да и про линейность не нужно забывать, любой способ гальванической развязки в таком месте сделает теоретический расчет величин совершенно нереальным, придется калибровать как минимум прототип, а нам это не нужно.


Выходное сопротивление делителя в десять раз выше, чем рекомендуемые 10 кОм для источника сигнала АЦП, но благодаря конденсатору C2 проблем с измерениями не возникает.

Вообще, входное сопротивление цепей АЦП контроллеров AVR по даташиту заявлено не менее 100 МОм. Однако, тем не менее, тот же даташит рекомендует использовать источники с внутренним сопротивлением до 10 кОм. Почему так? Дело в принципе работы этого самого АЦП. Сам преобразователь работает по принципу последовательного приближения, а его входная цепь представляет собой ФНЧ из резистора и конденсатора. Получение 10-битного семпла производится итерационно и нужно, чтобы в течение всего времени измерения конденсатор был заряжен до полного измеряемого напряжения. Если выходное сопротивление источника слишком велико, то конденсатор будет продолжать заряжаться прямо в процессе преобразования и результат получится неточным. В нашем случае емкость C2 более чем в семь тысяч раз превышает емкость фильтра АЦП, а это значит, что при перераспределении заряда между этими конденсаторами при их параллельном включении в момент измерения, входное напряжение снизится не более чем на 1/7000, что в семь раз меньше предельной точности 10-битного АЦП. Правда, нужно иметь в виду, что работает такой трюк только для одиночных измерений со значительными паузами между ними, поэтому не стоит «улучшать» управляющую программу путем добавления в нее цикла для нескольких последовательных измерений с усреднением результата.


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

В качестве управляющих ключей используются силовые P-канальные MOSFET-ы любого типа с приемлемым сопротивлением открытого канала и максимальным напряжением сток-исток не менее 30 вольт. В приведенной выше схеме используются разные транзисторы. Так сделано потому, что им предстоит коммутировать разные напряжения и тип каждого из них подбирался под конкретные условия работы. Верхний транзистор должен быть более высоковольтным, а нижний должен по возможности иметь минимальное сопротивление открытого канала. Но, повторюсь, это решение продиктовано схемой включения устройства (см. выше), при другом включении требования к нижнему транзистору могут быть иными.

Для управления силовыми ключами используется пара одинаковых биполярных транзисторов. Сначала может показаться, что транзисторы эти лишние, но тут не все так просто. Полевые транзисторы с изолированным затвором начинают открываться не от любого напряжения нужной полярности на затворе, а лишь после достижения некоторого порогового уровня, который в даташитах фигурирует под названием «gate-to-source threshold voltage» и равен обычно 2…4 В. Теперь давайте просто посчитаем. Выходная цепь контроллера может формировать два логических уровня: логический »0» с напряжением, стремящимся к нулю; и логическая »1» с напряжением, стремящимся к питающему. При питании 5 вольт это будут напряжения около 0 и 5 В соответственно. В итоге при коммутации 12-вольтового источника, логический »0» на затворе создаст разницу напряжений исток-затвор 12 — 0 = 12 вольт, силовой транзистор открыт. Вроде все нормально, но вот логическая »1» с ее напряжение 5 В создаст между истоком и затвором напряжение 12 — 5 = 7 вольт, и силовой транзистор все равно останется открытым. Таким образом, пятивольтовый управляющий сигнал не может контролировать ключ, который коммутирует напряжение выше 7…9 вольт. Поэтому управляющие биполярные транзисторы фактически работают не столько сигнальными ключами, сколько усилителями, поднимающими управляющее напряжение с 5 вольт до напряжения бортовой сети.

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

Легко заметить, что управляющих транзисторов не было в аналоговой схеме на базе LM393N. Тут все дело в том, что выходной каскад выбранного компаратора построен по схеме с открытым коллектором, то есть его выход — это просто вывод коллектора оконечного транзистора. Такой принцип построения требует навешивать на микросхему дополнительные детали для создания нагрузки выходного каскада, но с другой стороны делает микросхему очень гибкой. Открытый коллектор позволяет компаратору управлять любым допустимым источником тока, а не только совместимым с тем, который обеспечивает питание самого компаратора.


Нужно сказать, что ограничение порогового напряжения силового MOSFET-а работает не только в сторону высоких напряжений, как сказано выше, но и в сторону низких. Ведь если минимальное напряжение открытия транзистора, скажем, 4 вольта, то при коммутации источника 3.3 В даже соединение затвора с землей не создаст между истоком и затвором нужной разности напряжений и транзистор останется закрытым. Так что 5 вольт — это, пожалуй, минимальное напряжение, которое можно надежно коммутировать выбранными транзисторами.

Настройка


Настройка устройства — это отдельный разговор. С одной стороны, в схеме нет ни одного настроечного элемента, но с другой, мы имеем дело с измерением напряжений с точностью не хуже 0.1 В. Как увязать все это? Тут два пути. Первый состоит в использовании резисторов R6, R7 и R8 с допуском не хуже 1% (а лучше 0.1%). Второй же предполагает использование обычных резисторов с обмером их реальных сопротивлений и коррекцией коэффициентов в исходном коде программы.

Первый метод хорош для серийного производства, нам же гораздо привлекательнее не заморачиваться с поиском нужных номиналов высокой точности, так что пойдем по второму пути. Измерять сопротивления можно обычным мультиметром, его точности тут вполне достаточно. Еще одним объектом измерения будет напряжение стабилизатора, питающего схему. АЦП контроллера может работать в разных режимах, но нам по ряду причин удобнее использовать такой, в котором отсчет цифрового результата преобразования ведется относительно питающего напряжения. Именно поэтому важно знать его как можно точнее.

Расчет крайне простой и состоит из вычисления коэффициента деления резистивного делителя и пропорции перевода полученного результата в LSB при аналогово-цифровом преобразовании.

s--qvfm_z0imo_xg5r7tnskhze0.png


Ux — входное напряжение делителя;
Ru — сопротивление верхнего плеча делителя (на которое подается Ux);
Rd — сопротивление нижнего плеча делителя (которое соединено с землей);
Uref — опорное напряжение АЦП (т.е. напряжение питания контроллера);
1024 — количество дискретных значений на выходе 10-разрядного АЦП;
LSB — числовое значение, получаемое программой из АЦП.

Начнем с делителя напряжения R6-R7. Для примера примем реальные сопротивления полностью соответствующие указанным на схеме. Питание тоже возьмем ровно 5.0 В. Пример расчета результатов преобразования напряжения 13.5 вольт:

nrica2q5vtygw-ck3mstvspkm4y.png


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

Формула расчета делителя, измеряющего температуру, в принципе ничем не отличается, только переменной величиной тут будет Ru, а Ux можно принять равным Uref. Результат будет иметь такой вид:

zvt7qi05g21ecwvaquqnjdbhjli.png


Для примера возьмем величину R8 из схемы, а R9 из даташита на NTCLE100E3 при температуре 0⁰C:

j5gp_kzjcx3buv0ayejxoazlwcu.png


Если кто скажет, что под влиянием нагрузки из последовательно соединенных R8 и R9 напряжение на логическом выходе может просесть, то он, конечно, будет прав. Теоретически. А на практике даже наиболее пессимистический сценарий, когда сопротивление терморезистора R9 окажется равным нулю, потребление от выхода контроллера будет не более 0.5 mА, что не вызовет сколь либо заметного падения. По крайней мере, в ходе натурных испытаний это падение не удалось зафиксировать при помощи вольтметра, имеющего точность 0.01 В.


Сопротивление терморезистора при различных температурах можно найти в даташите, но лучше, конечно, измерить их самостоятельно. Последнее может представлять некоторую проблему, поэтому как компромиссный вариант предлагаю измерить реальное сопротивление при одной температуре и найти таким образом поправочный коэффициент для сопротивлений при всех остальных температурах из даташита. Большинство терморезисторов имеют более-менее линейную зависимость сопротивления от температуры, так что подобный метод можно считать достаточно надежным.

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

Прошивка


Полный архив проекта для AtmelStudio (компилятор gcc-avr 5.4.0) можно скачать тут, также выложил уже собранный hex. А под катом листинг иходного файла, чтобы далеко не ходить.

Исходник
//#define F_CPU 1200000UL // определен в свойствах проекта

#include 
#include 
#include 
#include  
#include 

//#define DBG

#define TEMPERATURE_OVERHEAT 753 // LSB-величина температуры +50⁰C
#define TEMPERATURE_GIST     8   // ширина петли гистерезиса (в LSB) при переходах по оси температур
#define VOLTAGE_GIST         3   // ширина петли гистерезиса (в LSB) при переходах по оси напряжений

#define INTERVAL             WDTO_1S // длительность одного цикла измерений (1 секунда)
#ifndef DBG
#define CELL_CHANGE_TIMEOUT  90  // задержка перехода в новое состояние (в циклах INTERVAL, не выше 254)
#define OVERHEAT_TIMEOUT     300 // минимальная задержка возврата из режима "перегрев" (в циклах INTERVAL)
#else
#define CELL_CHANGE_TIMEOUT  2
#define OVERHEAT_TIMEOUT     3
#endif

typedef unsigned char bool; // просто правило хорошего тона
#define true  0 == 0        //   использовать осмысленные имена
#define false 0 != 0        //   для булевого типа данных

typedef enum {st_none = 0b00, st_primary = 0b01, st_secondary = 0b10, st_both = 0b11} t_states; // перечисление состояний нагрузок 
                                                                                                //   тип используется в битовых операциях, поэтому реальные значения элементов заданы жестко
typedef enum {adc_temperature, adc_voltage} t_measure;                                          // перечисление типов датчиков
typedef enum {move_null, move_up, move_down} t_movement;                                        // перечисление направлений перемещения по таблице состояний

// координаты ячейки таблицы состояний
struct t_coordidates {
  signed char row, col;
};

// информация о последнем перемещении по таблице состояний
struct t_correction {
  t_movement voltage, temperature;
};

#define CELLS_ROWS 3 // количество строк в таблице состояний (ось температур)
#define CELLS_COLS 5 // количество столбцов в таблице состояний (ось напряжений)

// таблица состояний
const t_states CELLS[CELLS_ROWS][CELLS_COLS] = {
  {st_both, st_both,    st_both,    st_primary, st_none},
  {st_both, st_both,    st_primary, st_none,    st_none},
  {st_both, st_primary, st_none,    st_none,    st_none}
};

// LSB-величины температур, которые являются границами строк таблицы состояний
const unsigned int ROWS_EDGES[CELLS_ROWS - 1] = {
  241, // 0⁰C
  157  // -10⁰C
};

// LSB-величины напряжений, которые являются границами столбцов таблицы состояний
const unsigned int COLS_EDGES[CELLS_COLS - 1] = {
  864, // 13.5V
  800, // 12.5V
  787, // 12.3V
  768  // 12.0V
};

unsigned int overheat_rest_time = 0; // счетчик времени задержки выхода из состояния "перегрев"
unsigned char cell_change_time  = 0; // счетчик времени задержки перехода между состояниями
unsigned char no_cur_cell_time  = 0; // счетчик времени, на протяжении которого рабочая точка ни разу не возвращалась в текущую ячейку

#define NULL_CELL (struct t_coordidates){.col = -1, .row = -1} // заглушка, обозначающая отсутствие состояния
#define NULL_CORRECTION (struct t_correction){.voltage = move_null, .temperature = move_null} // заглушка, обозначающая отсутствие перемещения

struct t_correction moved_from = NULL_CORRECTION; // информация о направлении последнего перехода между состояниями
struct t_coordidates cur_cell  = NULL_CELL,       // координаты текущей ячейки в таблице состояний
                     next_cell = NULL_CELL;       // координаты ячейки-кандидата на следующее перемещение

// инициализация пинов
static void init_pins() {
  DDRB |= (1 << PB0) | (1 << PB1) | (1 << PB3);     // устанавливаем пины 2 (PB3), 5 (PB0) и 6 (PB1) как выходы
  PORTB &= ~(1 << PB0) & ~(1 << PB1) & ~(1 << PB3); // устанавливаем низкий уровень на пинах 2 (PB3), 5 (PB0) и 6 (PB1)
}

// включение/выключение подачи питания на терморезистор
static void toggle_thermal_sensor(bool state) {
  if(state) {
    PORTB |= (1 << PB1);  // если state равно state_on, подаем высокий уровень на пин 6 (PB1)

    _delay_ms(5); // подождем завершения переходных процессов
  } else {
    PORTB &= ~(1 << PB1); // если state не равно state_on, подаем низкий уровень на пин 6 (PB1)
  }
}

// измерение аналоговых величин
static unsigned int measure_adc(t_measure measure) {
  if(measure == adc_temperature) {
    toggle_thermal_sensor(true); // если задача измерить температуру, подаем питание на терморезистор

    ADMUX = 0b10; // при измерении температуры используем для аналого-цифрового преобразования пин 3 (PB4)
  } else {
    ADMUX = 0b01; // при измерении напряжения используем для аналого-цифрового преобразования пин 7 (PB2)
  }

  ADCSRA = (1 << ADPS2) | // коэффициент деления тактовой частоты для АЦП = 16 (75 КГц)
           (1 << ADIE) |  // режим оповещения через прерывание
           (1 << ADEN);   // активация АЦП

  set_sleep_mode(SLEEP_MODE_ADC); // подготавливаем режим "тихого" преобразования
  do {
    sleep_cpu(); // запуск АЦП происходит после засыпания контроллера, а после завершения преобразования генерируется прерывание, которое будит контроллер
  } while(ADCSRA & (1 << ADSC)); // если после пробуждения преобразование все еще не завершено, продолжаем спать

  ADCSRA = 0; // выключаем АЦП

  toggle_thermal_sensor(false); // отключаем подачу питания на терморезистор

  return ADC; // возвращаем 10-битный результат преобразования
}

// инициализация прерываний и watchdog
static void init_interrupts(void) {
  sleep_enable(); // разрешаем спящий режим

  WDTCR = (1 << WDCE) | (1 << WDE); // подготавливаем watchdog
  WDTCR = (1 << WDTIE) | INTERVAL; // watchdog генерирует прерывание вместо сброса всего контроллера, интервал 1 секунда

  sei(); // разрешаем прерывания
}

// устанавливает состояния нагрузок в соответствии с содержимым ячейки таблицы состояний
static void toggle_loads(t_states states) {
  unsigned char port = PORTB & ~((1 << PB3) | (1 << PB0)),     // считываем текущее состояние всех выходов контроллера и обнуляем в нем биты, соответствующие нашим выходам
                bits = (((states & st_primary) >> 0) << PB3) | // устанавливаем нужные биты в соответствии с состояниями нагрузок
                       (((states & st_secondary) >> 1) << PB0);

  PORTB = port | bits; // устанавливаем новое состояние нагрузок
}

// сравнение двух переменных типа t_coordidates
static bool cells_equal(struct t_coordidates cell1, struct t_coordidates cell2) {
  return cell1.row == cell2.row && cell1.col == cell2.col;
}

// определение строки в таблице состояний по полученному с датчика LSB-значению температуры
static signed char get_cell_row(unsigned int temperature) {
  signed char row = 0;

  while(row < CELLS_ROWS - 1) {          // передвигаемся от самого высокого порогового значения в сторону самого низкого
    if(temperature >= ROWS_EDGES[row]) { // если temperature больше или равен пороговому значению, значит нужная строка найдена
      return row;
    } else {
      ++row;
    }
  }

  return CELLS_ROWS - 1; // если temperature слишком низкая и не превышает ни одного порогового значения, значит мы в самой нижней строке таблицы
}

// определение столбца в таблице состояний по полученному с датчика LSB-значению напряжения
static signed char get_cell_col(unsigned int voltage) {
  signed char col = 0;

  while(col < CELLS_COLS - 1) {      // передвигаемся от самого высокого порогового значения в сторону самого низкого
    if(voltage >= COLS_EDGES[col]) { // если voltage больше или равен пороговому значению, значит нужный столбец найден
      return col;
    } else {
      ++col;
    }
  }

  return CELLS_COLS - 1; // если voltage слишком низкое и не превышает ни одного порогового значения, значит мы в самом правом столбце таблицы
}

// возвращает пороговые значения температуры, отделяющие текущую строку таблицы сосояний от соседей
static void get_row_edges(signed char row, unsigned int *upper, unsigned int *lower) {
  *upper = row > 0 ? ROWS_EDGES[row - 1] : 0xffff - TEMPERATURE_GIST; // для крайней верхней строки верхний порог отсутствует, возвращаем максимально применимое значение
  *lower = row < CELLS_ROWS - 1 ? ROWS_EDGES[row] : TEMPERATURE_GIST; // для крайней нижней строки нижний порог отсутствует, возвращаем минимально применимое значение
}

// возвращает пороговые значения напряжения, отделяющие текущий столбец таблицы сосояний от соседей
static void get_col_edges(signed char col, unsigned int *upper, unsigned int *lower) {
  *upper = col > 0 ? COLS_EDGES[col - 1] : 0xffff - VOLTAGE_GIST; // для крайнего левого столбца левый (верхний по напряжению) порог отсутствует, возвращаем максимально применимое значение
  *lower = col < CELLS_COLS - 1 ? COLS_EDGES[col] : VOLTAGE_GIST; // для крайнего правого столбца правый (нижний по напряжению) порог отсутствует, возвращаем минимально применимое значение
}

// коррекция координат потенциальной ячейки-кандидата в соответствии с данными о последнем перемещении и ширины гистерезисов для температуры и напряжения
static void gisteresis_correction(struct t_coordidates* new_cell, unsigned int temperature, unsigned int voltage) {
  unsigned int upper_edge, lower_edge;

  get_row_edges(cur_cell.row, &upper_edge, &lower_edge); // определяем границы текущей строки
  if(new_cell->row > cur_cell.row && moved_from.temperature == move_up && temperature >= lower_edge - TEMPERATURE_GIST) {
    --new_cell->row; // если потенциальная ячейка-кандидат находится ниже текущей, последнее перемещение было вверх, но температура недостаточно низкая для преодоления ширины гистерезиса, то уменьшаем номер строки
  }

  if(new_cell->row < cur_cell.row && moved_from.temperature == move_down && temperature <= upper_edge + TEMPERATURE_GIST) {
    ++new_cell->row; // если потенциальная ячейка-кандидат находится выше текущей, последнее перемещение было вниз, но температура недостаточно высока для преодоления ширины гистерезиса, то увеличиваем номер строки
  }

  get_col_edges(cur_cell.col, &upper_edge, &lower_edge); // определяем границы текущего столбца
  if(new_cell->col > cur_cell.col && moved_from.voltage == move_up && voltage >= lower_edge - VOLTAGE_GIST) {
    --new_cell->col; // если потенциальная ячейка-кандидат находится правее текущей, последнее перемещение было влево (вверх по напряжению), но напряжение недостаточно низкое для преодоления ширины гистерезиса, то уменьшаем номер столбца
  }

  if(new_cell->col < cur_cell.col && moved_from.voltage == move_down && voltage <= upper_edge + VOLTAGE_GIST) {
    ++new_cell->col; // если потенциальная ячейка-кандидат находится левее текущей, последнее перемещение было вправо (вниз по напряжению), но напряжение недостаточно высокое для преодоления ширины гистерезиса, то увеличиваем номер столбца
  }
}

// экономим шесть байт по сравнению с stdlib::abs()
 static unsigned char absolute(signed char value) {
  return value >= 0 ? value : -value;
}

// определяем направления перемещения по координатам ячейки-кандидата
static void calc_movement(struct t_coordidates new_cell) {
  moved_from = NULL_CORRECTION;                                                   // по-умолчанию принимаем отсутствие перемещения
  if(!cells_equal(new_cell, NULL_CELL) && !cells_equal(cur_cell, NULL_CELL)) {    // направление имеет смысл только если определены и текущая ячейка, и ячейка-кандидат
    if(absolute(new_cell.row - cur_cell.row) == 1) {                              // учитываем только перемещение в соседнюю ячейку
      moved_from.temperature = new_cell.row < cur_cell.row ? move_up : move_down; // перемещение по строкам
    }

    if(absolute(new_cell.col - cur_cell.col) == 1) {                              // учитываем только перемещение в соседнюю ячейку
      moved_from.voltage = new_cell.col < cur_cell.col ? move_up : move_down;     // перемещение по столбцам
    }
  }
}

// установка новой ячейки-кандидата
static void set_next_cell(struct t_coordidates cell) {
  next_cell = cell;
  cell_change_time = 0; // сброс счетчика задержки перехода
}

// установка новой текущей ячейки
static void set_cur_cell(struct t_coordidates cell) {
  cur_cell = cell;
  no_cur_cell_time = 0; // сброс счетчика времени непрерывного нахождения в посторонних ячейках
  set_next_cell(NULL_CELL); // сброс ячейки-кандидата
}

// действия, связанные с переходом в новую ячейку
static void change_cell(struct t_coordidates new_cell) {
  if(cells_equal(new_cell, NULL_CELL)) { // переход в пустую ячейку обозначает безусловное отключение всех нагрузок
    toggle_loads(st_none);
  } else {
    toggle_loads(CELLS[new_cell.row][new_cell.col]); // определяем состояния нагрузок для данной ячейки и применяем их
  }

  calc_movement(new_cell); // вычисляем и сохраняем напрявление перехода
  set_cur_cell(new_cell);  // устанавливаем текущую ячейку
}

// основной метод
static void main_proc(void) {
  unsigned int temperature, voltage; // 10-битные LSB-величины измеренных температуры и напряжения
  struct t_coordidates cell;         // переменная для хранения координат потенциальной ячейки-кандидата

  if(overheat_rest_time) { // если счетчик выхода из состояния "перегрев" не нулевой, уменьшаем его значение на единицу и больше ничего не делаем
    --overheat_rest_time;
  } else {
    temperature = measure_adc(adc_temperature); // измеряем температуру
    if(temperature >= TEMPERATURE_OVERHEAT) {   // если температура выше или равна +50C, значит перегрев:
      change_cell(NULL_CELL);                   //   сбрасываем текущую ячеку (аварийно отключаем все нагрузки)
      overheat_rest_time = OVERHEAT_TIMEOUT;    //   устанавливаем счетчик возврата на максимальное значение
    } else {
      voltage = measure_adc(adc_voltage);   // измеряем напряжение

      cell.col = get_cell_col(voltage);     // вычисляем столбец потенциальной ячейки-кандидата по напряжению
      cell.row = get_cell_row(temperature); // вычисляем строку потенциальной ячейки-кандидата по температуре

      if(cells_equal(cur_cell, NULL_CELL)) { //  если текущая ячейка ранее не была определена, то немедленно устанавливаем ее на основе приведенных выше измерений
        change_cell(cell);
      } else {
        gisteresis_correction(&cell, temperature, voltage); // производим возможную коррекцию вычесленных ранее координат с учетом направления последнего перемещения и ширин гистерезисов

        if(cells_equal(cell, cur_cell)) { // если потенциальная ячейка-кандидат соответствует текущей ячейке, то значит кандидата у нас нет
          set_next_cell(NULL_CELL);
          no_cur_cell_time = 0; // побывали в текущей ячейке, сбрасываем счетчик
        } else {
          if(no_cur_cell_time++ > CELL_CHANGE_TIMEOUT) { // если в течение CELL_CHANGE_TIMEOUT+1 рабочая точка ни разу не побывала в cur_cell, значит текущая ячейка была выбрана неудачно
            change_cell(cell); // устанавливаем текущей ту ячейку, в которой рабочая точка сейчас
          } else {
            if(cells_equal(next_cell, NULL_CELL) || !cells_equal(next_cell, cell)) { // если ячейка-кандидат не определена или не соответствует новому кандидату, устанавливаем нового кандидата
              set_next_cell(cell);
            } else {
              if(++cell_change_time >= CELL_CHANGE_TIMEOUT) { // если кандидат стабилен, и таймаут перехода в другую ячейку истек, переходим, иначе только инкрементируем счетчик
                change_cell(cell);
              }
            }
          }
        }
      }
    }
  }
}

// обработчик прерывания от watchdog
ISR(WDT_vect) {
  WDTCR |= (1 << WDTIE); // после каждого срабатывания watchdog нужно заново "заказывать" прерывание вместо сброса контроллера
}

// пустой обработчик прерывания АЦП, для определения момента завершения преобразования используется флаг ADSC в measure_adc()
EMPTY_INTERRUPT(ADC_vect);

// точка входа
int main(void) {
  init_pins();       // инициализация пинов
  init_interrupts(); // инициализация прерываний и watchdog
        
  while(true) {                          // главный цикл, управление никогда не должно выходить из него
    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // включаем режим глубокого сна с минимальным потреблением тока
    sleep_cpu();                         // засыпаем и ждем пробуждения по прерыванию от watchdog 

    main_proc();                         // быстро делаем работу и снова идеем спать в следующей итерации
  }
}

Фъюзы должны иметь такие значения: L:0×6A, H:0xFF.


Принцип работы программы таков. Есть таблица состояний выходов, у которой горизонтальная ось — напряжение бортсети, а вертикальная ось — температура. Программа измеряет напряжение и температуру и по ним находит в таблице ячейку, которая определяет состояния нагрузок. Сама таблица имеет такой вид:

0jwxezo197qadbln8crg05zwlhq.png


Верхняя строка таблицы определяет режим перегрева и обрабатывается отдельным простым ветвлением кода, поэтому ее нет в исходном коде.

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

Допустим, в какой-то момент времени программа находится в состоянии, когда как минимум одна из нагрузок включена. По мере расходования заряда АКБ напряжение сети будет падать и рано или поздно достигнет порогового значения, когда нагрузку нужно отключить, что и произойдет в нужный момент. Однако, из-за того, что провода, соединяющие схему с АКБ, имеют ненулевое сопротивление, уменьшение текущего через них тока при отключении нагрузки приводит к некоторому увеличению напряжения на входе схемы. А т.к. мы только-только пересекли границу отключения, то даже незначительное увеличение напряжения почти гарантированно перебросит нас в состояние, когда нагрузка должна быть снова включена. И схема включает нагрузку. Напряжение снова немного проседает и снова пересекает пороговое значение. И так будет продолжаться довольно долго, по

© Habrahabr.ru