Люблю я на досуге поиграться с 8-битными AVR-микроконтроллерами. Почему? Потому что они просты в использовании и программировании, у них весьма очевидное внутреннее устройство, но при всем этом они позволяют быстро, дешево и без sms запиливать достаточно нетривиальные проекты. Arduino (и всевозможные дешевые китайские клоны, разумеется) - вообще достаточно популярная железка (и среда разработки) среди погромистов и инженеров. Имея на руках пару breadboard'ов и клубок проводов для них, с Arduino вы сможете без пайки собрать макет какого-нибудь проекта и закодить его, практически не напрягаясь.
Но я сюда пишу не для того, чтобы поднять продажи китайцам, а чтобы заняться настоящей хардкорной разработкой под AVR. Сегодня мы с вами начнем писать настоящую операционную систему с вытесняющей многозадачностью, которую потом запустим на обычных железяках Arduino! Но это еще не все, ведь мы будем писать эту ОСь на C C++14, сдабривая все это щедрым количеством ассемблерных вставок и макросов. Вот это будет пламенная смесь! А назовем мы нашу операционную систему пафосно - Atmos!
Перед тем, как начать, я должен сказать, что подобные ОС достаточно профессионального уровня и с длительной поддержкой уже существуют. Это, например, кросс-платформенная богатая на фичи и толстая FreeRTOS, или компактная (но не очень удобная) Femto OS. Вы, конечно, можете открыть их исходные коды и постараться разобраться, в чем я вам желаю удачи. Но я в этом цикле статей поясню базовые принципы, которые стоят за созданием такой операционки для AVR и буду писать ее по шагам, разжевывая подробно каждый этап. Если вы готовы вместе со мной окунуться в раскаленные пучины кода, то вперед!
Сперва я отвечу на вопрос, который мог у вас возникнуть: "В AVR и так памяти для кода и оперативки раз-два и обчелся, какая нах*й еще многозадачность?!". Ну тогда сначала определим, зачем нам вообще может понадобиться операционная система с вытесняющей многозадачностью для AVR8. Например, у вас в контроллере несколько кусков кода (функций/задач), которые должны выполняться независимо и одновременно, а пихать их все в один цикл - не комильфо. Или, например, одна из этих функций должна ожидать определенных условий и только тогда запускаться. Особенно, если такие условия выставляются другими уже работающими задачами. Или часть времени у вас контроллер вообще должен простаивать, а все эти задачи должны приостанавливаться на заданный промежуток времени. А когда ни одна из задач не выполняется, вообще неплохо было бы переключить контроллер в режим экономии энергии. И так далее. "Оке, это, вроде бы, полезные фишки, ну а что с памятью?" - спросите вы. А с памятью все не так плохо. В 3-4 килобайта можно уместить массу функционала со стороны ОС. Даже если брать Atmega8 - у этого контроллера 8 кб доступно, итого у вас останется еще ~4 килобайта для ваших задач. А с оперативкой вот что: на каждую задачу будет необходимо выделить блок оперативной памяти определенного размера, порядка 40 байтов (для архитектуры reduced tiny - почти в два раза меньше). Сама ОС тоже потребует около 20 байтов оперативной памяти. А дальше все уже зависит от того, сколько памяти требует ваш код. Если брать ту же Atmega8, то для трех выполняемых задач и ОС потребуется порядка 140 байтов оперативки из 1 килобайта доступной. Почему именно такие цифры, увидим дальше, по ходу разработки. По-моему, это не очень большое количество ресурсов, которое у вас отожрет ОС, особенно, если вы не планируете забить контроллер под завязку. Конечно, независимо работающие задачи могут потребовать больше кода со своей стороны (для синхронизации доступа к портам, например), но в любом случае оверхед не будет слишком объемным.
Ну, а теперь поехали! Для начала определим, а можно ли вообще такую ОСь запилить под 8-битные AVR. Что нам для этого потребуется? Как мы выяснили, памяти для кода и для данных нам хватит, даже если брать совсем простецкий контроллер. ОС наша будет предлагать вытесняющую многозадачность, то есть сама решать, когда какой задаче выполняться. И, разумеется, вовремя переключать управление с одной задачи на другую, ожидающую в очереди. Для этого нам нужен некий механизм, чтобы дать ОС возможность в нужные моменты времени перехватить управление. Таймеры и их прерывания, которые есть даже в самых примитивных AVR'ках, отлично подойдут. А как мы будем, собственно, переключать исполнение с одной задачи на другую? Нам нужно будет сохранить где-то текущее состояние выполняемой в данный момент времени задачи, а потом восстановить такое же состояние для другой задачи и передать ей управление. Сохранить состояние мы можем в оперативной памяти (как раз в тех самых 40 байтах). Передать управление тоже несложно - достаточно выполнить команду перехода на нужную инструкцию, на которой мы остановились в прошлый раз. Доступ у нас к этой информации в AVR имеется. Остается один момент - у каждой задачи будет свой независимый стек (прямо как у процесса или потока в Windows/*nix!), и нам надо будет его как-то сохранять и восстанавливать. В контроллерах AVR и это не проблема - нам доступен регистр указателя стека SP
(иногда состоящий из двух 8-битных частей SPL
и SPH
). Мы можем читать и записывать его значение. Итого, у нас есть все необходимое для реализации нашего хитрого плана.
Сначала мы займемся тем, что запилим код, позволяющий универсально, практически под любые контроллеры AVR, настраивать таймер, который будет использоваться операционкой для переключения задач. Так как у большинства AVR'ок таймеров несколько, мы дадим возможность выбирать, какой из таймеров должен использоваться ОС. Кстати, этот код можно будет использовать и для того, чтобы просто настроить некий таймер на срабатывания с определенным интервалом времени. Код я буду писать в бесплатной и весьма неплохой среде Atmel Studio 7 и компилировать его, соответственно, компилятором GCC, входящим в комплект поставки этой среды. Делать я буду стараться все, как у серьезных мужиков: писать комменты на английском, задумываться над универсальностью кода и компактностью его после компиляции, а потом еще и залью все это дело на GitHub. Проверять все это дело будем на Arduino, потому что Arduino нет только у ленивого, да и программировать просто, через ту же Atmel Studio.
Начнем с того, что запустим Atmel Studio и создадим проект "atmos".
Код я буду проверять на Arduino Mega 2560, которая основана на контроллере Atmega2560, поэтому такой контроллер и выберем, как целевой. Впрочем, код я буду тестировать и на Arduino Uno r3, в котором стоит Atmega328P, но девайс мы сможем поменять легко уже потом, когда это потребуется. А пока что ставим Atmega2560.
Ну и все, получаем готовый проект, который ничего, кроме бесконечного пустого цикла, не делает, зато компилируется.
Начнем с того, что создадим каталог kernel
("Solution explorer" - "atmos" - правой кнопкой мыши - "Add" - "New folder" - kernel). Далее в эту папку добавим файл config.h
(правой кнопкой мыши на папке kernel - "Add" - "New item..."). Как у любой уважающей себя операционной системы, у нашей тоже будет файл конфигурации со всякими настройками, которые позволят собрать систему так, как того пожелает пользователь.
Содержимое этого файла у нас пока будет следующим:
1 2 3 4 5 6 7 |
#pragma once /** Defines which AVR timer to use for scheduler. */ #define ATMOS_TIMER_INDEX 0 /** Defines time in microseconds between two scheduler timer ticks. */ #define ATMOS_TICK_PERIOD_US 10000 |
Всего две настройки: какой из таймеров контроллера использовать для планировщика операционной системы, и с каким интервалом этот таймер должен вызывать наш код (в микросекундах). Эти настройки мы будем использовать далее.
Как мы будем конфигурировать таймеры? Для этого у AVR есть несколько регистров. Как часто водится, в Atmel не смогли прийти к консенсусу, и зафигачили чуть ли ни в каждый контроллер свои регистры таймеров, свои управляющие биты, вообще везде сделали все по-своему. Придется нам теперь как-то это унифицировать. В целом, наша задача не так сложна: не нужно настраивать некие хитрые режимы работы таймера (с управлением с порта ввода-вывода, или там с ШИМом). Нам просто нужно, чтобы таймер тикал один раз в заданный промежуток времени. У подавляющего большинства AVR-контроллеров у каждого таймера есть так называемый режим CTC
- Clear Timer on Compare match. Буквально: обнулить таймер при совпадении. Поясняю: у нас есть некий регистр, в который мы пишем нужное нам значение. Например, 100. Таймер тикает, значение счетчика таймера (который хранится в другом регистре) увеличивается на единицу, и каждый тик это значение сравнивается с той сотней. Когда они совпадут, таймер обнулится, а у нас в коде вызовется прерывание. То есть, заданная функция, реагирующая на это прерывание, получит управление. Режим CTC позволяет гибко настраивать частоту получения этих самых прерываний, и именно это нам и необходимо. Плюс, мы сможем в той или иной мере регулировать частоту самих тиков таймера. По умолчанию она совпадает с частотой работы процессора, например, 16 Мегагерц в Arduino Mega 2560. Это означает, что один раз за 1/16000000 секунды (62.5 наносекунды) значение таймера будет увеличиваться на единицу. Мы в большинстве случаев сможем поделить эту частоту на 8, 64, 256 или 1024, настроив таймер соответствующим образом. На некоторых контроллерах для некоторых таймеров можно задать и другие делители частоты. Я потратил некоторое время, чтобы изучить datasheet'ы разных контроллеров AVR и написал код, который содержит необходимую информацию о регистрах, битах, названиях прерываний для каждого таймера этих контроллеров. Это все нам потом понадобится, чтобы сконфигурировать конкретный таймер на конкретном контроллере. В проект я добавил файл timer_config.h
, который и содержит все определения. Для каждого таймера определяются следующие макросы:
ATMOS_TIMERx_BITS
- количество битов в счетчике таймера. Обычно 8 или 16, хотя бывают изощренные случаи типа 10. (10 мы пока поддерживать не будем.)ATMOS_TIMERx_PRESCALER_CONTROL
- регистр, в который пишутся биты управления делителем частоты. К счастью, это всегда один регистр, я не встречал такого, чтобы часть битов надо было устанавливать в одном регистре, а часть - в другом.ATMOS_TIMERx_MODE_CONTROL
- регистр, который содержит бит управления режимом работы таймера, позволяющий переключить таймер в режим CTC. Этот макрос может отсутствовать, если таймер заведомо по умолчанию работает в таком режиме. Да, есть такие контроллеры, для которых это выполняется.ATMOS_TIMERx_CTC_MODE_BIT
- номер бита, который используется для включения режима CTC. Может так же остутствовать, как иATMOS_TIMERx_MODE_CONTROL
, в той же ситуации.ATMOS_TIMERx_INTERRUPT_CONTROL
- регистр, который используется для включения прерывания таймера. Это либо прерывание CTC, либо прерывание по переполнению. Как правило, если таймер мы переключаем в режим CTC, записывая соответствующий бит в регистрATMOS_TIMERx_MODE_CONTROL
, то и прерывание мы будем ловить CTC. Если же таймер изначально работает в режиме CTC (и даже не имеет битов перевода его в такой режим), т.е. обнуляется не по переполнению, а когда значение счетчика совпадает со значением регистра сравнения, то мы должны будем ловить прерывание по переполнению (хоть это и звучит странно). Это та ситуация, когда макросыATMOS_TIMERx_MODE_CONTROL
иATMOS_TIMERx_CTC_MODE_BIT
отсутствуют.ATMOS_TIMERx_COMPARE_INTERRUPT_BIT
- Номер бита, который включает прерывание таймера по совпадению значений или по переполнению.ATMOS_TIMERx_COMPARE_REGISTER
- Регистр CTC. Он содержит значение, с которым каждый тик сравнивается значение счетчика таймера.ATMOS_TIMERx_INTERRUPT_NAME
- Название прерывания по совпадению значений или по переполнению.ATMOS_TIMERx_COUNTER
- Регистр счетчика таймера. Содержит текущее значение счетчика таймера, которое увеличивается на единицу каждый тик.
Здесь в каждом названии "x
" - это номер таймера (0, 1, 2 и т.д.). Другие опциональные макросы, которые могут присутствовать, а могут и не присутствовать для таймера:
ATMOS_TIMERx_PRESCALER_HAS_32_128
- Если определено, то указывает на то, что делитель частоты таймера поддерживает значения 32 и 128. По умолчанию, как я уже говорил, делитель частоты таймера поддерживает только значения 1, 8, 64, 256 и 1024. К счастью, названия битов, которые используются для конфигурации делителя частоты, и их комбинации одинаковы всегда для всех таймеров любого контроллера.ATMOS_TIMERx_PRESCALER_HAS_16384
- Если определено, указывает, что делитель частоты таймера поддерживает значения 1/2/4/8/16/32/64/128/256/512/1024/2048/4196/8192/16384 и имеет четыре конфигурационных бита, а не три, как обычно.ATMOS_TIMERx_HAS_16BIT_MODE
- Если определено, то указывает, что таймер 8-битный, но имеет 16-битный режим.ATMOS_TIMERx_16BIT_MODE_CONTROL
- Регистр, который используется для переключения таймера из 8-битного в 16-битный режим. Пока что мы не будем запиливать поддержку включения в подобных таймерах 16-битного режима, но макросы я все равно сделал на будущее.ATMOS_TIMERx_16BIT_MODE_BIT
- Номер бита, который используется для переключения таймера в 16-битный режим.ATMOS_TIMERx_16BIT_MODE_HIGH_BYTE_COMPARE_REGISTER
- Старшая часть регистра сравнения CTC, которая используется, когда таймер переведен в 16-битный режим.ATMOS_TIMERx_16BIT_MODE_HIGH_BYTE_COUNTER_REGISTER
- Старшая часть регистра счетчика таймера, которая используется, когда таймер переведен в 16-битный режим.
Целая гора макросов! Вам повезло, и я все уже сделал за вас. У меня получился файл на 500+ строк, в котором уже есть определения для самых популярных контроллеров, и вы сможете даже добавить свои. Вот, как это выглядит:
Этот файл вы можете посмотреть полностью, скачав проект по ссылке в конце статьи, либо зайдя на гитхаб проекта.
Итак, теперь наша система все знает о том, как сконфигурировать тот или иной таймер на многих контроллерах.
Теперь немного отвлечемся и добавим в проект файл checks.h
, в который пропишем некоторые правила, которые еще на этапе компиляции скажут нерадивому разработчику, что он ошибся в выборе профессии параметрах, и ОСь с такими, как у него, настройками не соберется. Вот что я добавил в этот файл:
Здесь у нас подключается timer_config.h
, который мы только что разобрали, а дальше идут несколько проверок.
- У вас AVR XMEGA? Нахер такое счастье! Мы пока не будем поддерживать эту архитектуру, а почему - разберем в следующих статьях.
- Не определены макросы
ATMOS_TIMERx_BITS
? Это значит, что под ваше устройство нет конфигурации таймеров в файлеtimer_config.h
. Идите и добавляйте, если хотите, чтобы собралось! - Не определен макрос
F_CPU
, который содержит частоту работы контроллера? Ну так иди в настройки и задай его там! Для Arduino Mega 2560 это будет 16000000 (16 МГц), как я уже говорил. Впрочем, для Arduino Uno r3 тоже. Про настройки проекта и про то, как задать этот макрос, мы с вами еще поговорим отдельно, когда у нас появится, что компилировать. - Определен макрос
__NO_INTERRUPTS__
? Э, дружок, выключил прерывания, и ждешь, что что-то взлетит? Нам с тобой не по пути! Включай прерывания обратно!
Переходим к следующему файлу - timer_selector.h
. Тут мы уже немножечко обратим внимание на настройки пользователя, которые заданы в config.h
, и выведем с их помощью новые макросы. У нас ведь в файле timer_config.h
описаны макросы в зависимости от номера таймера. ATMOS_TIMER0_BITS
или, там, ATMOS_TIMER4_MODE_CONTROL
. А пользователь может выбрать какой-то конкретный таймер. И нам в итоге неплохо бы иметь макросы без индексов таймера: ATMOS_TIMER_BITS
или ATMOS_TIMER_MODE_CONTROL
. Вот этот файл и будет заниматься тем, что заполнит значения этих самых макросов без индексов в зависимости от того, что у нас пользователь написал в макросе ATMOS_TIMER_INDEX
. Код тут унылый и копипастный, приводить не буду, можно посмотреть в проекте полностью. Результат работы кода, как я уже сказал - определить макросы без индексов для того таймера, который выбрал пользователь. Кроме того, этот файл используется для проверки, имеет ли макрос ATMOS_TIMER_INDEX
допустимое значение. Я не встречал устройств, у которых было бы больше шести таймеров, поэтому и индекс может быть от нуля до пяти включительно.
Далее все будет интереснее. Переходим к файлу timer_params_calc.h
. Этот файл будет использоваться для того, чтобы непосредственно вычислить, какое значение нам нужно будет записать в регистр CTC, и какие биты записать в регистр, контролирующий делитель частоты таймера, чтобы получить именно такую частоту срабатываний, которая указана в файле config.h
в макросе ATMOS_TICK_PERIOD_US
. Алгоритм мы применим простой. Будем брать по очереди все доступные делители частоты (1, 2, 4, 8 и т.д.) и просчитывать для них возможность установки такого значения регистра CTC, чтобы прерывание срабатывало с заданной нами частотой. Таким образом, файл timer_params_calc.h
можно будет подключать в другие файлы много раз подряд (мы опустим директиву #pragma once
), задавая в определенном макросе, какой делитель мы хотим попробовать. Мы будем начинать такой "перебор" с самых маленьких значений делителя частоты к самым большим, в порядке возрастания. Ведь чем меньше делитель частоты, тем меньше будет и погрешность в частоте срабатываний. Как только мы найдем подходящий, мы закончим перебор. Приведу здесь содержимое рассматриваемого файла с русскими комментариями:
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 37 38 39 40 41 42 43 44 45 46 47 |
//Перед включением этого файла необходимо определить макрос //ATMOS_TIMER_PRESCALER_VALUE, содержащий желаемое значение //делителя частоты, а также макрос ATMOS_MAX_TIMER_VALUE, //который указывает максимально возможное значение счетчика таймера. //Если заданный период срабатываний укладывается в диапазон //значений с заданным делителем частоты... #if ATMOS_TICK_PERIOD_US >= (ATMOS_TIMER_PRESCALER_VALUE * 1000000 / F_CPU) \ && ATMOS_TICK_PERIOD_US <= (ATMOS_TIMER_PRESCALER_VALUE * 1000000 * ATMOS_MAX_TIMER_VALUE / F_CPU) //...то тогда мы вычисляем значение, которое потом запишем в регистр CTC... # define ATMOS_TIMER_TOP_VALUE (1ull * ATMOS_TICK_PERIOD_US * F_CPU) / (1ull * ATMOS_TIMER_PRESCALER_VALUE * 1000000) //...и вычисляем комбинацию битов для установки заданного значения делителя частоты. # if ATMOS_TIMER_TOP_VALUE > 0 && ATMOS_TIMER_TOP_VALUE <= ATMOS_MAX_TIMER_VALUE # ifdef ATMOS_TIMER_PRESCALER_HAS_16384 # if ATMOS_TIMER_PRESCALER_VALUE == 1 # define ATMOS_TIMER_PRESCALER_CONTROL_VALUE _BV(ATMOS_TIMER_PRESCALER_CS0) # elif ATMOS_TIMER_PRESCALER_VALUE == 2 //...пропущено... # elif ATMOS_TIMER_PRESCALER_VALUE == 16384 # define ATMOS_TIMER_PRESCALER_CONTROL_VALUE _BV(ATMOS_TIMER_PRESCALER_CS3) | _BV(ATMOS_TIMER_PRESCALER_CS2) | _BV(ATMOS_TIMER_PRESCALER_CS1) | _BV(ATMOS_TIMER_PRESCALER_CS0) # endif //ATMOS_TIMER_PRESCALER_VALUE # elif defined(ATMOS_TIMER_PRESCALER_HAS_32_128) # if ATMOS_TIMER_PRESCALER_VALUE == 1 # define ATMOS_TIMER_PRESCALER_CONTROL_VALUE _BV(ATMOS_TIMER_PRESCALER_CS0) //...пропущено... # elif ATMOS_TIMER_PRESCALER_VALUE == 1024 # define ATMOS_TIMER_PRESCALER_CONTROL_VALUE _BV(ATMOS_TIMER_PRESCALER_CS2) | _BV(ATMOS_TIMER_PRESCALER_CS1) | _BV(ATMOS_TIMER_PRESCALER_CS0) # endif //ATMOS_TIMER_PRESCALER_VALUE # else //ATMOS_TIMER_PRESCALER_HAS_32_128 # if ATMOS_TIMER_PRESCALER_VALUE == 1 # define ATMOS_TIMER_PRESCALER_CONTROL_VALUE _BV(ATMOS_TIMER_PRESCALER_CS0) //...пропущено... # elif ATMOS_TIMER_PRESCALER_VALUE == 1024 # define ATMOS_TIMER_PRESCALER_CONTROL_VALUE _BV(ATMOS_TIMER_PRESCALER_CS2) | _BV(ATMOS_TIMER_PRESCALER_CS0) # endif //ATMOS_TIMER_PRESCALER_VALUE # endif //ATMOS_TIMER_PRESCALER_HAS_32_128 # else //ATMOS_TIMER_TOP_VALUE # undef ATMOS_TIMER_TOP_VALUE # endif //ATMOS_TIMER_TOP_VALUE #endif //Если с заданным делителем частоты ну никак не получить нужную частоту //срабатываний таймера, то подчищаем ненужные макросы. #ifndef ATMOS_TIMER_PRESCALER_CONTROL_VALUE # undef ATMOS_TIMER_TOP_VALUE # undef ATMOS_TIMER_PRESCALER_VALUE #endif //ATMOS_TIMER_PRESCALER_CONTROL_VALUE |
Итак, на входе мы имеем максимально возможное значение счетчика таймера (ATMOS_MAX_TIMER_VALUE
), значение делителя частоты (ATMOS_TIMER_PRESCALER_VALUE
) и желаемый период срабатывания прерываний (ATMOS_TICK_PERIOD_US
), который задается в файле config.h
. Мы вычисляем минимально и максимально возможный период срабатываний данного таймера с заданным делителем частоты. Если желаемый период срабатываний попадает в этот промежуток - ОК, берем этот делитель частоты. При условии, что наш таймер его поддерживает, конечно. Если что-то не сошлось, то удаляем ненужные макросы, и на этом все. Будем пробовать следующий делитель.
Здесь мы снова немного прервемся и рассмотрим совсем крошечный файлик, utils.h
, в котором есть два вспомогательных макроса:
1 2 3 4 |
#pragma once #define STRINGIFY(s) XSTRINGIFY(s) #define XSTRINGIFY(s) #s |
Это необходимо для того, чтобы преобразовать значение любого макроса в строку, а потом вывести ее, например, в журнал компиляции. Пригодится для диагностики и отладки.
Теперь разберем файл timer_params.h
. Это практически последняя точка всей логики, которая предоставит нам готовенькие значения для конфигурации таймера. Эти значения мы потом запишем в нужные регистры, и все заработает.
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#pragma once #include <avr/io.h> #include <stdint.h> #include "timer_selector.h" #include "utils.h" //Определим значение макроса ATMOS_MAX_TIMER_VALUE, //имея ATMOS_TIMER_BITS, который задал пользователь. //Макрос ATMOS_MAX_TIMER_VALUE содержит //максимально допустимое значение счетчика таймера. #if ATMOS_TIMER_BITS == 8 # define ATMOS_MAX_TIMER_VALUE UINT8_MAX #elif ATMOS_TIMER_BITS == 16 # define ATMOS_MAX_TIMER_VALUE UINT16_MAX #else //ATMOS_TIMER_BITS static_cast("Unsupported ATMOS_TIMER_BITS value"); #endif //ATMOS_TIMER_BITS //Выведем в консоль сборки, какой таймер, а также какой //период срабатываний выбрал пользователь. #pragma message ("Selected timer #" STRINGIFY(ATMOS_TIMER_INDEX) " (" STRINGIFY(ATMOS_TIMER_BITS) "-bit)") #pragma message ("Selected timer tick duration " STRINGIFY(ATMOS_TICK_PERIOD_US) " us") //Теперь пробуем подобрать делитель частоты, для которого //мы сможем получить требуемую частоту срабатываний прерывания. #define ATMOS_TIMER_PRESCALER_VALUE 1 #include "timer_params_calc.h" //Если после пробы делителя частоты со значением 1 мы не смогли //получить требуемую частоту, будем пробовать делитель 2, 4 и т.д. //Переберем все возможные делители. В файле timer_params_calc.h //мы имеем проверку допустимости делителя для выбранного таймера. //Так что если либо делителя нет у выбранного таймера, либо этот делитель //попросту нам не подошел, пробуем дальше. #ifndef ATMOS_TIMER_PRESCALER_CONTROL_VALUE # define ATMOS_TIMER_PRESCALER_VALUE 2 # include "timer_params_calc.h" #endif //ATMOS_TIMER_PRESCALER_CONTROL_VALUE #ifndef ATMOS_TIMER_PRESCALER_CONTROL_VALUE # define ATMOS_TIMER_PRESCALER_VALUE 4 # include "timer_params_calc.h" #endif //ATMOS_TIMER_PRESCALER_CONTROL_VALUE //...пропущена часть делителей... #ifndef ATMOS_TIMER_PRESCALER_CONTROL_VALUE # define ATMOS_TIMER_PRESCALER_VALUE 16384 # include "timer_params_calc.h" #endif //ATMOS_TIMER_PRESCALER_CONTROL_VALUE //В итоге, если мы не смогли подобрать подходящий делитель, то выведем ошибку. #ifndef ATMOS_TIMER_PRESCALER_CONTROL_VALUE static_assert(false, "Unable to deduce timer prescaler and limiting values"); #else //ATMOS_TIMER_PRESCALER_CONTROL_VALUE //А если смогли - то выведем просчитанные значения регистров. # pragma message ("Selected timer prescaler " STRINGIFY(ATMOS_TIMER_PRESCALER_VALUE) \ ", top value " STRINGIFY(ATMOS_TIMER_TOP_VALUE) \ ", prescaler bits: " STRINGIFY(ATMOS_TIMER_PRESCALER_CONTROL_VALUE)) #endif //ATMOS_TIMER_PRESCALER_CONTROL_VALUE //Уберем ненужный теперь макрос. #undef ATMOS_MAX_TIMER_VALUE |
После выполнения данного файла мы будем иметь либо ошибку компиляции (если не удалось подобрать под заданные параметры подходящий делитель частоты/значение регистра CTC), либо следующие макросы:
ATMOS_TIMER_PRESCALER_VALUE
- Значение делителя частоты, которое было выбрано нашей логикой.ATMOS_TIMER_TOP_VALUE
- Значение регистра CTC. До этого значения наш таймер будет считать, а потом обнуляться, вызывая прерывание.ATMOS_TIMER_PRESCALER_CONTROL_VALUE
- Битовая маска, которую надо записать в регистр управления делителем частоты таймера.
Мы приблизились к победному концу! Давайте же возьмем и воспользуемся вычисленными значениями ATMOS_TIMER_PRESCALER_VALUE
и ATMOS_TIMER_TOP_VALUE
, чтобы сконфигурировать таймер! Организую это я в файле scheduler_timer_setup.h
. Мы напишем функцию initialize_scheduler_timer
, которая и будет делать всю работу, а именно:
- Выставлять значение регистра CTC таймера (
ATMOS_TIMER_COMPARE_REGISTER
) вATMOS_TIMER_TOP_VALUE
. - Записывать в регистр настройки делителя частоты таймера (
ATMOS_TIMER_PRESCALER_CONTROL
) вычисленную битовую маскуATMOS_TIMER_PRESCALER_CONTROL_VALUE
. - Если необходимо, то переводить таймер в режим CTC, устанавливая в соответствующем управляющем регистре (
ATMOS_TIMER_MODE_CONTROL
) битATMOS_TIMER_CTC_MODE_BIT
. Также я сделал трюк, позволяющий сэкономить несколько байтов программной памяти в том случае, если регистрыATMOS_TIMER_PRESCALER_CONTROL
иATMOS_TIMER_MODE_CONTROL
совпадают (а такое бывает у некоторых таймеров некоторых контроллеров). - Включать прерывание таймера по совпадению, устанавливая в регистре контроля прерываний (
ATMOS_TIMER_INTERRUPT_CONTROL
) битATMOS_TIMER_COMPARE_INTERRUPT_BIT
.
Приводить код этого файла я тут не буду, можете глянуть его в проекте.
Пора уже бы все это скомпилировать и запустить, но остались еще настройки проекта atmos
, которые мы не выставили подобающим образом. Переходим в настройки (правой кнопкой мыши на названии проекта в "Solution Explorer" - "Properties"). Нам нужно выставить настройки для компиляторов C и C++ отдельно. Кроме того, у нас две конфигурации - Release
и Debug
(финальная и отладочная, соответственно), которые тоже неплохо бы настроить. Сначала выберем обе конфигурации ("Configuration" - "All Configurations"). Переходим на вкладку "AVR/GNU C Compiler" - "Symbols", жмем кнопку добавления символа справа, в открывшемся окне вводим F_CPU=16000000
, нажимаем "OK". Так мы задали частоту, на которой работает наш контроллер - 16 МГц.
Теперь то же самое делаем для вкладки "AVR/GNU C++ Compiler" - "Symbols". Далее настраиваем конфигурации по отдельности. Выбираем сначала конфигурацию Debug
. Переходим на вкладку "AVR/GNU C Compiler" - "Miscellaneous". В поле "Other flags" пишем строку: -std=gnu99 -fdiagnostics-show-option -Wextra -fno-strict-aliasing -Wall -pedantic -Wa,-ahlmsd=$*.lst
. Этими опциями мы выставляем стандарт языка, включаем побольше предупреждений компилятора, заставляем компилятор генерировать ассемблерные листинги (могут пригодиться), выключаем оптимизацию strict aliasing, которая может нам все подпортить. Скажете, вот, dx херово пишет код, оптимизации отрубает, но ядро линукса тоже без strict aliasing собирается. В аналогичное поле на вкладке "AVR/GNU C++ Compiler" - "Miscellaneous" вводим: -std=gnu++14 -fdiagnostics-show-option -Wextra -fno-exceptions -fno-strict-aliasing -Wall -pedantic -Wa,-ahlmsd=$*.lst -save-temps
. Почти все то же самое, но еще отключили исключения C++ и просим компилятор сохранять временные файлы (опять-таки, пригождаются при разборе проблем с компиляцией ассемблерных вставок). Теперь ставим следующие настройки для конфигурации Release
. Для компилятора C: -std=gnu99 -c -gstabs -Wa,-ahlmsd=$*.lst -Wextra -fno-strict-aliasing -Wall -mrelax -fdiagnostics-show-option -pedantic
. По сравнению с конфигурацией для отладки, тут мы еще включили релаксацию (которая меняет длинные команды jmp
, call
на более короткие rjmp
, rcall
, когда это возможно). Для компилятора C++: -std=gnu++14 -c -gstabs -Wa,-ahlmsd=$*.lst -Wextra -fno-exceptions -fno-strict-aliasing -Wall -mrelax -fdiagnostics-show-option -pedantic -save-temps
. Также в конфигурации Release
нам необходимо добавить опцию к линковщику (вкладка "AVR/GNU Linker" - "Miscellaneous", поле "Other Linker Flags") -mrelax
. На этом все, сохраним конфигурацию.
Все, что осталось сделать - это вызвать уже функцию initialize_scheduler_timer
и проверить, что таймер срабатывает именно с той частотой, с которой нам нужно. В файле config.h
я установил следующие значения макросов:
1 2 |
#define ATMOS_TIMER_INDEX 1 #define ATMOS_TICK_PERIOD_US 1000000 |
Это значит, что мы будем настраивать таймер 1 вызывать прерывание ровно раз в секунду (100000 микросекунд = 1 секунда). Нулевой таймер нам не подойдет, потому что ну никак не получится заставить его дергать прерывание раз в секунду - он восьмиразрядный. Максимум, что мы из него можем выжать - 1 раз за 16.32 миллисекунды. Можете, кстати попробовать скомпилировать проект со значением ATMOS_TIMER_INDEX
, равным нулю, и периодом в 1 секунду, и получите соответствующую ошибку. Проверять код я буду на двух железках сразу: на клонах Arduino Mega 2560 и Arduino Uno r3. Откроем файл main.cpp
и напишем следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <avr/io.h> #include <avr/interrupt.h> #include "kernel/scheduler_timer_setup.h" int main() { DDRC |= _BV(PC0); atmos::initialize_scheduler_timer(); sei(); while(true) { } } ISR(ATMOS_TIMER_INTERRUPT_NAME) { PORTC ^= _BV(PC0); } |
Что тут происходит? Мы подключили необходимые заголовочные файлы, включая тот, в котором содержится определение нашей функции initialize_scheduler_timer
. Мы переключаем вывод 0
порта C
в выходной режим, устанавливая бит PC0
в единицу в специальном регистре DDRC
. Затем производим инициализацию таймера, вызвав нашу функцию, и разрешаем прерывания, выполняя инструкцию sei
. Далее идет обработчик прерываний, который будет вызываться один раз в секунду. Все, что он будет делать - это подавать на нулевой вывод порта C
то единицу, то ноль, и менять это значение раз в секунду. Давайте взглянем на схему Arduino Mega 2560. Вывод PC0
соответствует выводу номер 37 на ардуине.
Подключим к этому выводу светодиод через резистор с сопротивлением 300 Ом - 1 кОм.
Нам надо как-то запрограммировать нашу ардуину. Это можно делать и через Atmel Studio. Но сначала все равно нужно установить на компьютер среду разработки Arduino IDE. Потом, подключив Arduino к компьютеру, надо определить, какой COM-порт она занимает. Конечно, это виртуальный COM-порт, потому что железку вы подключаете в USB-разъем. У меня это COM3.
Затем, в Atmel Studio заходим в меню "Tools" - "External Tools...", нажимаем на кнопку "Add" и вводим следующие параметры:
- Title: Arduino 2560 COM3 (или любое другое название, на ваш вкус).
- Command:
C:\Program Files (x86)\Arduino\hardware\tools\avr\bin\avrdude.exe
(путь к программатору avrdude в вашем каталоге, куда установили Arduino IDE). - Arguments:
-C"C:\Program Files (x86)\Arduino\hardware\tools\avr\etc\avrdude.conf" -patmega2560 -cwiring -P\\.\COM3 -b115200 -D -Uflash:w:"$(BinDir)\$(TargetName).hex":i
(здесь тоже нужно скорректировать путь к конфигурации avrdude, если Arduino IDE установлена у вас в другую директорию, и индекс COM-порта). - Включаем флажок Use Output Window.
Можно сразу добавить такую же конфигурацию аналогично и для Arduino UNO r3:
- Title: Arduino UNO COM4 (у меня Arduino UNO r3 определяется на порту COM4).
- Command:
C:\Program Files (x86)\Arduino\hardware\tools\avr\bin\avrdude.exe
. - Arguments:
-C"C:\Program Files (x86)\Arduino\hardware\tools\avr\etc\avrdude.conf" -patmega328p -carduino -P\\.\COM4 -b115200 -D -Uflash:w:"$(BinDir)\$(TargetName).hex":i
. - Снова включаем флажок Use Output Window.
Все готово! Подключаем Arduino Mega 2560 к компьютеру, далее компилируем программу (я выбрал конфигурацию Release
, в которой включены все оптимизации), нажимая F7.
Если все собралось без ошибок, то заливаем прошивку в Arduino, выбирая меню "Tools" - "Arduino 2560 COM3". Вуаля, светодиод включается-выключается раз в секунду! Я проделал все то же самое и с Arduino Uno R3 (светодиод подключается к выходу A0, который соответствует выводу контроллера PC0
). Не забываем перед сборкой и прошивкой поменять тип устройства на Atmega328P:
А вот и результат:
Мы получили код, работающий на целой куче контроллеров AVR8, и проверили его на двух контроллерах, просто пересобрав и не изменив ни строчки. Теперь мы можем настроить любой таймер контроллера так, чтобы получать срабатывание прерывания с нужным нам периодом. Кода написано много, а в результате компиляции мы получим всего с десяток ассемблерных инструкций (например, на Arduino Mega 2560 это всего 8 инструкций), потому что бОльшая часть этого кода выполнится на этапе компиляции.
Вы молодцы, если осилили дочитать эту статью до конца! Следующая часть будет гораздо менее объемной, так как все основные настройки Atmel Studio, программатора и проекта мы уже сделали. Скачать проект целиком можно по этой ссылке. Также он доступен на GitHub.
Спасибо. Очень познавательно!
Крутая тема. Пока не интересуюсь, но заранее спасибо!
>пишем на c++14
>#define name number
ппц.
А почему не на 17, кстати? в шланге давно есть многие фичи.
Как ты собираешься конфигурировать исходник под разные, скажем, операционные системы без define'ов? Никак, вот и здесь для разных конфигураций и контроллеров используются define. Почему не на 17? Потому что 17 стандарт еще не принят, а в Atmel Studio 7 gcc версии 5.4, он почти ничего о 17 стандарте вообще не слышал.
>Как ты собираешься конфигурировать исходник под разные, скажем, операционные системы без define'ов
Файлом конфигурации, в который прописывать все нужные параметры, но не в виде макросов, а в виде нормального типизованного плюсового кода. Зато можно будет использовать constexpr и прочее метапрограммирование, а не танцевать вприсядку с лестницой дефайнов.
Файл конфигурации cскорее всего генерировать с помощью cmake.
>Почему не на 17? Потому что 17 стандарт еще не принят, а в Atmel Studio 7 gcc версии 5.4, он почти ничего о 17 стандарте вообще не слышал.
Ну так есть же бекэнд для llvm под avr.
P.S. я его не пробовал, так как с аврками со времени окончания универа дел не имею.
Я не в курсе, как можно сделать файл конфигурации без дефайнов, который позволит полностью исключить из компиляции кусок кода (чтобы его даже не пытался собирать компилятор) или изменить прототип функции. А cmake - это для тех ОС, которые уже претендуют на завершенность и возможность использования в продакшене. А то, что я тут делаю, - это игрушечный вариант для того, чтобы показать, как в общем работают такие ОС. Кстати, не видел ни одной ОС под AVR, где настолько бы заморочились. Даже в FreeRTOS, которая позиционирует себя как продакшен-вариант и даже компилируется не только под AVR, используют обычные
#define
.>Даже в FreeRTOS, которая позиционирует себя как продакшен-вариант и даже компилируется не только под AVR, используют обычные #define.
Просто макросы - это стиль мышления сишников-ретроградов, а FreeRTOS написана именно на Си и именно ретроградами. Если бы я делал, я бы заюзал cmake для выбора микроконтроллера: для каждого mcu отдельная папка с его специфическими инклюдами, cmake создаёт выпадающий список по именам папок, в зависимости от выбранного mcu инклюдит нужный файл в файл конфигурации. Остальные параметры ты заносишь в cmake, он их заносит в генерируемый файл конфигурации. После ты прямо или косвенно инклюдишь конфиг во все файлы.
Короче: весь код по возможности должен быть тем, что написано. Иначе потом можно *измотаться* во время отладки, когда вроде-бы написано одно, но почему-то программа делает другое, а после разгребания километров препроцессированного вывода выяснится, что это макрос пошалил.
Я знаю о минусах макросов, но автогенеренные или частично генеренные файлы плохи тем, что их не отредактируешь, и сборка усложняется. Можно редактировать только шаблоны. Даже boost, по образцу которого строят стандартную библиотеку для современных плюсов, без макросов для конфигурации кода не обходится. Плюс, код после раскрытия макросов можно вывести, если вдруг трудности с отладкой. Я не пытаюсь защитить дефайны или как-то побудить их часто применять, но во многих случаях их использование оправдано. Если использовать в меру, конечно, и не дефайнить конструкции языка. Игрушечная ОС вполне себе такой случай.
Поддерживаю
Раз уж решили нагнать пафоса, тогда б делали метапрограммированием всё - и настройку конфигураций, и их проверку, и компиляцию платформозависимого оптимального кода.
Конечно тут есть нюансы - на мой взгляд макросы отлаживать проще, но заполнять их сложнее, тем более, когда они по разным файлам разбросаны.
И раз уж происходит ображение к методам и членам ОСьки через область видимости "atmos::", то можно было б тупо написать шаблон для конфигурации со статическими методами, создать тип (typedef) из этого шаблона в момент настройки. В итоге, он не будет жрать ни памяти, ни процессорного времени на раскруту, да и обращение к его методам останентся как в примере (всё статическое же).
Без макросов не обойтись хотя бы по той причине, что названия регистров в разных контроллерах отличаются и также являются макросами. Да и конфигурировать куски кода с помощью макросов все-таки проще. Особенно учитывая, что для полноценного метапрограммирования в AVR GCC не хватает стандартной библиотеки и. Можно, конечно, написать самому некоторую часть, но, имхо, оверкилл.
Здравствуйте! Спасибо огромное за эту, долгожданную мною статью. Не могли бы вы объяснить мне что означает 1ull в файле timer_params_calc.h ? Может эта опечатка и нужно null?
Добрый день,
1ull
- это значение 1 типаunsigned long long
. Суфиксull
(илиllu
) указывает, что число должно быть типаunsigned long long
(8-байтовый тип). Если бы мы объявили число без такого суффикса, то значение имело бы типint
(2-байтовый тип). Это делается для того, чтобы обезопасить себя от возможных переполнений, когда мы вычисляем достаточно большие числа с помощью макросов. В конечном счете в результате вычислений мы получаем небольшие числа (однобайтовые или двухбайтовые), но в процессе вычислений (которые выполняются на этапе компиляции) значения могут быть достаточно большими. Например, когда мы умножаемATMOS_TICK_PERIOD_US = 1000000
наF_CPU = 16000000
, мы получаем значение16 * 10^12
, и для его представления в нужно 6 байтов. Потом, конечно, мы это значение делим на такие же большие величины, и получаем маленькие значения, помещающиеся в 1 или 2 байта. Но в процессе получения этих маленьких значений, чтобы ничего не переполнилось и компилятор не ругался, мы явно указываем, что работаем с типамиunsigned long long
.Подробнее о суффиксах для числовых значений можно прочитать здесь, а о размерах стандартных типов C++ в AVR GCC - здесь.
Здравствуйте снова! Хотел поинтересоваться, вот если я допустим написал эту ОС по вашим урокам и прошил в arduino, получается я не смогу использовать не rfid, не lcd дисплеи и т.д. Или я ошибаюсь? Но по моему для работы библиотек(например для rfid) нужны ещё и библиотеки arduino!=(
Переписывать код для работы с этим rfid модулем для новой ОС(для так вами названной atmos) я не смогу=(
Я вообщем нe знаю что делать, очень нужно в этой ОС использовать rfid.
Да, большинство библиотек для ардуино используют стандартные библиотеки из поставки ардуино, которые в этой ОС не используются. Поэтому есть два варианта: Либо портировать нужную библиотеку rfid на голый avr (я обычно так делаю даже без использования каких-либо ОС, потому что в итоге занимает меньше места и работает быстрее, но я и прошиваю всё не в ардуино, а в голые атмеги в конечном счете, а ардуино использую только для прототипирования), либо втаскивать в проект штатные библиотеки ардуино. Во втором варианте могут быть проблемы, так как часть библиотек ардуино инициализируются определенным образом, не совместимым с инициализацией ОС.
И, насколько я знаю, сейчас нет доступных ОС, совместимых полностью с ардуино. Мой проект в большей части обучающий и не для продакшена, но другие имеющиеся в открытом доступе операционки тоже предполагается ставить на голые контроллеры без ардуиновских обвязок.
Привет, dx! Спасибо большое за твои статьи!
Знаю, что статье уже довольно много времени, однако есть вопрос, может, ты сможешь помочь?
Когда я заливаю код на мою atmega328p, то получаю такое сообщение:
avrdude.exe: stk500v2_ReceiveMessage(): timeout
avrdude.exe: stk500v2_ReceiveMessage(): timeout
avrdude.exe: stk500v2_ReceiveMessage(): timeout
avrdude.exe: stk500v2_getsync(): timeout communicating with programmer
avrdude.exe done. Thank you.
И соответственно код не работает. Можешь подсказать, почему такое может быть?
P.s. В device выбрана atmega328p, com3, пути command и arguments изменены:
E:\Program Files\Arduino\hardware\tools\avr\bin\avrdude.exe
-C"E:\Program Files\Arduino\hardware\tools\avr\etc\avrdude.conf" -patmega328p -cwiring -P\\.\COM3 -b115200 -D -Uflash:w:"$(BinDir)\$(TargetName).hex":i
Привет, ошибка выглядит так, как будто неправильно настроены параметры avrdude, либо есть проблемы с подключением arduino. А если прошивать какую-нибудь программу через Arduino IDE, работает? У меня, кстати, немного другая командная строка предложена для uno:
C:\Program Files (x86)\Arduino\hardware\tools\avr\etc\avrdude.conf" -patmega328p -carduino -P\\.\COM4 -b115200 -D -Uflash:w:"$(BinDir)\$(TargetName).hex":i
- тут вместо-cwiring
-carduino
(другой тип программатора).