Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Настройка

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

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

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

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

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

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

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

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

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

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

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

Прошивка

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

Исходник

//#define F_CPU 1200000UL // определен в свойствах проекта #include <avr/io.h>
#include <avr/wdt.h>
#include <avr/sleep.h>
#include <avr/interrupt.h> #include <util/delay.h> //#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 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 истинно, подаем высокий уровень на пин 6 (PB1) _delay_ms(5); // подождем завершения переходных процессов } else { PORTB &= ~(1 << PB1); // если state истинно, подаем низкий уровень на пин 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:0x6A, H:0xFF.

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

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

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

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

Для подавления колебательного процесса в программу внесен механизм гистерезиса, который после перехода границы состояний как бы сдвигает только что пересеченную черту немного назад против движения рабочей точки. Таким образом, при пересечении, скажем, границы 12.5 В снизу вверх, для ее пересечения в обратном направлении, нужно будет опуститься уже до 12.4 В. Этот же принцип в значительной мере препятствует переключению состояния также под влиянием шумов АЦП и наводок. Константы, определяющие ширину гистерезиса по вертикали и по горизонтали тоже могут быть легко изменены.

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

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

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

Он настроен на генерацию прерывания вместо принудительной перезагрузки и просто периодически выводит контроллер из глубокого сна для очередного цикла работы. В качестве «сердца», задающего цикличность измерений, используется сторожевой таймер (watchdog).

Но на самом деле это не так. Может показаться, что такое решение лишает нас основной функции сторожевого таймера – предотвращения зависания. Таким образом, если произойдет зависание контроллера, то не будет выполняться не только основной цикл программы, но и обработчик прерывания watchdog. Watchdog устроен таким образом, что флаг, включающий генерацию прерывания вместо перезагрузки, аппаратно сбрасывается при каждом срабатывании. А это значит, что первое срабатывание сбросит флаг прерывания, а следующее за ним уже инициирует полную перезагрузку.

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

Но как оказалось, в моем случае минимальную длину дала опция O2, в то время, как предназначенная для минимизации размера опция Os выдала код, с трудом влезающий в отведенные 1024 байта. Кстати, размер итогового бинарника зависит от опций оптимизации, что логично. Так что если программа на пределе возможностей контроллера и не влазит совсем чуть-чуть, всегда имеет смысл поиграться данным параметром.

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

Файлы схемы и разводки платы

Скачать редактируемые файлы в формате Eagle можно здесь.

Показать больше

Похожие публикации

Добавить комментарий

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

Кнопка «Наверх»