Продолжаем трактат об отладке программ. На этот раз в бой идут одни старики.
Осциллограф
Очень часто хочется в динамике поглядеть как работает программа. Особенно если ее структура сложней чем просто суперцикл. Если там конечные автоматы на прерываниях или разделение задач на флаговом автомате/очередном диспетчере, то аналитическое тупление в код и прогоны в отладчике мало что дадут — наслоение прерываний, процессов в очереди, стадии и взаимодействие разных конечных автоматов взорвут мозг кому угодно. А если еще программа не написана с нуля, а собрана из кучи чужих исходников, да даже если из своих, но древних и уже забытых.
Тут очень хорошо помогает осциллограф. Но сначала надо как то вывести сигнал на него. Для этого подойдет любая ножка которой мы можем дрыгать. Обычно для отладки оставляю парочку. На худой конец, можно на время отладки переназначить какой-либо вывод и использовать его.
Для примера возьму ту программу, что идет в доке к демоплате Pinboard. Там стоит мой диспетчер очереди, а также немного автоматики на прерываниях. Плюс старая библиотека для HD44780
В принципе все с виду работает пучком и никаких косяков. Но одолели меня сомнения, так ли это? Если двигатель вращается, то это еще не значит, что он работает хорошо и без ошибок. Так и у нас, надо проверить, все ли в порядке. Слушать мы будем осциллографом «механику» тиканья службы таймера. Почему его? Ну так вокруг него вся логика программы построена.
Переназначаем выводы PD4 и PD5 на выход и будем их использовать для своих грязных целей. Для отладки в смысле.
Поставим дрыг импульсом на прерывание таймерной службы и посмотрим насколько красиво и правильно тикает системный таймер. Дискретность которого 1мс. И который определяет почти все выдержки в программе. В общем, прописываем в таймерное прерывание такую байду:
1 2 3 4 5 6 | OutComp2Int: SBI BTA_P,BTB ; For Debug TimerService ; Служба таймера диспетчера CBI BTA_P,BTB ; For Debug RETI |
Вначале мы ставим бит порта PD4, а перед выходом сбрасываем. Это даст нам время выполнения и частоту выполнения. И тыкаемся нашим осциллографом, глядим:
Обана, а в таймере то глюки есть! Тикать то он тикает и с первого взгляда все нормально. Но почему то у него возникла аритмия и если это сейчас не вылезло, то может вылезти потом, при добавлении новых фич, где от четкости таймера будет многое зависеть. Причем глюк по времени плавающий, возникающий раз в сколько то сработок. А это значит, мы его трассировкой не поймаем. Можно, конечно, и аналитически протупить в код и найти, но это еще надо знать, что глюк есть. Да и код может быть такой, что сам черт ногу сломит.
Попробуем вычислить кто это нам тут таймер сбивает. Первое что приходит на ум — другое прерывание. Оно во время своей обработки запрещает другие прерывания и может сбить нам таймер. Что там у нас еще есть? Да хотя бы прерывание от АЦП. Выведем как его на вторую отладочную ногу:
1 2 3 4 5 6 7 8 9 10 | ADC_INT: SBI BTA_P,BTC ; For Debug PUSH OSRG ; Сохраняем рабчий регистр IN OSRG,ADCH ; Берем данные из АЦП STS ADC_Data,OSRG ; Перекладываем их в ОЗУ POP OSRG ; Восстанавливаем рабочий регистр CBI BTA_P,BTC ; For Debug RETI ; Выход из прерывания |
Это будет дрыганье ногой PD5.
Смотрим что получилось:
Как видно из видео, у нас есть проблема — где то в коде есть процедура которая блокирует прерывания. Причем это не атомарный доступ, слишком уж большие задержки. Скорей что то похожее на цикл ожидания, в котором есть CLI/SEI конструкция. Где то что то я забыл удалить после отладки. Начинаем проглядывать код уже на предмет забытых прерываний. Мелкие процедурки видны сразу, а крупные, состоящие из нескольких файлов и макросов можно и прощупать.
Давайте как пощупаем задачу вывода на экран, переместим дрыгалку туда:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | ;----------------------------------------------------------------------------- ; Задача обновления экрана дисплея. Дисплей обновляется 5 раз в секунду, каждые 200мс ; данные берутся из видеопамяти и фоном записываются в контроллер HD44780 Fill: SBI BTA_P,BTC ; ForDebug SetTimerTask TS_Fill,200 ; Самозацикливание задачи через диспетчер таймеров LCDCLR ; Очистка дисплея LDZ TextLine1 ; Взять в индекс Z адрес начала видеопамяти LDI Counter,DWIDTH ; Загрузить счетчик числом символов в строке Filling1: LD OSRG,Z+ ; Брать последовательно байты из видеопамяти, увеличивая индекс Z RCALL DATA_WR ; И передавать их дисплею DEC Counter ; Уменьшить счетчик. BRNE Filling1 ; Пока не будет 0 (строка не кончилась) - повторять LCD_COORD 0,1 ; Как кончится строка - перевести строку в дисплее LDI Counter,DWIDTH ; Опять загрузить длинной строки Filling2: LD OSRG,Z+ ; Адрес второй строки видеопамяти указывать не надо - они идут друг за другом RCALL DATA_WR ; И таким же макаром залить вторую строку из видеопамяти DEC Counter ; Уменьшаем счетчик BRNE Filling2 ; Если строка не кончилась - продолжаем. CBI BTA_P,BTC ; ForDebug RET |
И смотрим что получилось:
Аналоговый вариант данного действа.
Как видим, у нас в задаче Fill есть запрет прерываний, который на значительный период блокирует не только таймерную службу, но и прерывания от АЦП и все остальное. При этом у нас один тик таймерной службы откладывается до тех пор, пока прерывания не разрешат. ВЫполняется, а в этом время уже нащелкало на второй тик и прерывание выполняется еще раз. Пока катастрофы не случилось, но будь Fill подольше, то мы бы прозевали один тик. А какое то событие уплыло по времени.
Это черевато тем, что разные синхронизированные по времени задачи могут глючить, причем глючить не всегда, а только тогда, когда на них выпадает совпадение по времени с Fill. Попробуй такое отловить. Тем более по дефолту то считаем, что Fill работает и не глючит! Ведь раньше то ничего не мешало! Ну ну. Просто условия не создавались нужные.
Лезем в lcd4.asm и находим там запреты прерываний почти везде:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | ;========================================================================================= BusyWait: CLI ; Ожидание флага занятости контроллера дисплея RCALL PortIn ; Порты на вход CBI CMD_PORT,RS ; Идет Команда! SBI CMD_PORT,RW ; Чтение! BusyLoop: SBI CMD_PORT,E ; Поднять строб RCALL LCD_Delay ; Подождать IN R16,DATA_PIN ; Считать байт PUSH R16 ; Сохранить его в стек. Дело в том, что у нас R16 убивается в LCD_Delay CBI CMD_PORT,E ; Бросить строб - первый цикл (старший полубайт) RCALL LCD_Delay ; Подождем маленько SBI CMD_PORT,E ; Поднимем строб RCALL LCD_Delay ; Подождем CBI CMD_PORT,E ; Опустим строб- нужно для пропуска второго полубайта RCALL LCD_Delay ; Задержка снова POP R16 ; А теперь достаем сныканый байт - в нем наш флаг. Может быть. ANDI R16,0x80 ; Продавливаем по маске. Есть флаг? BRNE BusyLoop ; Если нет, то переход BusyNo: SEI ; Разрешаем прерывания. RET |
И это только в процедуре ожидания флага готовности дисплея. Дальше оно есть и в процедуре чтения, записи и по всей библиотеке. Самое страшное, конечно, это в BusyWait т.к. оно зацикливается. Поэтому то и на половине задачи прерывания и срубались.
Е..й стыд! И я когда то это написал!? Нет, тогда запрет прерываний был оправдан. Я уже не помню почему, но без него в том случае было никак. Но вот переносить эти CLI/SEI в универсальную библиотеку это был полный маразм. Кстати, многие кто ее юзал отметили эту фишку с ненужным запретом прерываний. Многие, но далеко не все. Колитесь, кто не заглядывал в код и подключиле его к своей программе «as is» поленившись проверять и поверив на слово? ;) Все руки не доходят поправить ;)
Правим, заливаем в демоплату новую прошивку. Наблюдаем эффект:
Ничего не сломалось, все отлично работает. Done!
Ну и, напоследок, маленький трюк. Я тут вовсю юзаю многоканальный осцилл, а у меня тут еще и 16ти канальный анализатор есть… В общем, полный набор удовольствий. А кто то мучается с одноканальным осциллографом и как тут ему быть?
Не беда — можно применить одну хитрость. Обьединить несколько сигналов в один канал. Делается это вот по такой схеме:
![]() |
Работает просто — когда на одном входе единичка, то напряжение с нее течет в землю. Этот ток создает падение напряжения на R3 которое видно на осциллографе. Когда на входе две единички, то через R3 уже идет ток с двух источников и падение напряжения становится суммой двух сигналов и это явно видно. Значения резисторов R1 и R2 лучше подбирать так, чтобы они были в пределах одного порядка, но различались раза в полтора-два. Это даст визуальное разделение сигналов и можно будет понять что к чему.
У меня же на демоплате я решил заюзать те резисторы, что уже стояли — 500омные на ограничение тока для светодиодов. А в качестве суммирующего применил один из переменников, выкрутив его на сопротивление около 800Ом. И с него отправил сигнал на осциллографы.
Резисторы там правда правда одинаковые и сигналы будут равные по высоте, но тут у нас по частоте уже понятно кто есть кто, так что не запутаемся :)
Вот что получилось:
Таким образом, можно на каждый канал загнать столько сигналов, что в экран не влезут. Правда анализировать их на глазок будет уже сложней. Но где наша не пропадала! :)
Игры с синхронизацией
Также не стоит забывать про такой канал как внешняя синхронизация (Ext Trig) Обычно на него забивают т.к. не находят ему применения. А зря! Например, можно на него вывести какой либо сигнал который тоже надо ловить. И настроить осцил в такой режим, что без этого сигнала триггер не срабатывает (Normal режим, не Auto). И тогда если сигнала нет, то мы ничего и не увидим. Отсутствие информации тоже информация — о том, что сигнала нет :) А если есть, то у нас останутся еще два свободных канала на отлов последующих событий.
Также вовсю стоит играться с разными видами синхры. Аналоговые и самые дешевые цифровые ограничены только синхрой по фронтам, да всяким телесигналам. А вот более продвинутые цифровые, вроде ATTEN или тот же RIGOL (про Лекрои и прочие Тектроники я даже не вспоминаю) умеют синхронизироваться и по длине импульса, по расстоянию между импульсами ,по скорости нарастания/спада сигнала и еще по полудесятку разных параметров. Ловить ими сбои в работе программ милое дело! Раз уж раззорились на цифроосцил, так юзайте его возможности на все сто! :)))
> то аналитическое тупление в код
Хочется обратить внимание на общепринятую терминалогию. «Аналитическое тупление» называется «Статический анализ кода».
По теме тестирования программного обеспечения выпущено много книг, а сама тема уже очень стара :)
* терминологию
И вопрос не по теме: где можно посмотреть справку по правилам написания комментариев? Как вставлять ссылки, картинки, цитаты…
Никак. В комментах разрешен только тэг для ссылок.
А картинки как вставлять? Я знаю, это можно сделать :)
Никак. Если сильно надо, то я это делаю вручную. Я могу вставлять в комменты любые тэги.
Тема стара как самый первый компьютер. Но тем не менее актуальности не теряет.
Я в том смысле, что есть много литературы по этой теме и устоявшаяся терминология.
То, что это будет актуально всегда — без сомнений :)
Статический анализ кода звучит уж больно сухо. Пока выговоришь это уснешь :)
:)
А как, например, по другому назвать «Регрессивное тестирование»?
Так, чтобы сразу было понятно о чем речь. Название «Регрессивное тестирование» смысла несет мало и без пояснений чо это такое непосвященный не поймет. Оно и нагугливается то не с первого раза.
Ликвидация последствий прошлой отладки. Как то так.
Не, смысл у него другой: есть успешно протестированные части программы, которые после внесения изменений в программу нужно тестировать заново.
Также называют регрессионным тестированием, есть пояснение на Wiki (ссылка).
to abelankov
Какие конкретно книги можете посоветовать по отладке программ?
—
С Уважением Михаил Доронин
Ди, а что это за зелёный светящийся дисплей? Я тож такой хочу. :)
WH1602B-YMI-CTK
Красивый, ага. Но дорогой падла. Рублей 300-400 стоил.
Я себе этим летом собирал что-то типа велокомпьютера. Там стоял WH1602 с обычной подсветкой и чёрными символами. Вечером, когда было темно, разглядеть что-то на этом дисплейчике было сложно. Особенно, если едешь с большой скоростью.
Поэтому светящиеся символы того стоят :)
А что это там тот широкий импульс (от таймера кажись) имеет джитер заднего фронта. То есть прерывание его бывает разное по времени? Если я смог разглядеть там около неск. микросекунд.
Там зависит от того обрабатывает таймер задачу или нет. Если вхолостую тикает то одно время, а если какая то задача дотикала до выполнения, то о нее ставит в очередь и тратит на это несколько больше времени.
Спасибо, очень интересно. Ждем продолжения курса AVR.
Ну как бы это, я использовал библиотеку для LSD. Я видел там сплошные CLI и SEI, даже спрашивал про них у DIHALTA в посте про LCD. Но в своем проекте их просто убрал и точка.
Вот и получится LSD вместо ЖК-дисплея (LCD). Наверняка они там для атомарности операций, чтобы прерывания не порушили логику работы. Впрочем, если прерывания не используются, можно и убрать.
Очень полезная статья!
Век живи-век учись! Буду теперь всегда контролировать рывки от таймеров осциллографом на наличие «левых» импульсов.