====== 5.4 Сторожевой таймер ======
Данная статья является наброском к главе "5.4 Сторожевой таймер" книги "Отказоустойчивое ПО для МК".
===== Определение =====
**WDT** - сторожевой таймер (watchdog timer) - аппаратный механизм защиты от зависания программы. В самом простом случае WDT представляет собой счетчик, тактируемый системным или независимым генератором. При переполнении счетчика схема WDT генерирует сигнал сброса микроконтроллера, который приводит к его перезагрузке. Программа должна периодически посылать схеме WDT сигнал о том, что все в порядке; этот сигнал говорит сторожевому таймеру, что программа работает корректно, и счетчик сторожевого таймера обнуляется (в некоторых контроллерах обновляется константой).
Если программа перестает посылать сигналы сторожевому таймеру, т.е. сигналы обнуления WDT отсутствуют в течение времени, достаточного для переполнения счетчика, то это свидетельствует о том, что программа зависла, и ситуация воспринимается как аварийная. В результате длительного отсутствия сигналов от программы схема WDT формирует сигнал сброса микроконтроллера (в некоторых типах микроконтроллеров WDT генерирует еще немаскируемое прерывание).
Также многие WDT могут работать в оконном режиме, когда сигнал от программы на обнуление WDT не допускается раньше какого-то времени. Т.е. он не должен приходить слишком рано или слишком поздно. Такая функция может оказаться полезной для более быстрого реагирования на некоторые виды сбоя. Например, если функция обнуления WDT вызывается раньше времени, то это говорит о том, что какие-то функции отработали быстрее, чем ожидалось, что может свидетельствовать о наличии сбоя. Из такого режима нужно выйти как можно быстрее, не дожидаясь переполнения WDT (т.е. не давая программе еще какое-то время работать во внештатном режиме). Оконные WDT удобно применять в тех случаях, когда нет возможности контролировать время выполнения участков кода с помощью аппаратного таймера.
===== Типы WDT =====
WDT бывает встроенным в МК, внешним в виде специализированной микросхемы или внешним в виде отдельного электронного устройства (или узла). Обычно счетчик в составе WDT тактируется собственным генератором, не зависящим от основного системного генератора контроллера. Чаще всего это внутренний RC-генератор. Отдельный генератор применяется с той целью, чтобы WDT имел возможность продолжать счет (а в последствии - сформировать сигнал сброса) даже если произойдет срыв генерации основного генератора, тактирующего МК.
В зависимости от типа WDT обнуление его счетчика может производиться по-разному. Например, счетчик внешнего WDT обычно сбрасывается переключением управляющего входа из одного состояния в другое (в некоторых - только по одному фронту). Для сброса внутренних WDT может использоваться специальная инструкция микроконтроллера, или запись в какой-нибудь регистр (или отдельный бит регистра), или выполнение микроконтроллером последовательной записи двух констант (так называемого пароля) в определенный регистр WDT.
==== Внутренний WDT ====
Встроенный в МК сторожевой таймер является самым ненадежным из перечисленных и на него можно полагаться только в малоответственных системах. Есть две причины:
* Таймер управляется регистрами, которые доступны из программы. Следовательно, сбившаяся программа может произвести случайную запись в эти регистры, изменив тем самым режим работы WDT или вообще отключив его. В большинстве микроконтроллеров операции над регистрами WDT защищены паролем, но это не защищает от случайного попадания программного счетчика на код записи в такие регистры со случайным набором параметров; некоторые микроконтроллеры позволяют производить запись в такие регистры только единожды, однако это не спасает от помехи во время записи в регистр. (Некоторые контроллеры имеют механизмы, запрещающие какие-либо действия по настройки WDT в ходе работы программы.) Также есть ограничение на временной интервал между записью пароля (или его частей) и обновлением регистров WDT. Все эти меры снижают вероятность порчи регистров WDT случайными действиями программы, но не делают это невозможным.
* Технологические нормы, по которым производятся сами микросхемы: чем выше степень интеграции, тем тоньше проводники на кристалле и тем более они подвержены влиянию внешних помех.
Стоит добавить, что некоторые микроконтроллеры допускают тактирование внутреннего WDT от системного генератора, что делает его неэффективным при срыве генерации или при случайном входе в Sleep-режим.
Поэтому внутренний WDT можно использовать только в малоответственных системах или, на худой конец, там, где критична стоимость продукта, и лишний элемент в виде внешнего WDT будет непозволительной роскошью, или где есть ограничения по габаритам или массе конечного устройства.
Однако не следует забывать и о достоинствах внутреннего сторожевого таймера:
* во-первых, он может пробуждать контроллер из Sleep-режима с заданным интервалом (если WDT тактируется независимым генератором);
* во-вторых, он является единственной возможностью выполнить программный сброс в контроллерах, в которых не предусмотрена инструкция RESET (AVR, PIC16, LPC213x);
* в-третьих, возможность программного отключения WDT позволяет максимально снизить потребление микроконтроллера за счет отключения независимого генератора;
* в-четвертых, некоторые контроллеры при переполнении WDT перед формированием сигнала сброса генерируют немаскируемое прерывание, которое позволяет успеть выполнить минимальный набор операций по переводу микроконтроллера и каких-то внешних устройств в безопасный режим перед сбросом, а также записать в ОЗУ какие-то данные, которые при перезагрузке могут использоваться для идентификации сбоя и анализа его причин;
* в-пятых, есть возможность изменять интервал в зависимости от режима работы.
==== Внешний WDT в виде специализированной микросхемы ====
Более эффективным с точки зрения надежности (и в большинстве случаев достаточным) является внешний WDT, выполненный в виде отдельной микросхемы. Обычно такие микросхемы комбинируют в себе функции сторожевого таймера и монитора напряжения питания. Такие микросхемы (они называются супервизорами, микропроцессорными мониторами или просто сторожевыми таймерами) выпускаются разными фирмами в различных вариантах. Сигналом "все хорошо" сторожевому таймеру в составе такой микросхемы обычно служит изменение логического уровня ("0"->"1" или "1"->"0") на специальном входе. Изменение уровня сбрасывает внутренний счетчик. Супервизоры имеют выход для управления сбросом микроконтроллера (обычно такие микросхемы имеют два выхода для управления сбросом: прямой и инверсный, - т.к. микроконтроллеры различных производителей имеют различные активные уровни входа сброса). Когда супервизор обнаруживает, что напряжение питания опустилось ниже какого-то предустановленного уровня или сигнал "все хорошо" отсутствует длительное время, он формирует активный уровень на выходе управления сбросом и удерживает его в активном состоянии либо в течение какого-то времени (если произошел сброс по переполнению счетчика сторожевого таймера) либо до того момента, как напряжение питания не придет в норму. Это общая функция, присущая всем супервизорам со встроенным WDT, в остальном микросхемы-супервизоры могут отличаться:
* одни имеют фиксированное время переполнения счетчика WDT, другие имеют возможность выбора интервала из набора фиксированных, третьи имеют возможность настраивать интервал установкой дополнительного конденсатора в цепь задающего RC-генератора;
* одни могут работать как оконные, другие только как традиционные, третьи имеют возможность работать и так и так, причем позволяют выбрать ширину окна;
* некоторые микросхемы имеют дополнительный выход для индикации того, что последний сброс произошел именно по переполнению WDT. Такой выход может оказаться полезным в двух случаях: во-первых, при перезагрузке контроллера мы сможем определить причину сброса, что также позволяет производить тестирование WDT при первом включении; во-вторых, этот выход можно использовать для управления питанием контроллера для вывода его из зависания в случае возникновения эффекта тиристорной защелки;
* некоторые имеют возможность подключать батарейку для продолжения функционирования при отключении питания;
* разные микросхемы имеют различные времена удерживания выхода управления сбросом в активном состоянии, кроме того, некоторые супервизоры позволяют настраивать это время.
В зависимости от задач, которые мы хотим возложить на WDT, помимо его основной функции, мы можем выбирать ту или иную микросхему. Для управления WDT в составе такой микросхемы нам понадобится один вывод контроллера, настроенный на выход, для генерации сигналов обнуления WDT (в случае контроля выхода WDO потребуется еще один вывод МК, настроенный на вход).
Также при выборе микросхемы супервизора следует обращать внимание на следующие параметры:
* потребление (варьируется от единиц микроампер до единиц миллиампер);
* тип корпуса: от SOT23 до DIP16;
* диапазон рабочих температур;
* цена.
Ниже приведены примеры микросхем-супервизоров, в составе которых имеется WDT (это краткий список, на самом деле таких микросхем великое множество):
MCP131x - 4 интервала WDT (6.3, 102, 1600, 25600 мс), потребление 5мкА, нет ножки WDO
ADM699 - фиксированное и довольно длительное время переполнения (тип. 1.6с),
имеет выход ~WDO, позволяющий определить, что сброс был вызван переполнением WDT.
Нет внешнего тактирования. Нет входа для батарейки. 600 мкА.
ADM705
MAX69x (x - четный) - Имеет выход ~WDO. Можно подключать батарейку. 2мА.
MAX69x (x - нечетный) - есть внешнее тактирование, два режима счета (1.6сек или 100 ms).
Имеет выход ~WDO. Можно подключать батарейку. 2мА.
EM6151 - оконный (67/33 или 33/67), без батарейки, 35 мкА (+ слип-режим 25 мкА), нет WDO
TC1232, ADM1232, MAX1232 - три режима интервала (150мс, 600мс, 1200мс), не имеет выхода WDO
MAX6746-6753 - 5 мкА, регулируемое время WDT, маленький корпус (sot23-8), оконный
MAX6369 - 8мкА, регулируемое время, корпус sot23-8, есть WDO
//TODO: дополнить и разъяснить//
==== Внешний WDT в виде устройства ====
Отдельное устройство разрабатывается индивидуально с учетом особенностей конкретного устройства, используемого в нем микроконтроллера и конкретной программы. Сердцем такого устройства может быть маленький, например, 8-разрядный микроконтроллер (чаще OTP, например, PIC12CE518), который, помимо стандартного набора функций WDT, может иметь ряд дополнительных возможностей. Т.е. преимущества таких WDT в расширенном функционале и гибкости настройки.
Недостатками таких WDT являются габариты, а также то, что программа в маленьком МК должна соответствовать всем требованиям безопасности (начиная с самодиагностики и заканчивая контролем собственного внутреннего WDT), а также требует тщательнейшей отладки. Кроме того, сам микроконтроллер, используемый в качестве WDT, также подвержен всем видам сбоев, вызванных помехами. Но это компенсируется, во-первых, тем, что такие контроллеры выполнены по более грубой технологии, чем тот, которым они будут управлять, следовательно, они помехам подвержены гораздо меньше; во-вторых, есть возможность организовать "круговую поруку", когда маленький МК следит за большим, а большой следит за маленьким, что во много раз повышает вероятность самовосстановления работоспособности системы после сбоя.
Но построение схем-мониторов (не называю их WDT, т.к. функционал может быть гораздо шире простого счетчика) дает массу дополнительных возможностей, таких как:
* **контроль частоты тактирующего генератора** - это позволит формировать сигнал аварии, если по каким-то причинам тактовая частота контролируемого МК не соответствует ожидаемой (например, частота внутреннего RC-генератора ушла на морозе);
* **контроль последовательности действий** - в некоторых случаях может являться дополнением к run-time тестам, описанным в предыдущем параграфе;
* **может принимать более сложные сигналы управления, чем простая смена уровня** - смена логического уровня (являющаяся сигналом для большинства специализированных микросхем WDT) довольно мало информативна, она может происходить и при сбитой частоте, и при логическом зацикливании программы (когда требуемые действия выполняются, но не в той последовательности);
* **может изменять параметры окна в широком диапазоне** - в зависимости от режима работы целевого МК могут понадобиться различные временные интервалы: где-то скорость реакции на сбой должна быть максимальной, а где-то выполняются длинные критические секции, которые нельзя прерывать;
* **формирование предварительного сигнала для генерации немаскируемого прерывания** - крайне полезная функция, которая позволит целевому микроконтроллеру (если он, конечно, не завис наглухо) попасть в обработчик аварийного прерывания, в котором он может успеть перевести внешнее оборудование в безопасный режим.
К WDT этого типа можно также отнести одновибраторы, собранные на дискретных компонентах (иногда с использованием компараторов). Такие WDT наименее всего подвержены помехам, но они требуют много места на плате, не очень экономичны в энергопотреблении и более сложны в настройке и отладке.
===== Предпосылки к использованию =====
Как описано в предыдущем параграфе, все ошибки и сбои программы, возникающие во время ее работы, должны по возможности отслеживаться в ходе ее выполнения средствами самой программы. При обнаружении некорректного ее поведения следует делать анализ состояния программы с целью определить критичность сбоя и, если это возможно, принять адекватные меры по восстановлению работоспособности программы. WDT является последним рубежом при обеспечении отказоустойчивости ПО, он генерирует сигнал сброса в том случае, если средствами программы обнаружить сбой не удалось, или если программа попала в непредусмотренное зацикливание. Причинами такого поведения программы могут быть несколько факторов:
* допущенная программистом ошибка;
* ошибка компилятора;
* ошибка конкретной ревизии кристалла;
* сбой программатора (когда какая-либо ячейка может получить недозаряд);
* износ энергонезависимой памяти программы;
* внутреннее механическое повреждение кристалла (после резких перепадов температур или механических ударов);
* сильные ЭМ-помехи;
* радиационное излучение;
* и пр.
//(Детально о причинах отказов ПО см. "2. Причины и последствия сбоев")//
Следует обратить внимание, что многие факторы не зависят ни от программистов, ни от схемотехников, ни от ОТК.
Важное замечание: сбой всегда предпочтительнее обнаружить средствами самой программы (это достигается введением в логику программы обработки сигнатур, проверки данных, проверки рассчетов и пр.), т.к. в этом случае есть возможность безопасного восстановления работоспособности, даже если придется для этого произвести программный сброс контроллера, перед которым имеется возможность перевести все внешнее оборудование в безопасный или нейтральный режим.
Но, во-первых, средствами программы всего проверить и перепроверить нельзя. И ограничения здесь не только в памяти, используемой для тестов, и времени, которое тесты отнимут, но и в том, что сами тесты, являясь частью программы, нужно бы проверять. А потом проверять и эти проверщики и т.д. Поэтому run-time тесты всегда неполные. Во-вторых, существуют непредвиденные сбои, при которых программный счетчик может прыгнуть в такую точку программы, в которой при текущем состоянии ячеек RAM-памяти и текущих настройках периферии, можно задержаться очень надолго. Например, сбоем программного счетчика, программа попала в код передачи байта в программной реализации интерфейса SPI:
void spi_send_byte (unsigned char Data)
{
int i;
i = 8;
<---- Сюда попали из-за сбоя PC
do
{
if (Data & 0x80) PIN_DO = 1;
else PIN_DO = 0;
spi_delay();
PIN_CLK = 1;
spi_delay();
PIN_CLK = 0;
Data <<= 1;
} while (--i);
}
На момент сбоя в ячейке памяти, где располагается переменная i, могло находиться любое значение, и безобидный цикл на 8 проходов может превратиться в цикл на 50000 проходов. В общем случае от подобных сбоев может спасти только WDT. В предыдущем параграфе (5.3 run-time контроль) было сказано о контроле стека и сигнатурах, которые могли бы сигнализировать о том, что что-то идет не так, но подобные тесты, во-первых, нецелесообразно применять во всех функциях без исключения, а во-вторых, при большом значении числа в ячейках памяти, занятых переменной i, WDT может успеть переполниться раньше, чем программа дойдет до проверок в конце функции.
Два слова стоит сказать об ошибках программы, которые могут привести к зависанию. Само собой разумеется, что надо стремиться к написанию не зависающих программ, уделяя внимание не только качественной проработке проектируемого алгоритма, но также и качественному кодированию, и качественному тестированию. Однако, существует несколько факторов, которые по отдельности или все вмести затрудняют выполнение какого-либо этапа. Например, не всегда заранее известен точный алгоритм будущего устройства (для инновационных проектов), когда изменения в алгоритм вносятся на этапе разработки. Такие изменения могут быть продиктованы различными обстоятельствами, начиная маркетинговыми исследованиями и заканчивая статусом фирм-производителей компонентов (банкротство, перепродажа и пр.). Алгоритм в результате внесения изменений в него может оказаться несогласованным.
Также повлиять на качественное выполнение какого-либо этапа разработки могут факторы организационного характера: над программой трудятся несколько человек, каждый из которых может по-своему интерпретировать какие-либо моменты, описанные в ТЗ, или по-своему додумать то, что в ТЗ не указано явно; программа или ее часть может быть поручена программисту, не имеющему достаточной квалификации или достаточного опыта для качественной реализации. Не следует забывать о факторе времени, выделенного на разработку. Программы, имеющие большое количество состояний, режимов работы и параллельных процессов, не могут быть протестированы целиком, т.к. время тестирования может превысить экономически целесообразные сроки выпуска устройства; поэтому к конечному потребителю программа может попасть, грубо говоря, недоотлаженной. Это не означает, что она будет сбоить и виснуть на каждом шагу, просто при совокупности внештатных ситуаций и стечении каких-то обстоятельств, она может зависнуть.
Как уже было сказано, нельзя исключать инструментальные ошибки, т.е. ошибки компиляторов, программаторов, наконец, самих микроконтроллеров. Они составляют лишь малую толику от ошибок, допускаемых самими разработчиками, но, тем не менее, имеют место быть.
Итак, даже если разработчик уверен в своей аккуратности, методичности и пунктуальности, существуют факторы, которые могут привести к непредвиденным ошибкам в производимом им ПО, вызывающих в том числе и непредусмотренное зависание программы. Поэтому использование сторожевого таймера в качестве системного монитора является обязательным, особенно, когда речь идет об ответственных системах, сбои в которых могут привести к убыткам или нанесению вреда здоровью людей.
===== Порядок обнуления WDT =====
Примечание: в данном параграфе применяюся вызовы псевдо-функций wdt_clear() и soft_reset().
- Предполагаем, что **wdt_clear()** выполняет необходимую последовательность действий по обнулению счетчика WDT (например, для внешнего WDT она будет инвертировать управляющий выход, для микроконтроллеров AVR это будет последовательная запись 1 и 0 в регистр WDTCR.4, для МК PIC это выполнение инструкции CLRWDT),
- Предполагаем, что **soft_reset()** производит последовательность действий по сбросу микроконтроллера: где-то это будет выполнение инструкции RESET, где-то вечный цикл с блокировкой прерываний, который даст переполниться WDT, а где-то - прыжок на начальный адрес ROM, где произойдет переинициализация стека (в реальности этим действиям должны предшествовать действия по переводу внешнего оборудования в безопасный режим, но для наглядности пока это опустим).
==== Что такое порядок обнуления WDT ====
Порядок обнуления WDT - это правила, по которым программа выполняет проверки и принимает решение о том, что на данный момент времени она выполняется корректно и, следовательно, можно давать команду WDT на обнуление счетчика. Т.е. счетчик WDT можно обнулять только тогда, когда программа считает, что все в порядке. Здесь кроются две проблемы:
- для каждой программы понятие "все в порядке" сугубо индивидуально и во многом зависит от того, какие действия она выполняет как в течение всего периода работы, так и в данный момент времени. Поэтому никакого универсального подхода или общего свода правил для определения этого понятия не существует, и каждая программа требует индивидуальной проработки комплекса проверок.
- когда программа считает, что все в порядке, - это вовсе не значит, что все действительно в порядке. Программа не может проверять каждое свое действие в рамках какого-либо процесса, особенно вкупе с текущим его состоянием и уж тем более - вкупе с текущим состоянием параллельных процессов. Поэтому программа содержит какие-то контрольные точки, при прохождении которых она, делая какие-то проверки, принимает решение о соответствии ее поведения ожидаемому. Но в промежутках между этими точками может произойти все что угодно.
Частью таких контрольных точек можно считать описанные в предыдущем параграфе run-time тесты. Но эти тесты призваны уже заметить ошибку, т.е. на месте засечь некорректное поведение программы, из-за которого нужно либо попытаться восстановить работоспособность программы, либо произвести сброс, либо перейти в безопасный режим. Срабатывание run-time теста однозначно нам говорит о том, что далее выполнять программу нельзя. Однако, надо учесть, что и успешное прохождени конкретного теста не говорит о правильноси функционирования программы в целом. Также контрольными точками могут считаться факты выполнения конкретных участков кода, т.е. те факты, что программа дошла до какого-то места с правильными параметрами. В некоторых случаях приходится учитывать еще и последовательность выполнения таких участков во времени. Следует добавить, что существуют участки кода, в течение выполнения которых нельзя точно сказать, корректно все выполняется или нет; типичный пример - библиотечные функции (см. ниже).
Умение программиста создавать набор контрольных точек, способных заметить наибольшее число отклонений в поведении программы, акцентируя внимание на ответственных участках, во многом определяет насколько WDT окажется полезным в обнаружении сбоя для конкретной программы.
==== Часто совершаемая ошибка - безусловное обнуление ====
Довольно часто можно встретить такой подход, при котором счетчик WDT безусловно обнуляется в главном цикле программы, в прерывании по таймеру, возникающему раз в миллисекунду, или несколько раз в программе с интервалом в 5-10 инструкций (или операторов). Другими словами, программист делает все, чтобы сторожевой таймер не успел переполниться ни при каких условиях. Доходит даже до комичного кода:
char uart_getch (void)
{
while (!USART_RX) wdt_clear();
return USART_RXREG;
}
Думаю, не надо пояснять, чем этот код плох. Достаточно предположить, что в эту функцию мы можем попасть при отключенном модуле USART, в результате чего повиснем на все время жизни программы.
Такие подходы эквивалентны неиспользованию WDT с одной разницей: у программиста появляется иллюзия по поводу отказоустойчивости программы и, следовательно, сильно снижается бдительность в предвидении возможных причин отказов (что там думать? если что - сторожевой таймер спасет!). Ведь, например, при обнулении WDT в прерывании, программа может просто повиснуть в вечном цикле ожидая условие, которое никогда не выполнится, в то время как прерывания будут исправно выполняться. Или для систем с одним вектором прерывания (например, младшие семейства PIC-контроллеров) может получиться так, что по случайности будет разрешено прерывание, которое не предусмотрено обработчиком, в результате чего программа никогда не выйдет из обработчика (сразу же при выходе опять произойдет вход, т.к. сброс флага не был предусмотрен), а обработчик таймера, в котором производится сброс WDT, по-прежнему будет выполняться с заданным периодом. Основня программа в этом случае управление уже не получит.
==== Периодическое обнуление с проверкой флагов ====
Часто можно встретить такую рекомендацию (в литературе, в интернет-статьях, на технических форумах): основные функции программы должны после своего выполнения устанавливать флажок, подтверждающий, что функция выполнена; в главный цикл в main() или в прерывание добавляется код, который проверяет, что все нужные функции отработали, и только после этого обнуляет WDT.
Для этого в программе заводится переменная, содержащая по одному биту на каждую критическую функцию:
union
{
struct { // Каждый бит обнуляется в соответствующей подпрограмме
UINT8 bKeyboard : 1; // Обработка кнопок
UINT8 bLCD : 1; // Ввывод данных на экран
UINT8 bRelay : 1; // Включение/выключение реле
UINT8 bClock : 1; // Часы
} Bits;
UINT8 NotDone; // Когда все биты сброшены, эта переменная = 0
} g_WDT;
В главном цикле main() (или в обработчике периодического перрывания) вставляется код проверки флагов:
void main (void)
{
g_WDT.NotDone = 0;// При инициалиации считаем, что программа выполняется корректно
...
while (1) // Основной цикл программы
{
if (!g_WDT.NotDone) // Первый проход цикла или все подпрограммы отработали
{
// Устанавливаем флаги функций, требуемых к выполнению
g_WDT.Bits.bKeyboard = 1;
g_WDT.Bits.bLCD = 1;
g_WDT.Bits.bRelay = 1;
g_WDT.Bits.bClock = 1;
// Обнуляем счетчик WDT
wdt_clear();
}
KeyboardWork();
LCDWork();
RelayWork();
ClockWork();
}
}
Каждая критическая функция сбрасывает соответствующий ей бит, если она выполнилась корректно, например:
void KeyboardWork (void)
{
if (!KeyboardTimeout) return; // Обрабатываем кнопки с определенным периодом. Если время еще не
// настало, то выходим из функции (примечание: данный флаг может
// устанавлиается, например, в прерывании по таймеру
// Читаем кнопки и обрабатываем дребезг
...
g_WDT.Bits.bKeyboard = 0; // Обнуляем флаг, сообщая, что функция отработала успешно
}
//Примечание: флаги можно заводить не только для тех функций, которые вызываются из основного цикла main(), но иногда и для вложенных функций и даже для обработчиков прерываний. Коду проверки важно знать, что ответственные участки кода были выполнены.//
Для простых приложений такой подход можно считать очень удачным, т.к. он имеет очевидные преимущества:
- он прост и легок в проектировании;
- он легко контролируется, т.к. программа содержит единственное место, где производится обнуление;
- он практически не требует ресурсов (ни ROM, ни RAM, ни времени).
Однако он имеет и свои недостатки:
- все основные функции и ответсвенные участки кода обязаны выполняться даже тогда, когда это не нужно;
- появляется ограничение на время выполнения всех функций: оно не должно превышать периода переполнения сторожевого таймера. Это заставляет нас неоправданно увеличивать этот период (о недостатке больших периодов будет сказано ниже);
- сводятся на нет преимущества оконного сторожевого таймера, ведь одна функция из-за сбоя может выполниться мгновенно (из-за ложного перехода, или из-за непредусмотренного состояния управляющих переменных программы), а время выполнения других функций компенсируют этот сбой;
- не учитывается последовательность выполнения функций (иногда она важна);
- время реакции на сбой может достигать времени периода переполнения счетчика WDT;
- наконец, при таком подходе мы не имеем возможности при возникновении нештатной ситуации произвести сброс.
==== Обнуление с контролем отдельных функций ====
Также часто встречается рекомендация в вызываемой функции выставлять переменной конкретное значение, а после выхода проверять его и, если оно соответствует ожидаемому, производить обнуление WDT:
uint16 g_WDT_Temp; // глобальная переменная
...
g_WDT_Temp = 0;
process_func1(); // В теле этой функции к WDT_Temp прибавляется 0x1111
if (g_WDT_Temp == 0x1111) wdt_clear();
else soft_reset();
process_func2(); // В теле этой функции к WDT_Temp прибавляется 0x2222
if (g_WDT_Temp == 0x3333) wdt_clear();
else soft_reset();
process_func3(); // В теле этой функции к WDT_Temp прибавляется 0x3333
if (g_WDT_Temp == 0x6666) wdt_clear();
else soft_reset();
...
Метод является комбинацией с одним из видов run-time проверок. Если мы случайно пропускаем выполнение какой-либо функции (например из-за сбоя PC), то run-time проверка сигнатуры функции вызовет обработчик soft_reset(). Вне комбинации с run-time проверками этот метод не только малоэффективен, но даже опасен, т.к. если убрать во всех условиях ветку else, то даже после обнаружения сбоя (т.е. сбой не только произошел, но уже замечен) мы позволим программе выполнить еще несколько функций, уже находясь во внештатном режиме.
Т.е. данный метод является дополнением к run-time проверкам. На мой взгляд, он не очень эффективен, т.к.:
- сильно портит наглядность программы, даже если вынести условие в отдельную функцию;
- при изменении логики работы программы потребуется вносить коррективы в константы;
- довольно прожорлив по ресурсам;
- практически бесполезен при использовании внешних библиотек.
Однако он имеет два преимущества:
- быстрая реакция на сбой при использовании оконного сторожевого таймера. Например, функция process_func1() отработала, выставила сигнатуру, но из-за сбоя выполнилась не за 5 мс, как предполагалось, а за 100 мкс. И тогда даже при правильно установленной сигнатуре реакция на сбой будет моментальная, что не позволит выполняться остальным функциям уже при наличии сбоя.
- возможность использования при низкой тактовой частоте, когда скорость программы мала и время переполнения сторожевого таймера близко ко времени выполнения несложной функции (это особенно актуально при обработке исключения по срыву генерации).
==== Периодическое обнуление с контролем состояния ====
Суть метода заключается в том, что обработчик WDT, анализируя специально заведенные для этих целей глобальные переменные, знает, в каком режиме работает программа, что она должна делать и чего она делать не должна. Вызов wdt_clear() производится при прохождении всех проверок внутри обработчика периодического прерывания (чаще всего от таймера). Это позволяет нам вести постоянный контроль за состоянием программы с определенным периодом вне зависимости от времени выполнения отдельных функций. В сущности этот метод является расширенной версией метода обнуления с контролем флагов за исключением того, что он только в редких случаях может использоваться вне прерывания.
Метод хорош тем, что позволяет выполнять различные проверки для различных режимов работы, т.е. позволяет контролировать правильность работы программы в целом (например, для диктофона не могут быть одновременно активны режимы записи и воспроизведения; автосигнализация не может запоминать новые серийные номера брелоков, если она не в режиме программирования и т.п.). В зависимости от текущего режима работы и выпоняемых в данный момент действий, порядок и набор проверок может меняться. К примеру, электрическая плита может работать в 3х режимах: режим сна, режим готовки и режим настройки. В каждом из них выполняются разные наборы функций, которые было бы неудобно контролировать скопом, поэтому для контроля каждого набора создается своя ветвь проверок в зависимости от режима.
int g_Mode = 0;
void TIMER1_ISR (void)
{
static bool bAllowClearWDT;
switch (g_Mode)
{
case MODE_IDLE:
...
break;
case MODE_COOKING:
...
break;
case MODE_SETUP:
...
break;
default: // Несуществующий режим
// Выполняем какие-то действия по оживлению устройства, если это
// возможно, или производим программный сброс, предварительно
// запротоколировав сбой
break;
}
if (bAllowClearWDT) wdt_clear();
...
}
Этот метод лишен основных недостатков двух предыдущих методов обнуления (с контролем флагов и контролем функций), а именно:
- он позволяет функциям выполняться настолько долго, насколько это требуется;
- он отслеживает выполнение только тех функций, которые должны выполняться в данный момент;
- может учитывать последовательность выполнения функций;
- есть возможность произвести обработку внештатной ситуации вручную (в худшем случае, произведя сброс).
Но главное преимущество этого метода в том, что он может быть применен тогда, когда программа использует внешние библиотеки, учитывая, что в них нет четкого регламента по времени выполнения отдельных функций, а также при работе программы под управлением RTOS, где каждый из нескольких процессов живет своей жизнью (см. ниже).
Однако, при своей универсальности этот метод не лишен недостатков:
- он требует тщательной проработки алгоритма;
- он требует свободных ресурсов RAM (для переменных состояния) и ROM (для кода проверки);
- скорость реакции на сбой - один период WDT, т.е. нельзя применять с оконными WDT;
- при неаккуратном программировании время кода проверок всех условий может оказаться затянутым.
==== Комбинированный метод ====
Иногда бывает удобно выхватить из каждого метода что-нибудь одно и использовать в комбинации с другим. Например, может понадобится написать функцию (скажем, автомат состояний), для которой сам алгоритм проверки состояний превратится в сложный ветвящийся комплекс операторов ветвлений и циклов. В таком случае может оказаться целесообразным не учитывать эту функцию в проверках внутри прерывания, а воспользоваться для нее методом обнуления с контролем функций или обнулением в цикле, т.е. в критических узлах функции расставить вызовы wdt_clear() (обязательно с проверкой текущего состояния). Это может усложнить анализ причин сброса, но иногда намного упрощает проработку алгоритма контроля выполнения программы.
При выборе любого метода обнуления WDT нужно помнить главные правила:
- нельзя обнулять WDT безусловно;
- проверки, определяющие, нужно ли обнулять WDT, должны производиться максимально быстро, чтобы не перегружать ядро микроконтроллера действиями, не относящимися к рабочей функции программы;
- те сбои, которые можно обнаружить программно, следует обнаружать программно и реагировать на них адекватно; WDT - последний рубеж.
===== Выбор интервала =====
При проектировании алгоритма обнуления WDT перед прораммистом встает вопрос выбора интревала WDT. Диапазон временных интервалов, проедоставляемый сторожевыми таймерами разных производителей, а также внутренним WDT, довольно широк: от единиц миллисекунд до единиц секунд (для внешних) или до нескольких суток (для втутренних). Интревал следует выбирать исходя из возлагаемых на WDT задач. Например, если мы планируем использовать WDT только для вывода МК из зависания, то редко могут понадобиться интервалы длиннее секунды-двух. Если же WDT будет использован для периодической активации контроллера, работающего в режиме пониженного энергопотребления, то тут, в зависимости от конкретной задачи, могут быть уместны интервалы в несколько часов или даже суток. В некоторых случаях целесообразно изменять интервал в ходе работы: например, его можно делать короче при выполнении ответственных функций или, наоборот, делать длиннее при переходе в режим пониженного энергопотребления.
Интервалы, касающиеся специфичных приемнений WDT (об этом ниже), выбираются по-своему в каждом случае, и тут нужно руководствоваться особенностями конкретного применения. То же самое касается интервала для SLEEP-режима: в зависимости от области применения может быть выбран различный интервал. А вот выбирая интервал для использования WDT в качестве монитора прораммы (т.е. фактически с точки зрения программы - интервала обработки процедуры обнуления WDT), следует руководствоваться тремя основными критериями:
* рекция на сбой должна быть максимально быстрой;
* время всех предварительных проверок для обнуления WDT должно быть несоизмеримо мало по сравнению с периодом;
* время выполнения участков кода, в течение которых принципиально невозможно осуществить процедуру обнуления WDT, должно быть намного меньше интервала обработки кода обнуления счетчика WDT.
=== Максимальная скорость реакции ===
Чем быстрее произойдет сброс контроллера после возникновения сбоя, тем лучше. Это и понятно, раз произошел сбой и программа начала выполняться по внештатному расписанию, т.е. перестала соответствовать спецификации, то нужно это прекратить как можно быстрее. Например, для программы, контролирующей целостность настила ступеней движущегося эскалатора, непозволительно находиться в зависшем состоянии в течение времени, соизмеримого со временем прохода одной ступени над датчиком.
Кроме того, нужно помнить, что зависшая программа - это далеко не всегда программа, которая ничего не делает. Бывает, конечно, что собьестя генерация кварца тактирующего микроконтроллер. Но в большинстве случаев программа продожает выполняться, причем она может не просто зациклиться на каком-то небольшом участке (по каким-то причинам не выполняется условие выхода из цикла), но у нее может сбиться указатель стека, в результате чего после выполнения первой же встретившейся инструкции RETURN программа продолжит выполнение с произвольного адреса (в стеке могли находиться данные, которые после сбоя указателя стека ошибочно были восприняты как адрес возврата).
=== Время выполнения проверок ===
Если программа является многопоточным приложением, многие процессы в которой живут большей частью собственной жизнью, то код проверок правильности состояния программы может оказаться сложным и выполняться довольно длительное время (в худшем случае может достигать десятков и сотен микросекунд). Становится очевидным, что если делать эти проверки сравнительно часто, то снизится эффективность самой программы. И не только из-за того, что 1% или 10% времени будет уходить на проверки, а еще и из-за того, что проверка состояния программы нарушает время ее реакции на какое-либо событие (особенно, если проверка находится в обработчике прерывания высокого уровня).
=== Критические участки кода ===
Программа может содержать участки кода, критичные к выполнению с точностью до такта, где инструкции для обнуления счетчика сторожеваого таймера будет просто некуда втиснуть. Сами инструкции обнуления вставить, возможно, удастся, но на код проверки правильности состояния программы может уже не остаться места, а без этого кода WDT может сослужить плохую службу (мы уже говорили о зависании в цикле с безусловным обнулением WDT). Таким кодом может оказаться, например, обработка какого-то внешнего синала, требующая точной выборки по времени. Проблема может быть частично решена применением DMA-модуля, если он есть. Но в общем случае надо учитывать наличие таких фрагментов кода при выборе интервала обнуления WDT.
Также стоит отметить, что некоторые контроллеры имеют возможность переключаться на резервный генератор при срыве генерации основного. Обычно резервный генератор имеет частоту на два-три порядка ниже частоты основного генератора (часто 32768 Гц). Обычно при переходе на резервный генератор генерируется немаскируемое прерывание, которое позволяет нам выполнить код по переводу устройства в резервный режим перед выполнением сброса. Но следует учесть, что этот код будет выполняться в сотни или тысячи раз медленнее, чем при тактировании от основного генератора, а тактирование сторожевого таймера при этом останется без изменений. И при неправильном выборе интервала сторожевого таймера или при неучете особенностей выполнения кода при тактировании от резервного генератора может получиться ситуация, в которой счетчик WDT успеет переполниться до того, как будут выполнены все действия по подготовке к сбросу. Т.е. в обработчике немаскируемого прерывания, возникающего при срыве генерации основного генератора, следует применять обнуление WDT по особым правилам. Разумеется, в таком случае контролировать правильность программы уже не имеет смысла, но кое-какие проверки перед обнулением WDT выполнять, скорее всего, придется. В данном случае можно применять метод обнуления с контролем отдельных функций.
===== Работа с библиотечными функциями =====
Отдельно стоит вопрос порядка обнуления сторожевого таймера, когда в программе используются библиотеки сторонних производителей. Сами библиотечные функции никогда не производят обнуление WDT, с другой стороны в общем случае неизвестно, сколько времени может потребоваться на выполнение какой-либо библиотечной функции. Простая строка:
printf(" %5.3f\n", sin(t));
может выполняться многие тысячи тактов, и даже обрамление ее вызовами wdt_clear():
wdt_clear();
printf(" %5.3f\n", sin(t));
wdt_clear();
не всегда может спасти от переполнения WDT. Выхода здесь два:
* увеличивать интервал сторожевого таймера;
* обнулять WDT в прерывании (с контролем состояния).
**Увеличение интервала** - довольно сомнительное решение в данном случае. Тому есть несколько причин:
- Интервал WDT в таком случае берется совершенно неподходящим к конкретной задаче. Причем надо помнить, что мы не можем просто взять такой интервал, при котором библиотечная функция успеет выполниться (помним, что в общем случае мы не знаем времени ее выполения при различных условиях). Нам приходится брать такой интервал, в течение которого мы можем позволить этой функции выполняться, т.е. с запасом, иногда не оправданно затянутым. В результате чего мы теряем в скорости реакции на сбой.
- Если мы, даже взяв интервал с запасом, все же ошибемся (в одной из миллиона комбинаций параметров функции время ее выполнения затянется), то произойдет неконтролируемый сброс контроллера, т.е. такой сброс, при котором у нас нет возможности перевести внешнее оборудование в безопасный режим.
**Обнуление в прерывании с контролем состояния** намного лучше подходит для решения этой задачи. Такой подход позволяет нам учесть длительное время выполнения быблиотечной функции, не увеличивая при этом интервал самого сторожевого таймера. Для этого нужно всего лишь завести глобальную переменную, в которую перед вызовом библиотечной функции можно записывать предполагаемое время ее выполнения с каким угодно запасом. А в обработчике прерывания к коду проверок состояния программы добавить обработку этой переменной.
volatile struct
{
UINT8 ucActive;
INT16 nCounter;
} g_WDT_Library;
void TIMER1_ISR (void)
{
static bool bAllowClearWDT;
...
bAllowClearWDT = true;
if (g_WDT_Library.ucActive == 0x55) // Проверяем, не выполнятеся ли сейчас библиотечная функция
{
if (g_WDT_Library.nCounter <= 0)
bAllowClearWDT = false;
else
g_WDT_Library.nCounter--;
}
...
if (bAllowClearWDT) wdt_clear();
...
}
Перед вызовом библиотечных функций, время выполнения которых вывает сомнения, переменная g_WDT_Library.nCounter устанавливается в соответствии с ожидаемым временем выполнения (это удобно организовать в виде функции, т.к. могут потребоваться действия для обеспечения атомарного доступа к счетчику):
void wdt_lib_start (UINT16 value)
{
g_WDT_Library.ucActive = 0xAA; // Блокируем моификацию счетчика в прервании (любое число != 0x55)
g_WDT_Library.nCounter = value;
g_WDT_Library.ucActive = 0x55; // Запускаем отсчет
}
void wdt_lib_stop (UINT16 value)
{
g_WDT_Library.ucActive = 0xAA; // останавливаем контроль WDT для библиотечной функции
// Пишем любое число != 0x55
}
void main (void)
{
...
while (1)
{
...
wdt_lib_start(100); // Выделяем 100 мс на выполнение функции
printf(" %5.3f\n", sin(t));
wdt_lib_stop();
...
}
}
===== Работа в программе под управлением RTOS =====
При работе программы под управлением RTOS порядок обнуления WDT несколько усложняется, из-за того, что потребуется следить сразу за несколькими процессами (задачами). Построение полноценного алгоритма обнуления WDT в случае использования RTOS - это совсем нетривиальная задача; сложности, с которыми мы сталкиваемся приведены ниже:
* Задачи подолгу могут находиться в режиме ожидания, не получая управления. В таких случаях факт невыполнения задачи в течение какого-то времени не может быть интерпретирован однозначно: мы не знаем, находится ли задача в ожидании или она была удалена или заблокирована по ошибке, или ждет сигнала от другой задачи, которая повисла.
* В программе появляется довольно много ответственных переменных, которые для программы неподконтрольны, и мы не можем гарантировать их целостность (помним, что сильная помеха может изменить состояние любого триггера): дескрипторы задач (например, мы не можем контролировать, правильно ли восстановился указатель стека), сообщения (сообщение может оказаться активным, но указывать вникуда), очереди и прочие объекты RTOS.
* Задачи редко живут своей жизнью, обычно они как-то синхронизированы с другими задачами, и, пытаясь установить факт правильности выполнения программы, мы должны проверять не только состояние процесса, но и соответствие его состоянию параллельных процессов.
Ввиду этих сложностей обычно ограничиваются построением простых индивидуальных тестов с контролем состояний для каждой активной задачи и выполняют обнуление при успешном прохождении их всех. Т.е. своего рода надстройка над этими тестами в виде обнуления с проверкой флагов. Причем в случае с RTOS эти проверки можно делать только в прерывании высокого приоритета (выше системного). Все более детальные проверки лучше реализовывать в виде run-time тестов. Это, во-первых, очень разгрузит код проверки условий обнуления WDT, а во-вторых, как уже было сказано, что сбой всегда предпочтительнее заметить на программном уровне, чтобы иметь возможность попытаться восстановить работоспособность или, в крайнем случае, произвести сброс безопасно, предварительно переведя все внешнее оборудование в безопасный режим. А сторожевому таймеру оставить функцию сброса только в самых критических ситуациях.
Но, даже реализуя проверки по упрощенной схеме, не стоит забывать о некоторых особенностях, превносимых RTOS:
* Т.к. все задачи делят одно ядро, то и время выполнения отдельных функций может быть затянуто из-за того, что задача может быть вытеснена более приоритетной, причем иногда - на неопределенное время.
* Все ожидания событий в задачах должны делаться только с аварийным выходом по таймауту. Это позволит не только упоярдочить проверку правильности выполнения задачи кодом управления WDT, но и сделает предсказуемым поведение программы для run-time тестов.
* Не все задачи постоянно активны: некоторые еще не созданы, некоторые уже удалены, некоторые - в глубоком сне. Это должно быть учтено проверками, например, в каждой структуре, контролирующей ход выполнения задачи, дожен быть выделен отдельный байт для сигнатуры, которая бы говорила коду проверки, что данна задача в проверке не участвует (см. пример работы с библиотечными функциями).
===== Тестирование WDT при включении питания =====
В особо ответственных системах WDT как контролирующий узел обязательно должен быть протестирован (иначе полагаться на него нельзя). Такие проверки предписывают делать европейский стандарт IEC 60730 (Annex H 11.12.7 п. 8) или американский стандарт UL1998 (A2.1 п.8). В зависимости от условий работы тестирование может производиться единожды при включении питания или с каким-то периодом, но не нарушая при этом целостность выполнения программы (т.е. в случае периодического тестирования перед проведением теста все внешнее оборудование должно быть переведено в безопасный режим, сохранены в энергонезависимую память данные, которые необходимо восстановить после перезагрузки.
Более детально тестирование WDT описано в "5.2 Самодиагностика", здесь повторю только основные принципы проверки:
- В оперативной памяти выделяется несколько байт для сигнатуры, отвечающей за стадию проведения теста.
- При рестарте программы в первую очередь проверяется эта сигнатура на соответствие контрольного слова заданному шаблону (примечание: шаблон хранится в ROM). Если оно:
- соответствует шаблону рабочего режима, а причиной сброса был WDT, то переходим к тестированию WDT (п. 3), иначе проверяем причины сброса (п.3).
- Если причиной сброса был сигнал от WDT, то тест считается успешно пройденным и можно переходить на п.8. Иначе повторяем проверку с п.4 (такая ситуация может случиться, если сброс микроконтроллера произошел по каким-то другим причинам во время проведения теста на WDT).
- Записать шаблон в сигнатуру.
- Запрограммировать WDT на короткий интервал. Лучше всего, если это будет интервал, используемый в программе. Если в программе используется длительный интервал, то для теста можно взять укороченный, но так, чтобы под контролем оказалось максимальное количество битов счетчика WDT.
- Настроить необходимый минимум периферии, чтобы не было висячих выводов.
- Повисаем в вечном цикле.
- Настраиваем WDT на рабочий интервал и продолжаем работу. В сигнатуру прописываем шаблон, соответствующий рабочему режиму.
===== Поведение программы при сбросе от WDT =====
Основной параграф, описывающий поведение программы при сбросе, это "5.5 Что делать при обнаружении сбоя". Здесь опишу только основные моменты, касающиеся сброса контроллера, вызванного переполнением WDT.
Все действия делятся на два этапа:
- протоколирование;
- восстановление работы.
=== Протоколирование ===
Установить причины сброса, вызванного переполнением WDT, программными средствами довольно затруднительно. Даже если мы проанаизируем контрольные переменные в памяти (такие переменные должны быть размещены в сегментах RAM, не подвергающихся начальной инициализации или обнулению; для этого в разных компиляторах есть специальные директивы), то в лучшем случае получим только последствия сбоя, т.е. несоответствие значений этих переменных предполагаемым, а саму причину таким способом не установить. Кроме того, все эти переменные могут содержать достоверные значения, т.к. сбой мог произойти из-за срыва генерации, или из-за изменения значения программного счетчика, или из-за изменения триггеров самого WDT (об этом ниже).
Но причины сбоя можно попытаться проанализировать вручную. Для этого программа должна иметь возможность при старте сохранять в энергонезависимую память или передавать по внешним каналам связи набор значимых переменных, регистров, а также участка стека. Все эти данные могут в дальнейшем помочь нам установить в каком месте программы и в каком ее состоянии произошел сбой. Причем по возможности при каждом сбое лучше сохранять все эти данные в разные участки энергонезависимой памяти. Это наберет статистикку сбоев и увеличит наши шансы установить причину, что особенно полезно, если причина была в недостаточно хорошо проработанном алгоритме программы, т.е. если сбой был вызван самой программой, а не внешними причинами. Но даже при невозможности сохранять данные в разные участки энергонезависимой памяти (например, если не позволяет ее свободный объем), то обязательно нужно сохранять счетчик сбоев. При обслуживании устройства по этому счетчику можно будет делать выводы о том, был ли это случайный сбой, или же систематический, что является сигналом к срочной переработке устройства.
Какие данные нужно сохранять:
* все те переменные, которые участвуют в проверках на возможность обнуления, т.е. те, которые являются индикаторами правильности работы программы;
* содержимое стека. Здесь есть три сложности:
- Некоторые компиляторы используют стек для обмена данными между функциями, в результате чего используемая часть стека сильно разрастается, что потребует большего объема энергонезависимой памяти для протоколирования;
- При работе программы под управлением вытесняющей RTOS будет уже несколько стеков, что также требует большего объема энергонезависимой памяти.
- некоторые контроллеры имеют аппаратный стек без возможности программного доступа.
* некоторые регистры микроконтроллера остаются неизменными после сбоя, и их тоже есть смысл сохранить;
* счетчик сбоев и причину сброса (в данном случае WDT).
* если есть возможность, то полезно сохранить дату и время.
Чем больше данных мы сохраним, тем больше возможностей установить причину сбоя и, если это возможно, устранить ее.
----
TODO:
- Восстановление работы -
- Зависание и преждевременный сброс WDT -
- RC-генератор и температура -
- Другие применения WDT -
Виктор Тимофеев, сентябрь 2010