6432 subscribers

Ошибки допускают все. Исключений не бывает.

4,2k full reads
Изображение с сайта zerut.ru
Изображение с сайта zerut.ru

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

Я написал две статьи посвященные ошибками. "Ругать или предлагать анализ и решение? О критике старых электронных схем." и "Ошибки бывают и в промышленных системах". В комментариях к этим статьям некоторые задавали вопрос:

Других критиковать легко, а сам то? Ни разу не ошибался что ли?

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

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

Предметная ситуация

Для одной небольшой разработки, по просьбе знакомых, мне потребовалось формировать последовательность импульсов. Что это за разработка я опишу в отдельной статье, она может быть интересной, но отношения к теме ошибки не имеет. Эта последовательность импульсов не была особо сложной, но ее формирование на дискретных элементах, например, таймерах, было не таким простым и требовало применения нескольких микросхем. Поэтому было принято решение использовать простенький микроконтроллер. Нужно отметить, что это было единичное изделие. То есть сделал, отдал, забыл.

Мне нравятся 8-битные микроконтроллеры PIC Microchip, поэтому я выбрал PIC10F200 в корпусе SOT23-6. Это очень простой микроконтроллер, в котором есть всего один 8-битный таймер, а программа не может быть длиннее 255 команд. Большего мне и не требовалось. Зато этот микроконтроллер дешев и имеет удобный корпус.

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

8-битный таймер Timer0 есть во всех микроконтроллерах Microchip. Вот его блок-схема для PIC10F200 (и всех прочих микроконтроллеров Baseline)

Блок-схема Timer0 микроконтроллеров PIC Microchip семейства Baseline (из документации Microchip)
Блок-схема Timer0 микроконтроллеров PIC Microchip семейства Baseline (из документации Microchip)

Здесь для нас важно лишь то, что таймер состоит из 8-битного регистра-счетчика TMR0 на вход которого можно подавать через предварительный делитель (Prescaler) тактовые импульсы от системного тактового генератора микроконтроллера. Каждый дошедший до TMR0 импульс вызывает увеличение его содержимого на 1, то есть, инкрементирует этот регистр. Когда содержимое регистра достигнет максимального значения FF, следующий пришедший импульс вызовет переполнение регистра и его содержимое обнулится, но сам процесс счета остановлен не будет.

В более мощных микроконтроллерах факт переполнения регистра-счетчика таймера учитывается в отдельном регистре в виде флага T0IF, который можно проверять и изменять программно. Но для семейства Baseline факт переполнения таймера можно определить только проверив регистр TMR0 на равенство нулю.

8-битные микроконтроллеры PIC Microchip (я сейчас не говорю о семействе PIC18) относятся к RISC микроконтроллерам (процессорам), то есть имеют сокращенный набор команд. Команда проверки содержимого регистра для этих микроконтроллеров отсутствует. Простейшим способом проверить регистр на нуль без его изменения является команда

movf f,f

Эта команда выполняет пересылку содержимого регистра f в него же. То есть содержимое регистра не изменяется, зато устанавливается флаг Z в регистре STATUS, если содержимое регистра нулевое.

Это настолько стандартная практика проверки регистра на 0, что следующий фрагмент пишут почти не задумываясь

Пример проверки содержимого регистра на 0
Пример проверки содержимого регистра на 0

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

Команда btfss (Bit Test F, Skip if Set) выполняет проверку указанного бита (в примере бит Z) в указанном регистре (в примере регистр STATUS). Если бит установлен, то следующая за btfss команда пропускается. Для команды btfsc (Bit Test F, Skip if Clear) следующая команда будет пропущена если бит сброшен.

Собственно ошибка

Ну вот мы и добрались до самого интересного.

Что бы сформировать временной интервал мы должны установить коэффициент деления для Prescaler. Внутренний генератор микроконтроллера работает на частоте 4 МГц, но выполнение каждой команды занимает 4 такта. Таким образом, машинный цикл составляет 1 мкс (частота 1 МГц). Нам нужны миллисекундные интервалы, причем особая точность не требовалась, поэтому я выбрал максимально возможный коэффициент деления 1:256.

Теперь инкремент регистра TMR0 у нас выполняется каждые 256 мкс. Мне нужна длительность импульса порядка 40 мс, поэтому в регистр TMR0 будем загружать -160. Да, именно минус, так как регистр таймера инкрементируется. Остается только дождаться обнуления TMR0 и нужный временной интервал будет сформирован.

Без установки коэффициента деления фрагмент программы выглядит так

Формирование задержки на 40 мс. Здесь есть ОШИБКА!
Формирование задержки на 40 мс. Здесь есть ОШИБКА!

Это фрагмент программы с ОШИБКОЙ! Нет, все ранее приведенные рассуждения верны, но вот в реализации есть критическая ошибка! Видите ее? Подскажу, она в команде movf. Все еще не видно? Цикл ожидания обнуления регистра таймера никогда не кончится! В чем же дело?

В чем же ошибка?

Поиск этой ошибки занял некоторое время. Дело в том, что я очень редко работаю с микроконтроллерами семейства Baseline, поэтому такая вот проверка и ожидание переполнения таймера тоже требуется крайне редко. Обычно просто проверяется состояние флага переполнения соответствующего таймера. А с точки зрения компилятора, да и опыта программирования для PIC, код абсолютно корректен. А вот память человеческая, в данном случае моя, иногда упускает из виду ключевые, но кажущиеся малозначительными, подробности. Так в чем же дело в данном случае?

