Не раз и не два сталкивался с утверждением, что изучать ассемблер микроконтроллера это всего лишь пустая трата времени, дескать все можно сделать на Си, а если сильно надо то комманды можно и в даташите поглядеть.
Сейчас я одним маленьким примерчиком это утверждение зарою в землю, а сверху накрою могильной плитой.
Итак, есть у нас такой код (не ищите в нем практического смысла, я его просто как пример работы с разными операндами написал):
Человек ни разу не писавший под МК на ассемблере, начавший изучать МК сразу с Си, схавает такую конструкцию и даже не поморщится. Прожженый ассемблерщик же нецензурно матюгнется, обзовет первого быдлокодером и исправит исходник следующим образом:
А еще проверит не юзается ли где еще в прерываниях регистр TCCR0A. И внимательно посмотрит на работу с регистрами периферии.
В чем же дело? Какая разница между
Или между
и
Для программиста сишника в принципе никакой — и там и там какой то регистр или переменная. Единственное что это аппаратный регистры и поэтому могут меняться самопроизвольно, да переменная в прерывании меняется, поэтому идет с квалификатором volatile.
А для ассемблерщика между этими строками заложена огромная разница.
Во первых flag_byte расположена в памяти и это флаговый регистр, а изменение бита в памяти может быть только через чтение-модификацию-запись. Соответственно
Компилируется во что то вроде:
Во первых TCCR0A это аппаратный регистр, причем имеющий адрес в пределах 00-3F (адресное пространство на котором возможна работа команд IN/OUT), поэтому доступ к нему может быть через команду OUT, причем напрямую в регистр I/O сделать OUT константы нельзя, только через промежуточный регистр R16…R31 так что конструкция
Будет подобна предыдущей:
А вот регистр PORTB расположен в адресном пространстве от 00…1F, поэтому кроме команды OUT до него могут дотянуться также команды работы с битами CBI и SBI.
Так что конструкция
Скомпилируется в
А вот :
Может уже быть как
так и
В зависимости от числа одновременно меняемых битов и умности компилятора.
Да, это все здорово. Но к чему я это?
А к тому, что у нас тут есть еще и прерывание. В котором может быть изменены наши значения, а еще тот милый факт что прерывание, будучи разрешенным, может выскочить между двух любых инструкций. И при этом родить очень адский глюк которой может ВНЕЗАПНО вылезти через пару лет безглючной работы, а потом также бесследно исчезнуть, оставив разработчика задумчиво чесать репу на предмет «ЧТО ЭТО БЫЛО»? Часто тут грешат на глюки железа и дырявые процы. Хотя на самом то деле просто программа была корявая.
Покажу на примере, вот наш первоначальный код:
Программа выполняется себе, готовится записать бит options в переменную flag_byte…
А тут хопа, так фаза Луны совпала и вдруг пришло прерывание именно в этот момет. А что у нас в прерывании? Правильно! Сохранение регистров, что то вида::
А дальше наше тело прерывания:
Выставили мы битик rcv_buff и записали в ту же переменную флагов. А что потом? Правильно — возврат регистров из стека и возврат:
Куда возврат? Туда же где прервались:
Замечательно, а что же у нас в регистре R16 в данный момент?
Очевидно то же самое, что и до входа в прерывание — состояние флаговой переменной flag_byte. Вот только заковыка — это состояние флаговой переменной уже не актуально, устарело! Т.к. его только что изменило прерывание, выставив там какой то другой свой флаг. И теперь, произведя модификацию и запись у нас в flag_byte будет записана не актуальная инфа уже с двумя стоящими флагами rcv_buff и options, а восстановленная из «бэкапа» предыдущая копия с одним лишь options. А событие которое нам пыталось донести прерывание, выставив свой флаг, было забыто вообще.
И что самое гадкое такие ошибки фиг отловишь если не знаешь что такое может произойти. И могут они быть сразу, а могут коварно переждать в засаде весь процесс отладки, а когда девайс уйдет в серийное производство или на ответственный пост внезапно выскочат и хорошо если никто не умрет.
Лирическое отступление, можно пропустить:
Когда то давно отлаживал я программу одну, она была на ассемблере, но там я просто забыл что у меня есть переменная — адрес перехода. Которая может меняться и в прерывании тоже. Так вот, все работало идеально, но примерно на 30 минуте работы программа вставала колом или начинала гнать полную ересь. Я убил тогда на отладку около недели. Программа была довольно большая, там могло быть что угодно, начиная от переполнения стека до срыва очередей диспетчера задач. В ход пошла уже тяжелая артиллерия в лице логического анализатора и JTAG — без толку. Попробуй поймай багу на хрен знает какой итерации главного цикла, да еще при совпадении условий внешних воздействий в фиг знает каком порядке. Трассировать или отлаживать в протеусе это почти бесполезно.
Причем малейшие изменения в коде приводили к видоизменению баги. Добавил в код парочку NOP — клиническая картина резко меняется. Чудом мне удалось поймать момент, когда прогу перекашивало точно на 25 итерации главного цикла после отпускания кнопки. Выглядело это примерно так: послать в USART точно 10 байт, причем последний должен быть непременно «R», потом нажать кнопку и не отпускать пока не будет нажата вторая. И вот когда вторую отпустить прога встает колом.Дотрассировав до этого места в JTAG я наконец понял где собака порылась — адрес перехода состоял из двух байт и лежал в ОЗУ. Первый байт менялся в фоне программы, и мог быть изменен в прерывании. И вот когда это прерывание вклинивалось в между первым и вторым байтом при занесением нового адреса, то в результате получался гибрид состоящий из двух разных половинок адреса, ясен фиг что переход получался черти куда и выглядело это как срыв стека, хотя стек тут был вообще не причем. Впрочем, про характерную клиническую картину ошибок я тоже собираюсь потом написать.
Итак, враг найден. Но что же с ним делать? А делать тут собственно нечего. Единственный способ избежать этой напасти это либо не использовать раздельно используемые переменные, либо запрещать прерывания при их модификации в фоновой программе — делать атомарными.
Атомарная — значит не делимая. Не делимая прерыванием. Поэтому то команды запрета/разрешения прерываний
Спасают ситуацию.
А остальные? Они ведь тоже делаются не за одну операцию! Ну в данном случае в прерывании они не используются и поэтому пока можно спать спокойно, но держать на контроле надо.
А вот операции вроде SBI/CBI делаются за одну команду и являются атомарными от природы.
Страшно да? А ведь многие об этом даже не задумывались.
Да и в самом деле, что считать атомарным а что нет? Не закрывать же в конце концов параноидально все операции с разделяемыми переменными и IO регистрами командами cli/sei. Как никак прерывания вещь важная и созданная именно для того, чтобы реагировать быстро.
И, например, на AVR нельзя сделать атомарную операцию на регистры ввода вывода старше 1F — там просто нет команд логических операций на IO, а вот на PIC24 (а может и на более младших, хз не знаю я PIC) можно, через битовые маски по XOR, т.к. там есть команда xor по IO. Подробней можно прочитать в
Так что мало знать что есть вилы, надо уметь их обходить с минимальными потерями. Вот для этого то и надо хорошо знать ассемблер того контроллера на котором пишешь, на уровне хотя бы десятка — двух программ отличных от тупой мигалки светодиодом. Чтобы сразу за сишным кодом видеть возможные ассемблерные инструкции, знать что будет делать компилятор в том или ином случае. Где можно ожидать вилы в стоге сена.
Отлично, враг известен, чем его мочить тоже выяснили. Но любое оружие может замочить и хозяина. Если им неумело пользоваться. Поясню на примере:
Вот сделали мы какую нибудь функцию, а чтобы прерывания нам не нагадили мы добавили в них конструкцию cli/sei на критичные места. Все довольны, все счастливы. Ага, до тех пор пока в целях оптимизации и универсальности кода мы не вкатим эту функцию в обработчик прерывания. В принципе, ничего страшного нет, пользоваться в прерыванях функциями можно. Вот только надо учитывать что в прерываниях прерывания же аппаратно запрещены, а наша функция по выходу из критических мест их разрешает. И тут у нас вылезают другие вилы — вложенные прерывания. Это бага не столь законспирированная как неатомарный доступ, но вылезти тоже может ой как не сразу.
Что делать? А тут все просто. Если у нас есть функция с атомарными операциями и юзается она и в прерываниях и в основном теле программы, то надо бы это учитывать и не менять флаг прерывания зря. Т.е. если он был сброшен (в прерывании) то мы его и не возвращаем обратно — незачем.
На ассемблере это может выглядеть, например, так:
Разрешать прерывания тут не надо. Т.к. мы сохранили сразу весь SREG. Если прерывания были разрешены, то по доставании его из стека они достанутся разрешенными. Ну а если не были разрешены то ничего и не изменится.
На Си же можно тупо в лоб проверять условием наличие флага I регистра SREG и в зависимости от этого делать потом разрешение прерывания или нет. Запрещать их в любом случае. Либо применить инлайновый ассемблер своего Си компилятора.
А еще можно поискать уже готовые макросы в составе компилятора.
В WinAVR GCC например есть такой хидер как util/atomic.h
где есть макросы:
Где в качестве type возможны два варианта: ATOMIC_FORCEON — прерывания по выходу из атомарного блока будут включены и ATOMIC_RESTORESTATE в котором состояние флага I будет таким же какое и на входе в блок. Запрещены — так запрещены, разрешены так разрешены. Но работает чуть медленней и памяти требует больше.
Также там есть макрос разатомаривания.
Т.е. он делает обратную операцию — разрешает прерывания в своем чреве. Иногда удобней запретить прерывания во всей функции, но в центре разрешить, чем делать два атомарных блока в начале и в конце.
Синтаксис этих макросов такой же, а опции по аналогии NONATOMIC_RESTORESTATE — оставляет как было. NONATOMIC_FORCEOFF — принудительно выключает прерывания по выходу.
Так что можно вместо:
Написать
И это будет эквивалентно. Единственное что код потеряет часть переносимости. По хорошему бы вообще вынести все эти cli(); sei(); в отдельный хидер где заменить из на что нибудь вроде Interrupt_Enable(); Interrupt_Disable(); и тогда этот код можно будет перетащить на любой контроллер, лишь бы там был Си компилятор. А уж привязать к Interrupt_Enable(); местный аналог sei(); куда проще в одном файле в одном месте чем перелопачивать весь сырок.
З.Ы.
Также недавно появилась
Добавить комментарий