Только вперед! Девиз, с которым развивается архитектура AVR, с каждым месяцем, а порой и днем мы видим, как появляются новые и новые модели всем полюбившихся микроконтроллеров. Многочисленные конкуренты также не спят и привлекают новичков все новыми и новыми возможностями, как вам например 50MHz (1MIPS/MHz) 32-bits CISC ядро с 1Mb flash памяти вкупе с наборными DAC / ADC / I2C / USB / TFT-LCD Controller / DMA от Renesas Technology (серия H8SX) (+ все необходимые библиотеки для LCD / USB)? Не говоря уже о продукции кампаний SILABS (как вам контроллеры, работающие от 0,9 вольт?), TI (MSP430xxx), плеяды новых PIC от Microchip и сотнях, нет, тысячах моделей от Maxim, NEC, Hitachi, National Sem., Analog, ELAN, Sharp, Futjisu, NXP (различные вариации на ядре ARM) и даже FTDI, сегодня разве что ленивый не выпускает МК... Причем у каждого из них с той или иной стороны есть свои изюминки, так что тема выбора между всем этим разнообразием дело далеко не тривиальное…
Но на этом форуме мы стойче всех, неправда ли? Поэтому, несмотря на громадные искушения в своем большинстве остаемся верны AVR (или я ошибаюсь)))?
Особняками среди AVR новинок на сегодня являются два новых направления: AVR32 и линейка более привычных нам AVR – XMEGA. Первые являют собой решение гораздо более высокого класса и предназначаются скорее как конкурент практически полной монополии кампании ARM. Решения эти в первую очередь интересны с точки зрения соотношения цены / производительности / возможностям, но с одним но… Начинать первый пассаж с решений такого уровня не всегда просто (хотя возможно весьма и весьма полезно), да и пересаживаться с более привычной среды тоже не всегда с руки, особенно если проверенное временем решение в общем и целом соответствует запросам... Конечно же, всегда хочется большего и в одном флаконе... Так что бы как в ARM, но проще, компактнее, привычнее, с уже знакомыми средами разработки... И вот тут мы (вернее лично я) несколько отходим от AVR32 (оставляя его развиваться дальше) и обращаем свой взор на некие XMEGA... Что же это за звери? И чем они лучше “Мег” (например тех же самых новых, вроде ATmega32U4)?
Для ответа на этот вопрос существуют, во-первых презентации и вводные статьи от Atmel,
собственно даташиты на сами контроллеры, ну и конечно же Application Notes. Именно они и позволят поэтапно, шаг за шагом раскрыть таки тему “Быть или… а ну его?”
Правда перед этим, все же хотелось взглянуть на документ XMEGA A MANUAL, содержащего полное описание всех кирпичиков составляющих новую линейку, для начала постараюсь не повторятся, и перечислить только реальные нововведения по сравнению с ATMEGA:
- ЦПУ AVR теперь более экономично и может работать на более высоких частотах:
0 – 12 MHz @ 1.8 – 2.7V
0 – 32 MHz @ 2.7 – 3.6V
Каких либо других кардинальных улучшений в его структуру внесено, по всей видимости не было, перед нами все те же 1MIPS / 1MHz. Что, согласитесь с 32МГц уже довольно неплохо.
- DMAC – контроллер прямого доступа к памяти. Вот и он, этот вожделенный DMA собственной персоной, одна из возможностей, которая и привлекает многих к контроллерам класса ARM, но отныне спустившаяся классом ниже. Atmel вообще судя по всему придает очень важное значение этому модулю, так как одной из отличительных особенностей линейки AT91SAM7xxx (ядро ARM) как раз и является повсеместное использование DMA, в отличие от прямых конкурентов от NXP (линейка LCPxxx), где оный ограничивается только интеграцией с USB.
- Event System – система событий. А вот это уже что-то реально новое! Признайтесь, ведь уже случалось так, что не хватало прерываний, либо времени на их обработку? Теперь у вас появилась возможность упростить и увеличить производительность программ с большим количеством прерываний (например, компенсируя неважные программерские способности, толкающие на повсеместное использование перекрывающих друг друга ISR… *про себя*). Разработанная Atmel система, очень и очень смахивает на довольно известную концепцию кросс-платформенной библиотеки Qt (Trolltech), “сигналы и слоты”. Принцип следующий – имеется некий объект каким-то образом сигнализирующий
о своем состоянии (скажем внешнее прерывание по изменению уровня на одном из пинов) и есть другой, который хотелось бы как-то синхронизировать с первым (например запуск АЦП), да еще и не трогая спящий (поддерживается только режим Idle) в это время ЦПУ, ну не красота ли? Причем возможных комбинаций множество, а каналов целых восемь штук, “соединяй” не хочу (вообще там еще есть и пара бонусов вроде нескольких видов событий, фильтра для внешних сигналов и какого-то особого режима Quadrature Decoding, но это тема следующих выпусков “Ох как щас расскажу...”).
- Обновленная система тактирования. 4 внутренних RC резонатора: на 32КГц (один с ультра низким энергопотреблением, другой более точный и откалиброванный на заводе), 2МГц и 32МГц. Теперь, как и “у взрослых” имеется внутренний синтезатор частоты (PLL - ФАПЧ) позволяющий с помощью разнообразных множителей и делителей (используя в качестве источников сигнала все что имеют частоту выше 400КГц) получить очень широкую гамму частот (в том числе которые могут превышать номинальные частоты ядра более чем в два раза…Да здравствует оверклокинг в рядах ембеддеров!), которые к тому же теперь могут задаваться “на лету” (программно). Осталось добавить разве что развитую систему внутренней калибровки и общее количество регистров задействованных под весь этот “базар” – их более одного десятка =)). Что уже начинаете вспоминать, как первый раз ужаснулись при виде даташита какого-нибудь AT92S2313, либо теперь считаете себя чересчур ущербными со своей ATMega16? Что уж тут поделать, прогресс, его так, причем стоит заметить, Atmel вовсе не позиционирует новое семейство как конкурент ARM’ам и тому подобному, это просто некое продолжение развития (перспективность которого идет на убыль в связи с ценами уже упоминавшихся ARM и прочих) старого семейства... Представляете все это богатство на каком-нибудь логгере температуры / влажности (что является как раз таки одним из сегментов позиционирования)? А ведь это еще далеко не все из новинок…
- Для любителей “поскрытнячать”, новые не новые (никогда не использовал если честно)
движки шифрации DES / AES.
- PMIC – Дожились, теперь благодаря Programmable Multi-Level Interrupt Controller’у каждое прерывание может иметь свой приоритет и много других плюшек, вроде режима для прерываний с низшим приоритетом, при котором им гарантируется время, даже если все время крутится большое количество более высокоприоритетных ISR (не знаю почему, но мне это напомнило базовый принцип операционной системы с вытесняющей многозадачностью, многозадачность это конечно совсем другая тема, но если некий контроллер теперь будет за нас (размечтался...) думать о вещах, про которые нам, ленивым человекам думать лениво, почему бы и нет, интересно). В общем целом, по функциональности PMIC, конечно же не дотягивает до “своего соперника” AIC в AT91SAM7xxx (ARM), но явно проще в использовании и к тому же является явным прогрессом после Мег, так что в этом плане по-моему жаловаться особого не на что.
- ADC / DAC. А вот тут нас поджидает маленький сюрприз… Машинки экипированы сразу АЦП и ЦАП! Но даже не это самое главное! АЦП 12-битный и с 2 (!!) миллионами выборок в секунду (2000ksps)! Сравните с 10-битным АЦП в Мегах c их 15ksps, да что там Меги, те же самые AT91SAM7xxx просто уходят в сад, особенно если мы добавим сюда 12-битный же ЦАП с 1000ksps.... Вообщем учитывая позиционирование, цену и класс этих устройств, ничего кроме уважения подобная формула не вызывает.
- Аналоговый компаратор также подвергся улучшениям, теперь у нас в распоряжении есть два компаратора (каждый висит на своем порте) входы которых могут выбираться в пределах целого порта, а также среди двух внутренних источников – источника опорного напряжения АЦП и выхода ЦАП. Также оба компаратора теперь могут объединяться в один и работать в так называемом режиме “окна”, когда сигнал с одного из входов будет сравниваться не с одним, а с двумя другими входами (таким образом, наш входной сигнал как бы находится в некоем интервале, а компаратор будет нам выдавать результат – входит или не в ходит в заданный лимит). Также приятным бонусом теперь является задаваемый программно (в пределах 20 и 50мВ) гистерезис (кто не в курсе, что за зверь – почитайте про такие хитрые устройства как триггеры Шмидта).
- IrDA – О возрадуйтесь любители дистанционных пультов управления!
- К TWI добавлена совместимость с SMBus – популярным ответвлением I2C (ну не хотят люди отстегивать Philips за сертификацию с I2C, вот и придумывают кучу разных имен для одного и того же по сути интерфейса) используемым преимущественно в мониторинге температур и кое-чего еще в ПК.
- Новые режимы функционирования входов / выходов (пинов). Теперь нам доступны 6 режимов:
- Totem-pole (push-pull).
- Pull-down.
- Pull-up.
- Bus-keeper.
- Wired-OR.
-Wired-AND.
- Обновленный 16-битный таймер-счетчик. О всех затронутых нововведениях так сразу даже и не рассказать, обратитесь к даташиту или AN AVR1306, разочарованны не будете!
После всего этого можно сказать с уверенностью, прогресс есть и он заметен! Новая линейка стала не только экономичнее и быстрее, но и функциональнее практически во всех аспектах, будь то таймеры или обычные компараторы, последовательные протоколы или АЦП / ЦАП – есть где разгуляться! Единственно чего реально не хватает, как мне кажется, это USB, но всему свое время, развитие всей линейки еще только на пороге старта и требовать всего и сейчас будет неразумно. Еще что хотелось бы сказать в заключение ...и вся эта красота в корпусе DIP... =)), но, к сожалению как вы понимаете, это невозможно... поэтому придется довольствоваться пока 100-лапковыми TQFP и столько же (но на этот раз шариков) CBGA (спокойствие, только спокойствие!)).
Итак, теперь более или менее разобравшись с основными нововведениями, приступим так сказать к освоению... И как я уже и говорил самый для этого верный путь – к “аппликухам”! Как и обычно, все что касается AVR содержится на странице:
Следуя логике, мы с вами начнем с AVR1000 Getting Started Writing C-code for XMEGA. Оговорюсь, этот материал (а возможно и следующие, если кто-то найдет это интересным) не является просто переводом оригинала, моя цель – следуя (более или менее) соответствующему AN, сделать изложение его материала максимально проще и доступнее пользователю с любым уровнем подготовки. Как у меня это получится – не знаю, но хотелось бы во всяком случае попробовать и услышать отзывы, а там посмотрим)).
P.S Прошу прощения у автора сайта-курса avr123.nm.ru за некоторые намеки-шутки в адрес его проекта, это лишь шутки ради, честное слово)). И кто бы, что не думал, я хотел бы еще раз поблагодарить этого человека за бескорыстный труд в просвещении масс по теме МК, если бы не вы, я бы без сомнения не обладал бы даже и толикой тех скромных знаний, чьими обладаю на сегодняшний день.
AVR1000 Начинаем писать C-код для XMEGA.
Действительно данный AN начинает всю серию неспроста, ибо для того, что бы понимать содержание следующих, нужно обязательно проникнуться в стиль написания кода для новых AVR, а оный несколько изменился... Для опытных пользователей языка C, ничего существенного, но весьма деликатно для остальных. Весь смысл в прочтении этой статьи, по сути, содержится в следующем абзаце:
This document introduces a naming convention and register access methods that are different from what AVR programming veterans are used to, but one should be aware that the “classic” way to access registers is still supported by the header files. This also applies on the bit level.
Основная концепция нового подхода к конфигурации МК, отличного от того что по мнению Atmel использовали и используют ветераны AVR программирования, состоит из двух вещей: понятия “модуль” и разнообразных битовых маск.
Что такое модуль, по мнению программистов Atmel? Все очень просто, модулем мы можем назвать USART, TWI, ADC, PORT и т.д. То есть, по сути, каждый модуль представляет собой кирпичики, составляющие весь МК в целом, а раз так, почему бы в таком случае не отказаться от придумывания все новых и новых имен регистров и не назвать их все на один лад, скрыв каждый за своим модулем? Подход, берущий свои корни еще в переходе от C к C++, нашел свой путь и здесь. “Странно”, но факт что TWI, что USART или WD имеют регистры контроля, статуса и настройки прерываний, поэтому по сути их все можно назвать CTRL, STATUS и INTCTRL, что собственно и было сделано…
Вот пример программной реализации одного из модулей:
- Код: Выделить всё
typedef struct PMIC_struct
{
unsigned char STATUS; // Status Register
unsigned char INTPRI; // Interrupt Priority
unsigned char CTRL; // Control Register
} PMIC_t;
Во-первых, структуры... Есть две новости, хорошая – вас не смогут послать на страницы 5 и 6 курса AVR123.nm.ru =)), плохая – вас все таки отправят за чтением большущих книг вроде K&R (. Поэтому кратко. Структура (struct), как и объединения (union) – основное средство языка C для формирования новых типов. Скажем, вам хочется сформировать простейший тип для объявления дробей:
- Код: Выделить всё
struct Drob // Дробь)).
{
char chislitel;
char znamenatel;
};
Как видите сначала мы помещаем в “коробку” две переменные, пасс, другой, ничего не происходит ((… А ничего происходить и не должно, это и есть основной принцип структур – приведение разрозненных (и не очень) переменных под одно имя (шапку). Ничего магического. Используем:
- Код: Выделить всё
struct Drob test; // Создаем новую переменную с только что определенным типом.
Потом вы увидите, как при объявлении избавиться от написания sctruct каждый раз, пока это не имеет никакого значения. Так вот, продолжаем, первое желание написать:
test = константа / число / переменная типа int...
Провалится под злорадный смех компилятора, использовать в этом случае можно только те переменные (члены) которые мы поместили в эту структуру, они то как раз и имеют тип char, что дает им право присваивать себе значение стандартных типов, сама переменная test не может присваиваться, умножаться или делиться ни с чем, даже с точно такой же переменной имеющей подобный ей тип, это основное положение при использовании структур, все остальное достаточно просто.
Для того что бы указать член, с которым будет производиться работа, используется обычная точка:
- Код: Выделить всё
test.chislitel = 1;
test.znamenatel = 2;
В итоге, в test у нас содержится как бы дробь ½, но это только для нас, для компилятора это массив (что в некоем роде и представляет собой структура) из двух переменных и ничего больше. То есть, как видите в данном примере весь функционал структуры, как и это часто бывает, ограничивается обычным сведением под “одну крышу”. Мы можем создать структуру хранения чего угодно, например содержания памяти темп. датчика и насоздавать членов tempLsb, tempMsb и т.д., потом объявить переменную с типом этой структуры и вызывать каждый член из нее, таким образом у нас все под рукой, прибрано и подсчитано, в отличие от того же массива, где нужно было бы помнить индекс расположения того или иного байта… Обычное приведение порядка, принцип “разделяй и властвуй”, находящий свое начало еще в самой концепции языка С.
Во-вторых, логические операции и операции сдвига, дальнейшее повествование подразумевает, что вы понимаете, как работают операторы &, |, << и ~. Если это не ваш случай – не переживайте, обратитесь к курсу AVR123.nm.ru или к теме Bit manipulation (AKA Programming 101) на форуме avrfreaks.net.
Вернемся к нашему примеру модуля:
- Код: Выделить всё
typedef struct PMIC_struct
{
unsigned char STATUS; // Status Register
unsigned char INTPRI; // Interrupt Priority
unsigned char CTRL; // Control Register
} PMIC_t;
Перед нами модуль контроллера прерываний, состоящий из трех регистров: STATUS, INTPRI и CTRL, вы можете убедиться самостоятельно такие регистры действительно существуют и относятся к PMIC (как собственно и к многим другим модулям).
Теперь что бы разрешить прерывания с высоким приоритетом (бит 2 – HILVLEN) мы можем написать следующее:
- Код: Выделить всё
PMIC.CTRL |= (1 << 2);
Здесь ясно видно, что, во-первых, мы работаем с контроллером прерываний (PMIC) и во-вторых, обращаемся к регистру управления CTRL, просто, понятно, а главное универсально.
Ах, да перед тем как закончить с темой модулей, необходимо не очень важная ремарка. Вы уже наверное заметили что в объявлении регистра CTRL выше, оный представляет собой обычную переменную char (8-бит, обычный размер регистра для таких МК) и естественно, для того что бы наша писанина в этот член структуры возымела хоть какие-нибудь действия на контроллер прерываний, необходимо его разместить по определенному адресу в памяти МК, таким собственно образом и появляются все OCR1, DDRD и прочие знакомые нам имена регистров, с разницей лишь то, что теперь адресация происходит “по-структурно”:
- Код: Выделить всё
#define ADCA (*(volatile ADC_t *) 0x0200) //Пример размещения модуля ADC в памяти МК.
То есть, даже не вникая в хитрый смысл загадочных скобок и звездочек (быстро! Пока вас не отправили на n’ную страницу курса, прочтите про указатели!), более или менее ясно, что берется желаемое имя модуля + определение структуры (в примере выше ADCA может прекрасным образом быть PMIC, а ADC_t – PMIC_t) и адресуется все это по месту, где располагаются реальные адреса этих регистров. Причем стоит заметить, объявление членов в такой структуре не дело случая, все рассчитано и все три регистра PMIC (что верно и для других модулей) располагаются рядом друг с другом, скажем (для примера) STATUS: 0x300, INTPRI: 0x301 и CTRL: 0x302, поэтому и в структуре регистры объявлены в таком же порядке, теперь при адресации каждый из них займет свое место.
С модулями покончено, второй принцип нового подхода – повсеместное использование предопределений (масок).
Всего существуют четыре вида подобных предопределений, каждый из которых дистанцируется от других своим суффиксом в окончании, давайте поочередно рассмотрим все три вида, в том порядке, в каком они представлены в AN.
Первое и самое простое – предопределения скрывающие за собой номера битов, подобный метод известен, пожалуй всем кто хоть раз использовал компиляторы ICC / IAR EW / WinAVR (хотелось бы еще добавить CodeVisionAVR, но его автор почему-то вместо того что бы реализовать такую в принципе несложную, но полезную опцию, добавляет в простой и понятный интерфейс новые меню и “тулбары”), давайте напишем для примера включение приемника и передатчика USART (справедливо для Мег 16 / 32 / 64):
- Код: Выделить всё
UCSRB = (1 << TXEN) | (1 << RXEN);
То же самое можно было бы начеркать и с использованием реальных номеров битов TXEN и RXEN, но так значительно нагляднее, не находите?
Так вот, в новых XMEGA все зашло еще несколько дальше, теперь подобные определения помимо имени обязаны нести:
- Префикс, относящий его к одному из модулей.
- Суффикс _bp (bit position) в конце определения.
Возьмем для примера 16-битный таймер (модуль TCD0) и его регистр управления CTRLD (всего их 5: CTRLA /B/C/D/E – это из-за того что так много опций)), в нем существует такой противный бит EVDLY – отвечающий за задержку сигнала события принимаемого таймером, на 1 цикл. Так вот его нам в усмерть как хочется установить...
Делаем:
- Код: Выделить всё
TCD0.CTRLD |= (1 << TC_EVDLY_bp);
Как видно здесь, TC – это префикс говорящий нам что данный бит относится к таймеру (Timer Counter), а окончание bp – что это предопределение позиции бита, то есть выполняемой операцией будет установка (очистка) только одного отдельно взятого бита.
Это был если хотите “старый метод”, использующий битовые позиций, данный путь оставлен скорее ради совместимости, чем для повседневного использования (если вы конечно хотите следовать за инициативой Atmel). Следующие три типа определений несут за собой несколько новый подход. Начнем с аналога битовым позициям, битовые маски – предопределения, содержащие число с единицей в позиции требуемого бита, простейший пример – бит PD5 занимающий 6 позицию от начала байта PORTD, то есть:
PD7-PD6-PD5-PD4-PD3-PD2-PD1-PD0, что бы установить PD5 в 1 с использованием битовых позиций, мы бы написали:
- Код: Выделить всё
#define PD5 5
PORTD |= (1 << PD5);
Битовая маска же позволяет избавиться от оператора сдвига (<<) и для этого использует
заранее “вычисленные” значения, при которых мы получим единицу в искомой позиции:
- Код: Выделить всё
#define PD5 0x20 // В двоичной системе счисления это даст 00100000
PORTD |= PD5;
Что несколько проще и быстрее. Теперь предыдущий пример с регистром CTRLD и битом
EVDLY, можно переписать вот таким образом:
- Код: Выделить всё
TCD0.CTRLD |= TC_EVDLY_bm;
Единственное изменение, которое претерпело определение бита EVDLY – окончание _bm
или bit mask – это был второй вид битовых масок.
Возможно, по началу это покажется несколько непривычным, но как мне кажется это лишь дело времени, особенно если учесть что все AN для новых серий будут писаться именно таким образом, у вас есть причины ради чего адаптироваться…
Третий вид называется “Групповая маска” или маска группы битов – такие маски учитывают сразу целую группу битов относящихся к определенной опции в регистре, возвращаясь все к тому же регистру CTRLD, модуля TCD0, можно отметить две группы битов – EVACT (биты 7,6,5) и EVSEL (биты 3,2,1,0), соответственно групповую маску для каждой из них можно (что и было сделано) объявить следующим образом:
- Код: Выделить всё
#define TC_EVACT_gm (TC_EVACT2_bm | TC_EVACT1_bm | TC_EVACT0_bm)
#define TC_EVSEL_gm (TC_EVSEL3_bm | TC_EVSEL2_bm | TC_EVSEL1_bm | TC_EVSEL0_bm)
Как и раньше, единственное отличие в их объявлении от тех, что относятся к отдельным битам – новый суффикс _gm (group mask), в остальном они несут все те же самые атрибуты что и первые, за разницей в использовании, конечно же. Эти маски нужны в первую очередь для очистки предыдущих значений перед записью новых. Вы всегда можете сделать это побитно, но данный метод более универсален и быстр, вот таким образом можно стереть текущую конфигурацию действий таймера при получении сигнала события (за это отвечают биты группы EVACT: EVACT2, EVACT1 и EVACT0):
- Код: Выделить всё
TCD0.CTRLD &= ~(TC_EVACT_gm);
Ну и последний, и наверное самый важный тип битовых предопределений – конфигурационные битовые маски (bit group configuration masks), как и предыдущие, они содержат в себе определение сразу несколько битов, формирующих определенную группу. Разве что теперь они несут в себе также и определенную конфигурацию этой группы битов. Для наглядного примера мы возьмем регистр INTCTRL модуля USARTC0 и установим приоритет прерывания по получению данных (Receive Complete) в средний уровень (за этот отвечает группа из битов RXINTLVL0 и RXINTLVL0):
- Код: Выделить всё
USARTC0.INTCTRL |= USART_RXCINTLVL_MED_gc;
USARTC0.
Как стало видно, теперь изменилось две вещи, как обычно другое окончание – на этот раз это _gc (group configuration) и некое обозначение _MED, после имени бита... Как нетрудно догадаться оно скрывает за собой всего лишь слово MEDIUM – мы ведь действительно хотели установить средний приоритет для прерывания? Ну а что следует писать, если нам угодно указать высокий / низкий приоритет, либо вообще его отключить? Для этого есть два пути, либо понадеявшись на логику попытаться угадать, либо заглянуть в список определений в заголовочных файлах компилятора. В данном случае можно в принципе без проблем угадать:
USART_RXCINTLVL_OFF_gc - Выключить прерывание.
USART_RXCINTLVL_HIGH_gc - Высокий приоритет.
USART_RXCINTLVL_LO_gc - Низкий… Почему нельзя было добавить W в конце LO, неизвестно)).
Но в более сложных (когда в дело идут сокращения) такое может и не сработать)), AN и заголовочные файлы вам в руки)).
Теперь, когда вы более или менее поняли предназначение, неплохо бы показать их реальное использование в купе с group masks. Это очень важно и важно из-за повсеместного использования логических операций “ИЛИ”, в самом деле, оные очень полезны и позволяют менять (устанавливать) значение отдельных битов без вреда для других, но с другой стороны, они также не подменяют старые значения… Предположим что до того как нам захотелось установить средний приоритет для прерывания RX, регистр INTCTRL уже содержал единицы в позициях битов RXINTLVL, скажем эта была одна из конфигураций вроде USART_RXCINTLVL_HIGH_gc, когда оба бита были установлены в 1:
00110000
Что даст операция USARTC0 INTCTRL |= USART_RXCINTLVL_MED_gc? Абсолютно верно – ничего. Все исходит из определения лог. операции “ИЛИ” – только 0 и 0 дадут 0, то есть мы можем применять любые комбинации (маски) к текущему содержанию этого регистра, но как стояли единицы в позициях битов RXINTLVL, так они стоять и будут (что ноль, что 1 вместе с 1 которая уже содержится в регистре дадут всегда 1). Выход – не использовать операцию “ИЛИ” вовсе и каждый раз переписывать (присваивать) все значение регистра сразу, либо перед “ИЛИ” очищать позиции битов, над которыми предстоит работать... Первый способ естественно не входит в директивы нового стиля Atmel, поэтому был выбран второй метод, для которого и были созданы групповые маски битов – _gm. Попробуем применить на практике:
- Код: Выделить всё
USARTC0.INTCTRL &= ~(USART_RXCINTLVL_gm); // Очищаем текущие позиции битов RXCINTLVL.
USARTC0.INTCTRL |= USART_RXCINTLVL_MED_gc; // Пишем новые значения…
Но даже и это не устроило программистов Atmel! Дело все в том, что как вы уже могли видеть выше, при адресации вся структура, а значит и каждый из будущих регистров объявляются как volatile – а вот это мои дорогие друзья, может принести несколько побочных эффектов... Для начала, что дает объявление переменной с идентификатором volatile? Самое главное – это дает понять компилятору, что переменная может изменять свое значение в любое время, например при возникновении прерывания. И если, имея дело с переменной без volatile компилятор (этим “грешат” серьезные компиляторы вроде IAR EW) решит что в определенный промежуток времени эту переменную никто не изменяет (например, в каком-нибудь цикле), он ее попросту “выкинет”, загрузит значение в регистр и будет с ним работать справедливо (по его мнению) полагая, раз ничего не меняется, зачем ее каждый раз перезагружать из памяти программ в регистр?
С volatile история иная, здесь мы как бы говорим компилятору:
- Ей ты, вот в нее я могу загрузить новые значения в любое время, и при ее встрече ты обязан мне ее загружать в регистр(ы) каждый раз, даже если твой оптимизатор не видит в этом резона!
То есть, если кратко – с идентификатором volatile вы можете быть уверены, что при каждой операции с такой переменной будет использоваться ее реальное (сиюсекундное) содержание, что как несложно догадаться очень и очень важно при общении с регистрами.
Теперь возвращаясь к нашим двум строчкам, где мы в начале стирали значение битовых позиций в INTCTRL, а потом писали в него новые значения, можно понять, почему Atmel считает такой способ неэффективным, плюсы рокового слова volatile теперь принимают другую сторону – каждая новая строчка кода теперь будет вызывать новое обращение к считыванию INTCTRL, а ведь мы его по сути после очистки и не трогали и никакой необходимости в этом увеличении результирующего кода, нет. Поздно, ставя volatile в объявлении мы уже приказали компилятору не использовать его продвинутые техники оптимизации... Выход? Выход состоит в сведении двух операций, а именно очистки (лог. “И” + инверсия) и лог. “ИЛИ” с присвоением, вместе. Таким образом, компилятор будет обязан читать содержимое регистра только один раз, в результате экономя размер итоговой прошивки:
- Код: Выделить всё
USARTC0.INTCTRL = (USARTC0.INTCTRL & ~USART_RXCINTLVL_gm) | USART_RXCINTLVL_MED_gc;
Слева направо: побитное лог. “ИЛИ” с содержимым в скобках, в скобках у нас очистка позиций битов RXCINTLVL, значит мы производим лог. “ИЛИ” между регистром INTCTRL с уже очищенным содержимым тех битов и новым, желаемым значением (USART_RXCINTLVL_MED_gc), в заключении присваиваем итоговый результат все тому же регистру INTCTRL. Согласен, возможно поначалу покажется несколько муторным))), но опять таки пару дней практики …
Тем более что нам вовсе не нужно очищать предыдущие значения всегда, допустим, меняется значение всего регистра:
- Код: Выделить всё
USARTC0.INTCTRL = USART_RXCINTLVL_MED_gc |
USART_TXCINTLVL_OFF_gc |
USART_DREINTLVL_LO_gc;
Тогда можно не беспокоиться о сохранности отдельных битов и применять прямое присвоение (которое заменит старое содержание регистра новым).
*Примечание: во всех выше приведенных примерах с регистром INTCTRL, модуля USART я руководствовался прежде всего самим AN AVR1000, а не даташитами на конкретные МК. Позже же проверяя оные, я с некоторым удивлением отметил, что никакого INTCTRL в них не существует вовсе, а те самые биты RXCINTLVL находятся в регистре CTRLA. Урок – доверяй, но проверяй...*
Ну вот собственно и все о масках (и модулях), данные положения являются конечно одними из основных, но все же не всеми которые затрагиваются в AN AVR1000, поэтому в заключении мы поговорим о еще кое-каких мелочах...
Во-первых, возможно вы уже заметили, что выше я (как собственно и Atmel) и словом не оговорился о структуре (содержании) конфигурационных битовых маск (те что заканчиваются на _gc) и не зря, потому что устроены они весьма и весьма хитро, взгляните на пример уже известной нам группы битов RXCINTLVL:
- Код: Выделить всё
typedef enum
{
USART_RXCINTLVL_OFF_gc = (0x00 << 4);
USART_RXCINTLVL_LO_gc = (0x01 << 4);
USART_RXCINTLVL_MED_gc = (0x02 << 4);
USART_RXCINTLVL_HI_gc = (0x03 << 4);
} USART_RXCINTLVL_t;
К чему спрашивается такие сложности? Почему то же самое бы не записать проще (пример для последней маски):
- Код: Выделить всё
#define USART_RXCINTLVL_OFF_gc (0x03 << 4)
// либо:
#define USART_RXCINTLVL_OFF_gc 0x30 //0b00110000
Вся соль находится в желании использовать подобные маски в виде параметров, передаваемых функциям, что во-первых уменьшает их количество, а во-вторых приносит в этот процесс некоего рода стандартизацию. Взгляните на пример одной из функций предлагаемого Atmel драйвера тактовой системы МК:
- Код: Выделить всё
void TC_ConfigClockSource(volatile TC_t *tc, TC_CLKSEL_t clockSelection)
{
tc->CTRLA = (tc->CTRLA & ~TC_CLKSEL_gm) | clockSelection;
}
Как видите, никакой острой необходимости в создании подобных функций нет, даже напротив, они только увеличат общий размер программы (из-за многочисленных вызовов функций), зато в качестве удобного инструмента в начале нашего штурма XMEGA будут незаменимыми помощниками. Пока что не будем принимать во внимание первый параметр и всецело сконцентрируемся на втором – clockSelection, а вернее его типе TC_CLKSEL_t, не правда ли чем-то похоже на ранее приведенный пример USART_RXCINTLVL_t (та же _t в конце…)? Все верно, мы специально объединяем каждую из групп конфигурационных масок к определенном типу. Таким образом, если заранее известен тип передаваемых / принимаемых масок у вас не получится ошибиться и отправить в функцию по настройке тактовой частоты, маску конфигурации таймера-счетчика, никакой другой особой причины запихивать такие маски в перечисления, у Atmel не было (перечисления – ключевое слово enum) – еще более бесхитростные конструкции, чем структуры или объединения, их “изучение” я оставляю на вашу совесть))).
Во-вторых, вернемся к первому параметру выше приведенной функции TC_ConfigClockSource, а именно tc – указателю на “таймерный” модуль. Вопрос в том, почему понадобился именно указатель, а не обычная переменная? Резона два: первый –
таймеров / каналов АЦП / еще много чего, может быть много и делать для каждого из них свою функцию трудоемко и неэффективно, но если каждый их модулей имеют одинаковые размеры / расположение регистров, значит их можно с легкостью передавать через указатели, второй – мы собираемся передавать модуль, а это структура из множества регистров или довольно увесистый блок в памяти… А что если настроить нужно (используя предлагаемые Atmel драйверы) весь МК? Как вы думаете много вы сможете “напередавать” таких вот четырех-восьми байтных блоков? Это попросту не эффективно с точки зрения размера исходного кода. Поэтому запомните простую истину – вместе, структуры и указатели друзья эффективного кода, передавая не весь блок, а только его адрес, вы (в зависимости от размера структуры экономите) значительное место в памяти. Теперь зная, что все модули уже объявлены в одном из заголовочных файлов компилятора мы можем поступить следующим образом:
- Код: Выделить всё
TC_ConfigClockSource(&TCC0, TC_CLKSEL_OFF_gc);
Как видите, перед именем модуля мы добавили оператор взятия адреса – &, то есть в функцию будет отправлен именно адрес, а не содержание модуля. Очень компактно и эффективно, нужно только понять принцип и резон, ради которого все это добро используется, когда вы это понимаете, все нужные детали тут же встают на свои места.
Ну и самое самое последнее о чем мне хотелось бы вам поведать – это напоминание очень интересной структуры входящей в язык C, а именно совместную работу операторов ? и :
- Код: Выделить всё
some_register = ... | (some_parameter ? SOME_BIT_bm : 0) | ...
Дело в том, что такой метод заменяющий связку операторов if-else находит применение в драйверах Atmel почти повсеместно и вместо того что бы поместить его в AN AVR1000 его почему-то цитируют в анонсах к каждому из заголовочных файлов… Кратко. Конструкция работает следующим образом: если some_parameter неравен нулю – выполнению будет подлежать SOME_BIT_bm, в ином случае – 0. Главный вопрос – в чем интерес использовать такие конструкции вместо привычного if-else? В качестве примера возьмем одну из функций драйвера Clock System:
- Код: Выделить всё
void CLKSYS_XOSC_Config (OSC_FRQRANGE_t freqRange, bool lowPower32kHz, OSC_XOSCSEL_t xoscModeSelection)
{
OSC.XOSCCTRL = (uint8_t) freqRange | (lowPower32kHz ? OSC_X32KLPM_bm : 0) |
xoscModeSelection;
}
Здесь нужно в первую очередь обратить внимание на параметр lowPower32kHz, от него будет зависеть выполнение строки:
- Код: Выделить всё
OSC.XOSCCTRL = (uint8_t) freqRange | (lowPower32kHz ? OSC_X32KLPM_bm : 0) | xoscModeSelection;
То есть если lowPower32kHz имеет значение true, в итоге получится следующая строка:
- Код: Выделить всё
OSC.XOSCCTRL = (uint8_t) freqRange | OSC_X32KLPM_bm | xoscModeSelection;
а если false:
- Код: Выделить всё
OSC.XOSCCTRL = (uint8_t) freqRange | 0 | xoscModeSelection;
/* либо, так как лог “ИЛИ” с нулем в данном случае не имеет никакого эффекта: */
OSC.XOSCCTRL = (uint8_t) freqRange | xoscModeSelection;
Итог, если мы хотим использовать 32КГц RC резонатор с ультранизким энергопотреблением и передаем в параметр lowPower32kHz, true – он будет включен, иначе – нет (просто проигнорирован).
А теперь для наглядности, давайте перепишем то же самое, но более привычным способом:
- Код: Выделить всё
void CLKSYS_XOSC_Config (OSC_FRQRANGE_t freqRange, bool lowPower32kHz, OSC_XOSCSEL_t xoscModeSelection)
{
OSC.XOSCCTRL = (uint8_t) freqRange | xoscModeSelection;
if(lowPower32kHz)
{
OSC.XOSCCTRL |= OSC_X32KLPM_bm ;
}
}
Привычнее? Да, но Atmel будет писать первым способом и как вы понимаете не зря, даже в таком маленьком примере видно – код c ? и : эффективнее и компактнее, хотите привыкайте и используйте, хотите нет, но знать (для того что бы в полную силу использовать предлагаемые Atmel примеры) как это работает вы обязаны.
Теперь точно все, удачи всем!