А дело в том, что предварительный делитель, тот самый Prescaler, в микроконтроллере тоже реализован в виде счетчика. При этом каждая операция записи в регистр TMR0 вызывает сброс счетчика Prescaler (содержимого, а не коэффициента деления), то есть, отсчет начинается сначала. Дополнительно, инкремент запрещается в течении 2 машинных циклов после каждой записи в TMR0. Теперь понимаете в чем ошибка? Все еще нет?

Команда

movf TMR0,f

выполняет считывание содержимого регистра TMR0, что нам и требуется, и его ЗАПИСЬ обратно в регистр. А это приводит к сбросу счетчика предварительного делителя. Наш фрагмент программы существенно короче 256 команд, поэтому на выходе Prescaler никогда не будет сформирован импульс инкремента регистра-счетчика таймера. Вот она, ОШИБКА!

А как влияет запрет инкремента в течении 2 циклов после записи в регистр таймера? Конечно, влияет но в нашем случает эта проблема не успела проявиться. Большинство команд в 8-битных микроконтроллерах PIC (я не говорю про PIC18) выполняется за один машинный цикл. Исключение составляют команды переходов и вызова подпрограмм, которые выполняются за 2 цикла. Команды btfss и btfsc выполняются за 1 цикл, если следующая команда не пропускается, и за 2 цикла, если пропускается. В нашем случае, если временной интервал еще не истек, команда btfss будет выполняться один цикл, а команда goto два цикла. То есть, всего 3 цикла между проверками регистра TMR0. Поэтому шансы на инкремент у таймера все таки есть. Если не сбрасывать Prescaler.

Что делать? Как исправить ошибку? Мы должны избегать записи в регистр TMR0 во время проверки. Это возможно только в том случае, если результат использующейся для проверки его содержимого команды записывается куда то еще, а не в регистр TMR0. На самом деле вариантов у нас не много. Самым простым будет замена столь привычной команды

movf TMR0,f

на

movf TMR0,w

но это приведет к порче содержимого рабочего регистра WREG. Если это недопустимо, то WREG нужно предварительно где-нибудь сохранить. Итак, фрагмент программы без ошибки будет выглядеть так

Формирование задержки на 40 мс. Здесь ошибка исправлена
Формирование задержки на 40 мс. Здесь ошибка исправлена

Вот теперь все правильно и работает именно так, как требуется.

Почему эта ошибка стала возможной?

Как я уже написал, эта ошибка вызвана способностью человеческой памяти забывать отдельные, иногда очень важные, подробности. Если человек сталкивается (работает) с чем то довольно редко, то детали часто забываются, но "в целом" все кажется отлично запомненным и понимаемым.

Работа с таймерами в микроконтроллерах довольно проста и стандартна. Настроили. Загрузили в счетчик нужно значение. Подождали окончания счета. Автоматическое обнуление счетчика предварительного делителя при записи в регистр-счетчик таймера абсолютно естественно и логично. Оно выполняется не только для 8-битного Timer0, но и для 16-битных таймеров, например, Timer1. Иначе просто нельзя. И это упрощает работу с таймерами когда переполнение можно отследить опрашивая соответствующий бит или используя прерывания (в Baseline нет прерываний). Это настолько естественно, что на него не обращают внимания, как мы часто не замечаем, что дышим, или что у нас бьется сердце.

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

Заключение, или немного пофилософствуем

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

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

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

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

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

Бывают ошибки и чисто человеческие, разной степени фатальности. Я говорю об ошибках в человеческих отношениях, в выборе пути (не только жизненного), в принятии повседневных решений...

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

Пожалуй, подведу итоги...

  • Ошибаться плохо, но избежать этого практически не возможно. Особенно, во время разработки или обучения.
  • Нужно опасаться не столько ошибок, сколько не выявленных ошибок и их последствий, когда они попадают в готовое изделие. Да, я говорю о технических ошибках. Тестировать нужно максимально тщательно. Опытная эксплуатация необходима.
  • НЕДОПУСТИМО отказываться признавать и исправлять свои ошибки!
  • Помните, на ошибках учатся. Всегда изучайте причину появления своей ошибки и собственно ошибку. Во всех аспектах. Если ошибка от недостатка, или неточности, знаний, то нужно восполнять пробелы. Если ошибка от невнимательности, то стараться в дальнейшем быть внимательнее. Если от небрежности, то стараться быть аккуратнее и точнее. Помните, все, что нас не убивает, делает нас сильнее. Это касается и наших ошибок, в том числе. Если на них учиться, конечно.
  • Учитесь не только на своих ошибках, но и на чужих. Отличная схема или программа, которые вы нашли и использовали, безусловно, помогут вам. Но еще больше могут помочь схема или программа с ошибкой, когда эта ошибка найдена и описана самим автором или кем то еще. И втройне полезнее, если ошибку найдете и исправите вы сами. Это кажется парадоксальным, но это дает вам опыт. А это самое ценное.
  • Ну и последний пункт, для новичков, для тех, кто еще учится. Не бойтесь своей неопытности, опыт дело наживное! Все когда то были начинающими и все допускали ошибки, иногда еще глупее ваших. Если вы будете действительно стремиться к знаниям, если будете упорно трудиться (а это бывает ох как не легко иногда), то без сомнения сможете стать отличными специалистами! Но при этом, став специалистами, не забывайте времена, когда сами были начинающими и неопытными.