Лекция
Привет, Вы узнаете о том , что такое avr, Разберем основные их виды и особенности использования. Еще будет много подробных примеров и описаний. Для того чтобы лучше понимать что такое avr, архитектура программ , настоятельно рекомендую прочитать все из категории Цифровые устройства. Микропроцессоры и микроконтроллеры. принципы работы ЭВМ.
Все учебные курсы по микроконтроллерам которые я встречал (в том числе, к сожалению, и мой ассемблерный, но я надеюсь это постепенно поправить) страдают одной и той же проблемой.
В курсе бросаются строить дом не заложив фундамент. Только показав на примере как мигнуть светодиодом, сразу же кидаются в периферию. Начинают осваивать ШИМ-ы, таймеры, подключать дисплеи и всякие термодатчики.
С одной стороны это понятно — хочется действа и результата мгновенно. С другой — рано или поздно такой подход упрется в тот факт, что программа, надстраиваемая без четкой идеологии, просто обрушится под своей сложностью. Сделав невозможным дальнейшее развитие.
Итогом становится либо рождение жутких программных уродцев, либо миллионы вопросов на форуме вида «а как бы мне все сделать одновременно, а то частоты контроллера уже на все не хватает».
Самое интересное, что правильной организации программы учат программистов в ВУЗах, но вот только к микроконтроллеру народ обычно идет не от программирования, а от железа. А, как показала практика обучения в ВУЗе, электронщиков толковому программингу практически не обучают :( Приходится все додумывать самостоятельно.
Итак, что такое структура программы. Это, прежде всего, ее скелет. То какими путями движется код. Как организованы переходы между задачами прошивки. То как распределяется процессорное время. Без краткого ликбеза по общим принципам построения прошивки дальше двигаться нет смысла.
Все ниже написанное это лишь продукт моих умозаключений, поэтому терминология может отличаться от общепринятой. Если это сильно кому то будет резать глаз — поправляйте в комментах.
Итак, я для себя выделяю следующие структуры, по порядку возростания сложности конструкции и количеству управляющего кода:
А теперь подробно по каждому пункту:
Суперцикл
Самая простая организация программы. Отличается минимальным количеством управляющего кода (код который не выполняет полезную работу, а служит лишь для организации правильного порядка действий). Идеально подходит для задач из серии «помигать диодом».
Алгоритм прост как мычание (псевдо код):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void main(void); { while(1) { Led_ON(); Delay(1000); Led_OFF(); u=KeyScan(); switch(u) { case 1: Action1(); case 2: Action2(); case 3: Action3(); } } } |
И все примерно в таком духе. Т.е. тупое последовательное выполнение кода. Задержка — прям там же, в общей куче. Опрос клавиатуры. Тут же. Внутри суперцикла могут быть переходы и ветвления, но суть остается та же самая — тупое движение по коду.
Достоинства очевидны сразу логичность и простота (поначалу, потом это ад). Недостатки тоже вылазят сразу же — чем больше мы запихнем в наш код, тем он будет более неповоротливым и тормозным.
Однако суперцикл поддается оптимизации. Например, в функцию Delay() можно не тупо впустую щелкать тактами, а запихать туда опрос клавиатуры и еще кучу полезного экшна.
В итоге, на суперцикле можно сделать сверх компактную, надежную, но при этом совершенно монолитную программу. В нее будет нельзя ни добавить ни отнять. А еще чтобы все учесть, написать и отладить надо быть гением. Читали историю про Один байт? Вот это наверняка про нее или про следующий вариант.
Обычно народ, наигравшись с суперциклом, раскуривает прерывания и быстро переходит к варианту суперцикл+прерывания.
Суперцикл+прерывания
Чистый суперцикл используется крайне редко, потому как в любом микроконтроллере куча периферии и у них есть такая удобная вещь как прерывания (Хотя, я встречал на форуме microchip.ru высказывание что прерывания снижают надежность программы и лучше избавляться от них по возможности).
Здесь ситуация становится несколько иной. За счет прерываний у нас появляются параллельные процессы. Например, мы можем повесить опрос клавиатуры и мигание лампочкой на прерывание по таймеру (псевдокод):
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 32 33 34 35 36 |
ISR(Timer1) { u=KeyScan(); } ISR(Timer2) { if(LED_ON) { Led_OFF(); } else { Led_ON(); } } void main(void); { Timer1_Delay=100; // Выдержка таймера1 100 Timer1=1<<ON; // Включить таймер1 Timer2_Delay=1000; // Выдержка таймера2 1000 Timer2=1<<ON; // Включить таймер2 SEI(); // Разрешить прерывания while(1) { switch(u) { case 1: Action1(); case 2: Action2(); case 3: Action3(); } } } |
Уже лучше. Об этом говорит сайт https://intellect.icu . Вроде как программа теперь крутится тремя независимыми процессами. Нигде не тормозит и все в порядке. Да, до тех пор пока хватает аппаратных ресурсов. У нас ведь что тут получается — на каждый процесс по прерыванию. А таймеров то всего ничего и прерывания грузить тяжелыми по времени процессами нельзя — они должны быть быстрыми.
И вот именно в этом месте сон разума начинает рождать чудовищ. Появляются какие то дополнительные условия, проверки. На одно прерывание вешается куча разных событий, проверка флажков, попытка все это дело увязать… В результате конструкция превращается в макаронный клубок из переходов, вызовов, условий… В котором что либо изменить или понять невозможно. Не имея фундамента, конструкция рушится под своим весом. Разрушая мозг своему создателю. Начинается анархия…
Что делать? А что делать когда возникает анархия? Правильно! Нужно какое то единое правление — бюрократический аппарат. С теоретической точки зрения от него толку как от козла молока — клавиатуры он не опрашивает, ножками не дрыгает, данные не шлет. Только место занимает и процессорное время жрет. Но без него как без рук.
Флаговый автомат
Первый управляющий механизм это классический флаговый автомат. Реализаций его существует миллион, каждый придумывает свой собственный вариант, но разницы между ними большой нет.
Итак, в суперциклах у нас задачи вызывались одна за другой. Либо в порядке общей очереди, либо в виде прямого вызова. Очевидно, что эта цепь получается неразрывной и если в ней есть затыки (например какой нибудь DELAY), то встает вся цепь.
Флаговый автомат позволяет разорвать эту цепь.
Вот его простейший вариант.
Сначала мы определяем флаги. Обычно это битовое поле, где в одном байте насовано несколько значащих битов. Каждый за что нибудь да отвечает. Особые извращенцы под каждый флаг заюзывают по байту.
Пусть у нас будет байт flag, а в нем разные биты, выделяемые на нашем псевдокоде точкой. Итак, выглядит примерно это так (псевдокод):
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 |
void main(void) { //Start Task flag.ScanKey=1; //Запустить сканирование клавиатуры flag.Led_On=1; // Запустить мигалку while(1) { if(flag.keyscan==1) { u=ScanKey(); switch(u) { case 1: flag.Action1=1; // Поставить задачу1 на выполнение case 2: flag.Action2=1; // Поставить задачу2 на выполнение case 3: flag.Action3=1; // Поставить задачу2 на выполнение } } if(flag.led_On==1) Led_ON(); // Если стоит флаг включения - включить if(flag.Led_Off==1) Led_OFF(); // Если стоит флаг выключения - выключить if(flag.Action1==1) Action1(); // Если надо выполнить задачу 1 - выполнить if(flag.Action2==1) Action2(); // Если надо выполнить задачу 2 - выполнить if(flag.Action3==1) Action3(); // Если надо выполнить задачу 3 - выполнить } } |
Задача может выглядеть примерно так (всевдокод):
1 2 3 4 5 6 7 |
void Action1(void) { flag.Action1=0; // Задача выполнена. Сбросить флаг DoSomeThing(); // Делаем что то полезное if(Some_Event) flag.Action3=1; // Если какое то условие выполнено, то поставим на выполнение // Задачу 3 } |
Как видишь, тут нет прямой передачи управления между блоками. Все делается через флаги. Надо запустить задачу мы не передаем ей управление, а ставим флаг ее запуска. И при следующей итерации главного цикла задача будет выполнена, а флаг сброшен (или не сброшен, если задача запущена на циклическое исполнение).
Наращивание функционала идет без особых усилий — мы просто добавляем новые флаги и новые секции if(flag.****==1) { }
Только у нас лампочка должна была там мигать. Что делать с разнокалиберными временными задержками? Ведь флаговый автомат эту проблему так и не решил. Да, сам по себе флаговый автомат бесполезен. До тех пор пока на арену не вылазит…
Программный таймер
Могучее средство работы с временными задержками. Позволяет с помощью всего одного аппаратного таймера легко держать под контролем прорву задержек разной длительности.
Для начала надо сконфигурировать системный таймер, чтобы он генерировал нам системные тики. У меня за один тик принята 1мс. Поэтому я настраиваю таймер таким образом, чтобы он мне каждую миллисекунду генерировал прерывание. А вот в его прерывании мы и учиним беспредел.
Программный таймер обычно делается в виде массива структур (псевдокод):
1 2 3 4 5 6 |
volatile static struct // Глобальная переменная { Number; // Номер флага в флаговом байте Time; // Выдержка в мс } SoftTimer[Max_Numbers_of_Timer]; // Очередь таймеров |
А в прерывании по таймеру гоним примерно следующий код (псевдокод):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
ISR(Timer1) { for(i=0;i!=Max_Numbers_of_Timer;i++) // Прочесываем очередь таймеров { if(SoftTimer[i].Number == 255 ) continue; // Если нашли пустышку - следующая итерация if(SoftTimer[i].Time !=0) // Если таймер не выщелкал, то щелкаем еще раз. { SoftTimer[i].Time --; // Уменьшаем число в ячейке если не конец. } else { flags |= 1<<Number ; // Дощелкали до нуля? Взводим флаг в флаговом байте SoftTimer[i].Number = 255; // А в ячейку пишем затычку -- таймер пуст. } } } |
Видишь, все просто. Мы прочесываем массив структур таймеров, поэлементно. Если в поле Number у нас 255, то очевидно что это пустой таймер. Т.к. номер флага таким быть не может (в качестве номера флага может быть только число с одним единичным битом). Такой таймерный слот пропускается.
Если таймер не пуст, то мы проверяем поле Time на ноль. Если не ноль, то уменьшаем и переходим к следующему элементу массива.
А коли таймер дощелкал, то мы устанавливаем взводим во флаговом регистре бит лежащий в поле Number. И при следующем прогоне главного цикла управляющая конструкция if(flag.***==1) Запустит нам нужную задачу.
Число таймеров определяется исходя из числа одновременно отсчитываемых временных интервалов. Мне обычно хватает десятка. Если сделать меньше — может не хватит и получишь срыв таймерной очереди. Если сделать с большим запасом, то очередь будет дольше обрабатываться в прерывании, а это снижает точность отсчетов интервалов, да и вообще возникает лишний затык.
Ставится таймер откуда угодно функцией SetTimer примерно такого вида (псевдокод):
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 |
void SetTimer(NewNumber,NewTime) { InterruptDisable(); // Запрещаем прерывания. Помним об атомарном доступе! for(i=0;i!=Max_Numbers_of_Timer;i++) //Прочесываем очередь таймеров. Ищем нет ли уже там такого { if(SoftTimer[i].Number == NewNumber) // Если уже есть запись с таким флагом { SoftTimer[i].Time = NewTime; // Перезаписываем ей выдержку InterruptRestore(); return; // Выходим. } } for(i=0;i!=Max_Numbers_of_Timer;i++) //Если не находим, то ищем любой пустой { if (SoftTimer[i].Number == 255) { SoftTimer[i].Number = NewNumber; // Заполняем поле флага SoftTimer[i].Time = NewTime; // И поле выдержки времени InterruptRestore(); return; // Выход. } } InterruptRestore(); // Восстанавливаем прерывания как было. // тут можно сделать return c кодом ошибки - нет свободных таймеров } |
Работает тоже не сложно.
На входе у нас два значения — время NewTime и флаг который мы должны воткнуть.
Мы прочесываем нашу очередь таймеров, в поисках свободной ячейки либо таймера на то же событие с еще не истекшим сроком. Если находим такой же таймер — апдейтим его на новое время. Если не находим, то втыкаем данные впервую свободную ячейку. А если не нашли и свободной ячейки, то получаем Timer Fail. Это ошибка, ее надо учитывать делая очередь с запасом, либо точно высчитывая сколько таймеров нам надо.
Теперь, вооруженные знанием о софтверных таймерах, рассмотрим как будет организована наша мигалка (псевдокод):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void LED_ON(void) { flag.led_On=0; // Отработали, флаг можно сбросить SET_BIT_LED(); // Собственно, зажгли что то там SetTimer(OFF_LED_FLAG,1000); // Поставить флаг на погашение через 1с } void LED_OFF(void) { flag.led_Off=0; // Отработали, флаг можно сбросить CLR_BIT_LED(); // Собственно, погасили что то там SetTimer(ON_LED_FLAG,1000); // Поставить флаг на зажжение через 1с } |
Все! Видите какие простые и линейные получаются кусочки. И насовать в эту систему еще с полтора десятка цепочек не составит труда. Добавляем флаги и условия, происываем таймеры и вперед. Причем флаги мы можем ставить как в задачах, так и в прерываниях. А главный цикл будет с хрустом это пережевывать.
Думаю понятно, что в такой организации скорость работы главного цикла является критичной. В том плане что всякие хардверные тупые задержки вроде delay_ms(****) в ней КРАЙНЕ НЕЖЕЛАТЕЛЬНЫ. Т.к. они тормозят весь конвейер. Все должно быть сделано через службу таймеров. Если нужны очень короткие задержки, то можно завести второй аппаратный таймер с такой же байдой, но тикающий чаще. Но тут надо думать и смотреть.
Еще одним недостатком такой организации является жесткий порядок выполнения операций. Т.е. пока мы не пробежим по всей цепочке if(flag.***==1) мы не сможем выполнить какую либо операцию заново (хотя если цепь быстрая, то редко когда критично).
Это частично решается переходом на конструкцию на базе SWITCH-CASE конструкции с выходом в корень после каждой операции. В этом случае у нас возникает другое западло — часто вызываемые задачи могут заблокировать выполнение более медленных.
Думаю принцип понятен, поэтому я не буду приводить работающий пример кода. Мне просто лень :) Если кто то пойдет таким путем то я с радостью выложу его проект в статью. Дело в том, что я предпочитаю другую организацию — динамический диспетчер. Или просто диспетчер. И вот для нее я и приведу пример реального кода. На нем же будут дальнейшие примеры курса :)
Исследование, описанное в статье про avr, подчеркивает ее значимость в современном мире. Надеюсь, что теперь ты понял что такое avr, архитектура программ и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории Цифровые устройства. Микропроцессоры и микроконтроллеры. принципы работы ЭВМ
Комментарии
Оставить комментарий
Цифровые устройства. Микропроцессоры и микроконтроллеры. принципы работы ЭВМ
Термины: Цифровые устройства. Микропроцессоры и микроконтроллеры. принципы работы ЭВМ