Как настроить смартфоны и ПК. Информационный портал

EasySTM32 - Порты микроконтроллера. STM32

Понимаю, что статья уже получилась довольно длинной и всем хочется поскорее написать какую-нибудь программулину, делающую хоть что-то полезное, и залить её в контроллер, но так уж вышло, что контроллеры STM32 несколько сложнее простейших восьмибитных железяк, поэтому сейчас речь снова пойдёт, как пишут некоторые нетерпеливые читатели, «не о том».

В этой части мы поговорим о том, с чем нам чаще всего придётся оперировать при работе с контроллером — о рабочих регистрах ядра Cortex-M3, о режимах его работы и о том, как контроллер включается.

Итак, в ядре Cortex-M3 имеется 13 регистров общего назначенияR0..R12 , регистр, используемый для хранения указателя стека, — R13 , регистр связи — R14 , счётчик команд — R15 и 5 регистров специального назначения.

Регистры общего назначения разделяются на младшие регистры — R0..R7 и старшие регистры — R8..R12 . Разница между ними в том, что некоторые 16-тибитные команды набора thumb-2 умеют работать только с младшими регистрами, а со старшими — не умеют.

Регистров R13 вообще-то говоря два, а не один. Первый называется MSP — указатель основного стека , а второй PSP — указатель стека процесса . Однако в каждый момент доступен только один из этих регистров. Какой именно — определяется в одном из регистров специального назначения. Зачем такое надо? Это сделано для возможности организации защиты операционной системы (ага, на этот контроллер можно поставить ОС, если хочется) от кривых прикладных программ. MSP используется обработчиками исключительных ситуаций и всеми программами, использующими привилегированный уровень выполнения (например ядро ОС), а PSP — используется программами, не требующими привилегированного уровня выполнения (например, прикладными программами от которых мы хотим защитить ядро ОС). Указатели стека всегда должны быть выровнены на границу 32-хбитного слова, т.е. два их младших бита всегда должны быть сброшены в ноль.

Регистр R14 называется LR (link register) — регистр связи и используется для запоминания адреса возврата при вызове подпрограмм.

Регистр R15 называется PC (program counter) — счётчик команд и используется для хранения адреса текущей выполняемой команды.

Теперь о специальных регистрах.

Регистр xPSR содержит флаги результатов выполнения арифметических и логических операций, состояние выполнения программы и номер обрабатываемого в данный момент прерывания. Иногда об этом регистре пишут во множественном числе. Это сделано потому, что к трём его частям можно обращаться как к отдельным регистрам. Эти «подрегистры» называются: APSR — регистр состояния приложения (тут как раз хранятся флаги), IPSR — регистр состояния прерывания (содержит номер обрабатываемого прерывания) и EPSR — регистр состояния выполнения . Полностью структура регистра xPSR приведена на рисунке ниже.

Флаги в регистре APSR стандартные:

  1. N (negative flag) — отрицательный результат операции
  2. Z (zero flag) — нулевой результат операции
  3. C (carry flag) — флаг переноса/займа
  4. V (overflow flag) — флаг переполнения
  5. Q (saturation flag) — флаг насыщения

В регистре PRIORITY MASK используется только нулевой бит (PRIMASK), который будучи установлен в единицу запрещает все прерывания с конфигурируемым приоритетом. После включения бит PRIMASK сброшен в ноль — все прерывания разрешены.

В регистре FAULT MASK также использует только нулевой бит (FAULTMASK), который будучи установлен в единицу запрещает все прерывания и исключения, кроме немаскируемого прерывания (NMI). После включения бит FAULTMASK сброшен в ноль — все прерывания разрешены.

Регистр BASEPRI используется для запрещения всех прерываний, значение приоритета которых больше или равно, чем записано в этом регистре. Тут надо сказать, что чем меньше значение — тем выше уровень приоритета. В регистре BASEPRI используются только младшие 8 бит.

Регистр CONTROL используется для управления одним из режимов процессора — режимом потока. Нулевой бит этого регистра (nPRIV) определяет уровень выполнения (привилегированный — Privilegied, или непривилегированный — Unprivilegied), а первый бит (SPSEL) — используемый указатель стека (MSP или PSP). Разница между привилегированным и непривилегированным уровнями выполнения состоит в том, что для привилегированного уровня доступны все области памяти и все команды процессора, а для непривилегированного уровня некоторые области памяти закрыты (например, регистры специального назначения, кроме APSR, системная область) и, соответственно, команды для доступа в эти обасти — запрещены. Попытка выполнения запрещённых команд, пытающихся обратиться в закрытые области памяти вызывает генерацию исключения отказа.

Теперь о режимах работы процессора.

Процессор Cortex-M3 имеет два режима работы: режим потока (Thread) и режим обработчика (Handle). Режим Handle используется для обработки исключительных ситуаций, а режим Thread — для выполнения всего остального кода. Переключение из одного режима в другой происходит автоматически. Как мы уже говорили, когда разбирались с регистром CONTROL, в режиме Thread процессор может использовать как привилегированный уровень выполнения, так и непривилегированный, в режиме Handle — только привилегированный. Аналогично, в режиме Thread может использоваться как основной стек (MSP), так и стек процесса (PSP), а в режиме Handle — только основной стек.

Важно понимать, что, например, переключившись в режиме Thread с привилегированного уровня в непривилегированный, мы потеряем доступ в регистр CONTROL и обратно сможем переключиться только в режиме Handle. В режиме Handle бит nPRIV регистра CONTROL доступен для чтения/записи, но не влияет на текущий режим выполнения. Это позволяет изменить уровень выполнения, который будет у программы, когда процессор выйдет из режима обработчика в режим потока. Бит SPSEL в режиме Handle для записи недоступен и всегда читается как ноль, а при выходе из режима обработчика в режим потока восстанавливается автоматически. Все варианты переходов между различными режимами и уровнями выполнения иллюстрирует ориентированный граф, представленный на рисунке ниже:

Стартует контроллер всегда на внутреннем генераторе, на частоте 8 Мгц. Откуда брать тактовый сигнал в дальнейшем, на сколько его умножать или делить — настраивается в программе. Если в программе этого не сделать, то хоть десять внешних кварцев повесьте, контроллер так и будет работать от внутреннего генератора 8 МГц.

При старте контроллер анализирует сочетание уовней на двух своих ногах — BOOT0, BOOT1, и, в зависимости от этого сочетания, начинает загрузку либо из flash-памяти, либо из ОЗУ, либо из системной области памяти. Это делается с помощью уже описанного нами ранее механизма псевдонимизации. По идее загрузка всегда начинается с нулевого адреса, просто в зависимости от
сочетания на ногах BOOT0, BOOT1 начальные адреса памяти назначаются псевдонимами одной из трёх областей: flash, встроенного ОЗУ или системной области. Справа приведена табличка, в которой указано, какая именно область проецируется в начальные адреса памяти в зависимости от сочетания ног BOOT0, BOOT1.

При этом в системной области производителем зашита специальная программа (bootloader), которая позволяет запрограммировать flash-память. Но об этом позже.

Первым делом контроллер считывает 32-х битное слово по адресу 0x00000000 и помещает его в регистр R13 (указатель стека). Далее он считывает 32-х битное слово по адресу 0x00000004 и помещает его в регистр R15 (счётчик команд). Последнее действие вызывает переход на начало программного кода и дальше начинается выполнение программы.

Слово по адресу 0x00000004 (адрес начала основной программы) называется вектор сброса. Вообще в памяти контроллера после указателя стека по адресу 0x00000000, начиная с адреса 0x00000004 должна лежать таблица векторов исключений и прерываний, первый вектор в которой — это вектор сброса, а остальные вектора — адреса процедур обработчиков различных исключений и прерываний. В простейших программах, если вы не собираетесь обрабатывать исключения и прерывания, все остальные вектора, кроме вектора сброса, могут отсутствовать. Хотелось бы обратить внимание, что в таблице векторов указываются именно адреса начала обработчиков, а не команды перехода на эти обработчики, как, например, в 8-ми битных пиках или атмелах.

Доброго времени суток! Сегодня мы займемся изучением GPIO! И, в первую очередь, давайте посмотрим в каких режимах могут работать порты ввода-вывода в STM32F10x. А режимов этих существует море, а именно:

  • Input floating
  • Input pull-up
  • Input-pull-down
  • Analog
  • Output open-drain
  • Output push-pull
  • Alternate function push-pull
  • Alternate function open-drain

А если по-нашему, то при работе на вход:

  • Вход – Hi-Z
  • Вход – подтяжка вверх
  • Вход – подтяжка вниз
  • Вход – аналоговый

При работе порта на выход имеем следующие варианты:

  • Выход – с открытым коллектором
  • Выход – двухтактный
  • Альтернативные функции – выход типа «с открытым коллектором»
  • Альтернативные функции – двухтактный выход

Вот кстати документация на STM32F103CB –

В даташите есть внушительная таблица, в которой показано, какие альтернативные функции имеются у конкретного вывода.

Вот, например, выводы PA9, PA10:

В столбце Default видим, какие функции будут выполнять эти пины при их настройке для работы в режиме Alternative function. То есть, настроив эти пины соответствующим образом они из просто PA9 и PA10 превратятся в Rx и Tx для USART1. А для чего же тогда столбец Remap ? А это не что иное, как очень полезная функция ремаппинга портов. Благодаря ремапу, Tx USARTA ’а , например, может переместится с пина PA9 на PB6. Довольно часто эта функция оказывается чертовски полезной.

Ну с режимами вроде бы все более-менее понятно, пришло время окинуть взором регистры, которыми управляются порты ввода-вывода.

Раз уж только что обсудили в каких режимах могут существовать выводы STM32F10x, сразу же давайте прошарим как же их можно собственно перевести в нужный режим. А для этого выделены аж два регистра – CRL и CRH. В первом конфигурируются выводы от 0 до 7, во втором, соответственно от 8 до 15. Регистры, как вы помните, 32-разрядные. То есть на 8 выводов приходтся 32 бита – получается 4 бита на одну ножку. Открываем даташит и видим:

Например, надо нам настроить ножку PB5. Идем в регистр GPIOB->CRL и выставляем сответствующие биты так как нам требуется (на картинке 32-х битный регистр CRL). Для PB5 это биты:

После восьмибиток может показаться все достаточно сложным и каким то корявым, но на самом деле реализовано все довольно изящно =). Посмотрим, что тут есть еще.

Выходной регистр GPIOx_ODR – напоминает регистр PORTx в AVR. Все что попадает в этот регистр сразу же попадает во внешний мир. Регистр 32-разрядный, а ножек всего 16. Как думаете, для чего используются оставшиеся 16? Все очень просто, биты регистра с 15 по 31 не используются вовсе)

Входной регистр GPIOx_IDR – аналог PINx в AVR. Структура его похожа на упомянутую структуру ODR. Все, что появляется на входе микроконтроллера, сразу же оказывается во входном регистре IDR.

Еще два полезных регистра GPIOx_BSSR и GPIOx_BRR. Они позволяют менять значения битов в регистре ODR напрямую, без использования привычных бит-масок. То есть, хочу я, например, выставить в единицу пятый бит ODR. Записываю единичку в пятый бит GPIOx_BSSR, и все, цель достигнута. Вдруг захотелось сбросить пятый бит ODR – единицу в 5 бит GPIOx_BRR и готово.

Итак, основные регистры рассмотрели, но, на самом-то деле, мы в наших примерах будем делать все иначе, используя Standard Peripheral Library. Так что лезем ковырять библиотеку. За GPIO в SPL отвечают файлы stm32f10x_gpio.h и stm32f10x_gpio.c . Открываем их оба и видим очень много непонятных цифр-букв-значков итд.

На самом деле, все очень просто и понятно. За конфигурацию портов отвечает структура GPIO_InitTypeDef .

typedef struct { uint16_t GPIO_Pin; // Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define */ GPIOSpeed_TypeDef GPIO_Speed; // Specifies the speed for the selected pins. This parameter can be a value of @ref GPIOSpeed_TypeDef */ GPIOMode_TypeDef GPIO_Mode; // Specifies the operating mode for the selected pins. This parameter can be a value of @ref GPIOMode_TypeDef */ } GPIO_InitTypeDef;

Видим, что структура имеет три поля: GPIO_PIN, GPIO_Speed и GPIO_Mode . Нетрудно догадаться, что первая отвечает за номер ножки порта, которую мы хотим настроить, вторая – за скорость работы порта, ну и третья, собственно, за режим работы. Таким образом, для настройки вывода нам всего лишь нужно объявить переменную типа структуры и заполнить ее поля нужными значениями. Все возможные значения полей тут же – в stm32f10x_gpio.h . Например,

typedef enum { GPIO_Mode_AIN = 0x0 , GPIO_Mode_IN_FLOATING = 0x04 , GPIO_Mode_IPD = 0x28 , GPIO_Mode_IPU = 0x48 , GPIO_Mode_Out_OD = 0x14 , GPIO_Mode_Out_PP = 0x10 , GPIO_Mode_AF_OD = 0x1C , GPIO_Mode_AF_PP = 0x18 } GPIOMode_TypeDef;

Все значения уже рассчитаны создателями SPL, так что для настройки какого-нибудь вывода для работы в режиме Output push-pull надо всего лишь в соответствующей структуре задать поле: GPIO_Mode = GPIO_Mode_Out_PP.

Ну вот, структура объявлена, поля заполнены как надо, что же дальше? Ведь мы всего лишь создали переменную. Причем тут регистры, микроконтроллеры и вообще электроника? Лезем в файл stm32f10x_gpio.c и находим там тучу различных функций для работы с STM32 GPIO. Рассмотрим функцию GPIO_Init() (код приводить не буду, все в файле библиотеки). Так вот, эта функция как раз и связывает нашу созданную структуру и конкретные регистры контроллера. То есть мы передаем в эту функцию переменную, в соответствии с которой выставляются нужные биты нужных регистров микроконтроллера. Все очень просто, но от этого не менее гениально. Поковыряйте еще файлы библиотеки. Там функции на любой случай есть) Кстати очень удобно – перед функцией идет описание переменных, которые она принимает и возвращает, а также описание собственно того, что эта функция призвана делать. Так что, разобраться несложно, но надо немного дружить с английским. Хотя без этого никуда;)

Отвлечемся ненадолго от портов ввода-вывода и обсудим один довольно тонкий момент. Чтобы использовать порты, либо любую другую периферию, ОБЯЗАТЕЛЬНО надо включить тактирование. И порты, и периферия изначально отключены от тактирования, так что без этого действия ничего не заведется. Программа скомпилируется, но на деле работать ничего не будет. За тактирование в SPL отвечают файлы stm32f10x_rcc.c и stm32f10x_rcc.h . Не забывайте добавлять их в проект.

Давайте уже перейдем к программированию. Как это принято, заставим диодик помигать) Чтобы получше разобраться с Standard Peripheral Library немножко усложним обычное мигание диодом – будем опрашивать кнопку, и если она нажата – диод загорается, иначе – гаснет. Запускаем Keil, создаем проект, добавляем все нужные файлы, не забываем про CMSIS. Из SPL для этого проекта нам понадобятся 4 файла, уже упомянутые выше. Создание нового проекта описано в предыдущей статье учебного курса. Также там можно найти ссылки на библиотеки)

Итак, код:

/****************************gpio.c*********************************/ //Подключаем все нужные файлы #include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_gpio.h" //Тут будет вся инициализация всей использующейся периферии void initAll() { //Объявляем переменную port типа GPIO_InitTypeDef GPIO_InitTypeDef port; //Это функция из файла stm32f10x_rcc.c, включает тактирование на GPIOA //GPIOA сидит на шине APB2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE) ; //Про эту функцию напишу чуть ниже GPIO_StructInit(& port) ; //Заполняем поля структуры нужными значениями //Первый вывод – вход для обработки нажатия кнопки – PA1 port.GPIO_Mode = GPIO_Mode_IPD; port.GPIO_Pin = GPIO_Pin_1; port.GPIO_Speed = GPIO_Speed_2MHz; //А про эту функцию мы уже говорили //Отметим только что один из параметров – указатель(!) на //нашу структуру GPIO_Init(GPIOA, & port) ; //Настраиваем вывод, на котором будет висеть диодик – PA0 port.GPIO_Mode = GPIO_Mode_Out_PP; port.GPIO_Pin = GPIO_Pin_0; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, & port) ; } /*******************************************************************/ int main() { //Объявляем переменную для хранения состояния кнопки uint8_t buttonState = 0 ; initAll() ; while (1 ) { //С помощью функции из SPL считываем из внешнего мира //состояние кнопки buttonState = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) ; if (buttonState == 1 ) { GPIO_SetBits(GPIOA, GPIO_Pin_0) ; } else { GPIO_ResetBits(GPIOA, GPIO_Pin_0) ; } } } /****************************End of file****************************/

Кстати, возможно кто-то обратит внимание на наличие скобок { }, несмотря на всего лишь одну инструкцию в теле if и else . А это уже привычка) Очень рекомендуется так писать, особенно при разработке крупных проектов. При дописывании/исправлении программы невнимательный программист может не обратить внимания на отсутствие скобок и дописать вторую инструкцию, которая, как вы понимаете, уже окажется все блока if или else . Та же тема с циклами. Когда над проектом работает много народу, нет никаких гарантий, что кто-нибудь не окажется невнимательным, так что, чтобы не тратить минуты/часы на последующие поиски косяка, рекомендую ставить эти скобки всегда) Хотя может, кто-то и не согласится с такой логикой.

Нажимаем F7, компилируем, и вот наша первая программа для STM готова. Вроде бы код довольно подробно откомментирован, так что поясню только пару моментов.

Функция GPIO_StructInit(&port) – принимает в качестве аргумента адрес переменной port .

Эта функция заполняет поля структуры, переданной ей в качестве аргумента дефолтными значениями. Это делать не обязательно, но во избежание каких-нибудь непредсказуемых косяков, лучше всегда вызывать эту функцию.

Еще две функции, которые мы использовали:

  • GPIO_SetBits(GPIOA, GPIO_Pin_0);
  • GPIO_ResetBits(GPIOA, GPIO_Pin_0);

Ну вы и так догадались для чего они 😉

Итак мы закончили рассматривать STM32 порты ввода-вывода. В следующей статье познакомимся со средствами Keil’а для отладки.

Основные регистры порта ввода/вывода микроконтроллера STM32

Под портом понимается определенный именованный набор ног микроконтроллера. В STM микроконтроллерах они именуются как GPIOA,GPIOB,GPIOC и т.д. Порты ввода/вывода в микроконтроллерах STM32 имеют, как правило, по 16 линий (ног). Под линией понимается та или иная ножка микроконтроллера. Каждая линия порта может быть сконфигурирована определенным образом и выполнять следующие функции:

  • цифрового ввода;
  • цифрового вывода;
  • входа внешнего прерывания;
  • функцию ввода/вывода других модулей микроконтроллера.

Сконфигурировать порт на нужный режим работы можно использую регистры микроконтроллера. К этим регистрам можно обращаться напрямую, а также использовать специальные методы из библиотеки периферий.

Давайте рассмотрим основные регистры необходимые для работы с портами ввода/вывода.

Регистры отвечающие за конфигурацию порта

Прежде чем начать работу с портом вывода его нужно сконфигурировать под ваши нужды.

За настройку или конфигурацию порта отвечают регистры конфигурации. В микроконтроллерах семейства STM32F302xx, STM32F303xx и STM32F313xx это следующие регистры:

  • GPIOx_MODER;
  • GPIOx_OTYPER;
  • GPIOx_OSPEEDR;
  • GPIOx_PUPDR.

Регистр GPIOx_MODER (где x=A...F)

Это 32-х битный регистр отвечает за режим работы линии. Для конфигурации опеределенной линии требует 2 бита. Возможны следующие комбинации:

  • 00 - линия настроена на вход;
  • 01 - на выход;
  • 10- режим альтернативной функции;
  • 11 - аналоговый режим.

Регистр GPIOx_TYPER (где x=A...F)


Данный регистр используеться для настройки типа работы линии использует в работе 16 бит остальные 16 зарезервированны. Принемает следующие значения:

  • 0 - пуш пульнай режим;
  • 1 - открытый сток

Регистр GPIOx_PUPDR (где x=A...F)


Данные регистр отвечает за подтяжку. Принемает следующие значения:

  • 00 - без подтяжки
  • 01 - подтяжка к плюсу питания
  • 10 - подтяжка к земле

Регистр GPIOx_SPEEDR (где x=A...F)


Регистр настройки скорости работы линии.

  • 00 - 2 МГц;
  • 01 - 10 МГц;
  • 10 - 50 МГц.

Регистр вывода (выходной регистр) GPIOx_ODR (где x=A…F) – output data register


Данный регистр используется для вывода данных в порт. При записи определенных значений в данный регистр на линии (ножке) устанавливается аналогичное значение. Так как у нас 16 линий а регистр имеет 32 бита, то используются только первые (младшие) 16 бит.

Регистр ввода (регистр состояния порта или входных данных) GPIOx_IDR (где x=A…F) – input data register


Данный регистр доступен только для чтения. Своего рода индикатор состояния порта. Аналог PINxв микроконтроллерах серии AVR.

Регистр побитовой установки выходного порта GPIOx_BSRR


Данный регистр побитно устанавливает состояние ножки в порте вывода.

Подробнее о всех регистрах конкретного микроконтроллера можно узнать в referens manual.pdf который можно скачать с официального сайт www.st.com

Настройка порта микроконтроллера при помощи библиотеки

Также порт можно настроить при помощи специальной библиотеки, в которой находятся разные методы для работы с регистрами ввода/вывода, а также объявлены специальные переменные. Данная библиотека освобождает программиста от необходимости «вручную» вычислять какое значение нужно записать в тот или иной регистр.

Рассмотрим пример кода включающий светодиод

#include "stm32f30x.h" // Device header #include "stm32f30x_rcc.h" #include "stm32f30x_gpio.h" void init_led(void) { RCC_APB2PeriphClockCmd (GPIOE,ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; //Задаем максимальную скорость GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //Режим выход GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //Указываем ножку к которой присоединен светодиод GPIO_Init(GPIOE,GPIO_InitStructure); // Инициальзация структуры } int main(void) { init_led(); GPIO_SetBits(GPIOE,GPIO_Pin_8); //Кстанавливаем высокое состояние на ножке while(1) { _NOP(); } }

В целях экономии электроэнергии, вся периферия у микроконтроллеров отключена. Для того чтобы «активировать» ту или иную периферию необходимо для начала подать на него тактовые сигналы.

Мы работаем с портом GPIOE поэтому нам необходимо включить тактирование при помощи метода

RCC_APB2PeriphClockCmd (uint32_t RCC_APB2Periph, FunctionalState NewState);

Который принимает два значение первый это собственно сам порт который нам необходимо включить, а второй это состояние данного порта, включен или выключен.

При возникновении, некоторого события контроллер прерываний автоматически прерывает выполнение основной программы, и вызывает соответствующую функцию обработки прерываний. После выхода из функции обработчика прерываний программа продолжает выполнение с того места, где произошло прерывание. Все происходит автоматически (при правильной настройке NVIC, но об этом ниже).

Из самого названия видно, что контроллер NVIC поддерживает вложенность прерываний и приоритеты. Каждому прерыванию при настройке NVIC присваивается свой приоритет. Если во время обработки низкоприоритетного прерывания возникает высокоприоритетное, то оно, в свою очередь, прервет обработчик низкоприоритетного прерывания.

Как это работает?

Данный пост не претендует на абсолютную полноту, я советую изучить раздел прерываний в Cortex™-M3 Technical Reference Manual . Поскольку эта часть ядра не претерпела изменений, ее описание дано в первой ревизии r1p1 на ядро Cortex-M3.
Вход в прерывание и выход из него
При инициации прерывания NVIC переключает ядро в режим обработки прерывания. После перехода в режим обработки прерывания регистры ядра помещаются в стек. Непосредственно во время записи значения регистров в стек осуществляется выборка начального адреса функции обработки прерывания.

В стек перемещается регистр регистр статуса программы ( Program Status Register (PSR) ), счетчик программы (Program Counter (PC) ) и регистр связи (Link Register (LR) ). Описание регистров ядра приведено в Cortex-M4 Generic User Guide . Благодаря этому, запоминается состояние, в котором находилось ядро перед переходом в режим обработки прерываний.

Также сохраняются регистры R0 - R3 и R12. Эти регистры используются в инструкциях для передачи параметров, поэтому, помещение в стек делает возможным их использование в функции обработки прерывания, а R12 часто выступает в роли рабочего регистра программы.

По завершении обработки прерывания все действия выполнятся в обратном порядке: извлекается содержимое стека и, параллельно с этим, осуществляется выборка адреса возврата.

С момента инициации прерывания до выполнения первой команды обработчика прерывний проходит 12 тактов, такое же время необходимо для возобновления основной программы после завершения обработки прерывания.

Вложенность прерываний
Как было сказано выше NVIC поддерживает прерывания с различными приоритетами, которые могут прерывать друг друга. При этом, могут возникнуть различные ситуации, обработка которых по разному оптимизирована.

1. Приостановка низкоприоритетного прерывания
В этой ситуации, обработка низкоприоритетного прерывания прекращается. Следующие 12 циклов выполняется сохранение в стек нового набора данных и запускается обработка высокоприоритетного прерывания. После его обработки, содержимое стека автоматически извлекается и возобновляется обработка низкоприоритетного прерывания.
Больших отличий от прерывания основной программы не наблюдается.

2. Непрерывная обработка прерываний
Эта ситуация может возникнуть в двух случаях: если два прерывания имеют одинаковый приоритет и возникают одновременно, если низкоприоритетное прерывание возникает во время обработки высокоприоритетного.
В этом случае, промежуточные операции над стеком не производятся. Происходит только загрузка адреса обработчика низкоприоритетного прерывания и переход к его выполнению. Отказ от операций над стеком экономит 6 тактов. Переход к следующему прерыванию происходит не за 12 тактов, а всего за 6.

3. Запаздывание высокприоритетного прерывания
Ситуация возникает, если высокоприоритетное прерывание происходит во перехода к обработке низкоприоритетного (за те самые 12 тактов). В этом случае переход к высокоприоритетному прерыванию будет происходить не менее 6 тактов с момента его возникновения (время необходимое для загрузки адреса обработчика прерывания и перехода к нему). Возврат в низкоприоритетное уже описан выше.

Приоритеты прерываний
Помимо простой установки приоритета прерываний, NVIC реализует возможность группировки приоритетов.
Прерывания в группе с более высоким приоритетом могут прерывать обработчики прерываний группы с более низким приоритетом. прерывания из одной группы, но с разным приоритетом внутри группы не могут прерывать друг друга. Приоритет внутри группы определяет только порядок вызова обработчика, когда были активизированы оба события.

Значение приоритета прерывания задается в регистрах Interrupt Priority Registers (см. Cortex-M4 Generic User Guide). При этом, часть бит отвечает за приоритет группы, в которой находится прерывание, а часть - за приоритет внутри группы.
Настройка распределение бит на приоритет группы или приоритет внутри группы осуществляется с помощью регистра Application Interrupt and Reset Control Register (ВНИМАТЕЛЬНО!!! см. Cortex-M4 Generic User Guide).

Как вы, наверно, заметили, в Cortex-M4 Generic User Guide сказано, что настройка приоритетов и группировки приоритетов зависят от конкретной реализации implementation defined .
А вот дальше не очень приятная вещь. В к МК STM32F407 про NVIC почти нет информации. Но есть ссылка на отдельный документ. Для того, чтобы разобраться с реализацией NVIC в STM32 придется прочитать еще один документ - . Вообще говоря, я советую внимательно изучить данный документ и по всем другим вопросам, в нем работа ядра расписана более подробно, чем в документации от ARM.
В нем, уже можно найти:

A programmable priority level of 0-15 for each interrupt. A higher level corresponds to a
lower priority, so level 0 is the highest interrupt priority

Из возможных 8 бит приоритета используются только 4. Но этого вполне достаточно для большинства задач.
Маскирование прерываний
Предположим, что у нас стоит задача запуска ракеты-носителя при нажатии на красную кнопку, но только при условии, что повернут ключ.
Нет совершенно ни какого смысла генерировать прерывание на поворот ключа. А вот прерывание на нажатие красной копки нам понадобится. Для того, чтобы включать/выключать различные вектора прерываний, существует маскирование прерываний.
Маскирование прерывания осуществляется с помощью регистров Interrupt Set-enable Registers .
Если прерывание замаскировано, это не означает, что периферия не генерирует события! Просто NVIC не вызывает обработчик этого события.
Таблица векторов прерываний
Все возможные прерывания, поддерживаемые NVIC, записываются в таблицу векторов прерываний. По сути своей, таблица векторов прерываний есть ни что иное, как список адресов функций обработчиков прерываний. Номер в списке соответствует номеру прерывания.
Создаем таблицу векторов и располагаем ее в правильном месте
Для тог, чтобы таблица векторов с правильными адресами функций обработчиков прерываний располагались в начале флеш памяти МК, создадим и подключим к проекту файл startup.c.

Содержимое файла

// Enable the IAR extensions for this source file. #pragma language=extended #pragma segment="CSTACK" // Forward declaration of the default fault handlers. void ResetISR(void); static void NmiSR(void); static void FaultISR(void); static void IntDefaultHandler(void); // The entry point for the application startup code. extern void __iar_program_start(void); extern void EXTI_Line0_IntHandler(void); extern void EXTI_Line6_IntHandler(void); // A union that describes the entries of the vector table. The union is needed // since the first entry is the stack pointer and the remainder are function // pointers. typedef union { void (*pfnHandler)(void); void * ulPtr; } uVectorEntry; // The vector table. Note that the proper constructs must be placed on this to // ensure that it ends up at physical address 0x0000.0000. __root const uVectorEntry __vector_table @ ".intvec" = { { .ulPtr = __sfe("CSTACK") }, // The initial stack pointer ResetISR, // The reset handler NmiSR, // The NMI handler FaultISR, // The hard fault handler IntDefaultHandler, // MPU Fault Handler IntDefaultHandler, // Bus Fault Handler IntDefaultHandler, // Usage Fault Handler IntDefaultHandler, // Reserved IntDefaultHandler, // Reserved IntDefaultHandler, // Reserved IntDefaultHandler, // Reserved IntDefaultHandler, // SVCall Handler IntDefaultHandler, // Debug Monitor Handler IntDefaultHandler, // Reserved IntDefaultHandler, // PendSV Handler IntDefaultHandler, // SysTick Handler //External Interrupts IntDefaultHandler, // Window WatchDog IntDefaultHandler, // PVD through EXTI Line detection IntDefaultHandler, // Tamper and TimeStamps through the EXTI line IntDefaultHandler, // RTC Wakeup through the EXTI line IntDefaultHandler, // FLASH IntDefaultHandler, // RCC EXTI_Line0_IntHandler, // EXTI Line0 IntDefaultHandler, // EXTI Line1 IntDefaultHandler, // EXTI Line2 IntDefaultHandler, // EXTI Line3 IntDefaultHandler, // EXTI Line4 IntDefaultHandler, // DMA1 Stream 0 IntDefaultHandler, // DMA1 Stream 1 IntDefaultHandler, // DMA1 Stream 2 IntDefaultHandler, // DMA1 Stream 3 IntDefaultHandler, // DMA1 Stream 4 IntDefaultHandler, // DMA1 Stream 5 IntDefaultHandler, // DMA1 Stream 6 IntDefaultHandler, // ADC1, ADC2 and ADC3s IntDefaultHandler, // CAN1 TX IntDefaultHandler, // CAN1 RX0 IntDefaultHandler, // CAN1 RX1 IntDefaultHandler, // CAN1 SCE EXTI_Line6_IntHandler, // External Lines IntDefaultHandler, // TIM1 Break and TIM9 IntDefaultHandler, // TIM1 Update and TIM10 IntDefaultHandler, // TIM1 Trigger and Commutation and TIM11 IntDefaultHandler, // TIM1 Capture Compare IntDefaultHandler, // TIM2 IntDefaultHandler, // TIM3 IntDefaultHandler, // TIM4 IntDefaultHandler, // I2C1 Event IntDefaultHandler, // I2C1 Error IntDefaultHandler, // I2C2 Event IntDefaultHandler, // I2C2 Error IntDefaultHandler, // SPI1 IntDefaultHandler, // SPI2 IntDefaultHandler, // USART1 IntDefaultHandler, // USART2 IntDefaultHandler, // USART3 IntDefaultHandler, // External Lines IntDefaultHandler, // RTC Alarm (A and B) through EXTI Line IntDefaultHandler, // USB OTG FS Wakeup through EXTI line IntDefaultHandler, // TIM8 Break and TIM12 IntDefaultHandler, // TIM8 Update and TIM13 IntDefaultHandler, // TIM8 Trigger and Commutation and TIM14 IntDefaultHandler, // TIM8 Capture Compare IntDefaultHandler, // DMA1 Stream7 IntDefaultHandler, // FSMC IntDefaultHandler, // SDIO IntDefaultHandler, // TIM5 IntDefaultHandler, // SPI3 IntDefaultHandler, // UART4 IntDefaultHandler, // UART5 IntDefaultHandler, // TIM6 and DAC1&2 underrun errors IntDefaultHandler, // TIM7 IntDefaultHandler, // DMA2 Stream 0 IntDefaultHandler, // DMA2 Stream 1 IntDefaultHandler, // DMA2 Stream 2 IntDefaultHandler, // DMA2 Stream 3 IntDefaultHandler, // DMA2 Stream 4 IntDefaultHandler, // Ethernet IntDefaultHandler, // Ethernet Wakeup through EXTI line IntDefaultHandler, // CAN2 TX IntDefaultHandler, // CAN2 RX0 IntDefaultHandler, // CAN2 RX1 IntDefaultHandler, // CAN2 SCE IntDefaultHandler, // USB OTG FS IntDefaultHandler, // DMA2 Stream 5 IntDefaultHandler, // DMA2 Stream 6 IntDefaultHandler, // DMA2 Stream 7 IntDefaultHandler, // USART6 IntDefaultHandler, // I2C3 event IntDefaultHandler, // I2C3 error IntDefaultHandler, // USB OTG HS End Point 1 Out IntDefaultHandler, // USB OTG HS End Point 1 In IntDefaultHandler, // USB OTG HS Wakeup through EXTI IntDefaultHandler, // USB OTG HS IntDefaultHandler, // DCMI IntDefaultHandler, // CRYP crypto IntDefaultHandler, // Hash and Rng IntDefaultHandler, // FPU }; // This is the code that gets called when the processor first starts execution // following a reset event. Only the absolutely necessary set is performed, // after which the application supplied entry() routine is called. Any fancy // actions (such as making decisions based on the reset cause register, and // resetting the bits in that register) are left solely in the hands of the // application. void ResetISR(void) { // // Call the application"s entry point. // __iar_program_start(); } // This is the code that gets called when the processor receives a NMI. This // simply enters an infinite loop, preserving the system state for examination // by a debugger. static void NmiSR(void) { // // Enter an infinite loop. // while(1) { } } // This is the code that gets called when the processor receives a fault // interrupt. This simply enters an infinite loop, preserving the system state // for examination by a debugger. static void FaultISR(void) { // // Enter an infinite loop. // while(1) { } } // This is the code that gets called when the processor receives an unexpected // interrupt. This simply enters an infinite loop, preserving the system state // for examination by a debugger. static void IntDefaultHandler(void) { // // Go into an infinite loop. // while(1) { } }

Использование
@ ".intvec" Располагает таблицу __vector_table в начале секции, объявленной в файле линкера. Сам файл можно посмотреть тут:

Сама секция задается в начале ROM памяти. Адреса можно посмотреть (документ, в котором описана адресация флеш памяти STM32):

Комбинация директивы IAR и спецфункции IAR:
#pragma segment="CSTACK" __sfe("CSTACK") Записывает в начале флеша указатель на верхушку стека.

Саму таблицу заполняют адреса функций, реализующий вечный цикл. Исключение сделано только для интересующих нас функций:
extern void EXTI_Line0_IntHandler(void); extern void EXTI_Line6_IntHandler(void);
В функции, вызываемой при старте, просто производится переход к
extern void __iar_program_start(void); Это функция - main(). Сам символ можно переопределить, если возникнет желание:

Переходим к основному файлу
Для начала выпишем и переопределим все адреса и битовые поля, которые нам понадобятся.

Листинг

//Definitions for SCB_AIRCR register #define SCB_AIRCR (*(unsigned volatile long*)0xE000ED0C) //acces to SCB_AIRCR #define SCB_AIRCR_GROUP22 0x05FA0500 //change priority data //Definitions for RCC_AHB1_ENR register #define RCC_AHB1_ENR (*(unsigned volatile long *)(0x40023830)) //acces to RCC_AHB1ENR reg #define RCC_AHB1_ENR_GPIOA 0x1 //GPIOA bitfield #define RCC_AHB1_ENR_GPIOC 0x4 //GPIOC bitfield #define RCC_AHB1_ENR_GPIOD 0x8 //GPIOD bitfield //Definitions for RCC_APB2_ENR register #define RCC_APB2_ENR (*(unsigned volatile long *)(0x40023844)) //acces to RCC_APB2ENR reg #define RCC_APB2_ENR_SYSCFG 0x4000 //SYSCFG bitfield //Definitions for GPIO MODE registers #define GPIOA_MODER (*(unsigned volatile long*)(0x40020000)) //acces to GPIOA_MODER reg #define GPIOC_MODER (*(unsigned volatile long*)(0x40020800)) //acces to GPIOC_MODER reg #define GPIOD_MODER (*(unsigned volatile long*)(0x40020C00)) //acces to GPIOD_MODER reg //GPIO ODR register definition #define GPIOD_ODR (*(unsigned volatile long*)(0x40020C14)) //acces to GPIOD_MODER reg #define GPIO_ODR_13PIN 0x2000 #define GPIO_ODR_14PIN 0x4000 //Bitfields definitions #define GPIO_MODER_0BITS 0x3 //Pin 0 mode bits #define GPIO_MODER_0IN 0x0 //Pin 0 input mode #define GPIO_MODER_6BITS 0x300 //Pin 6 mode bits #define GPIO_MODER_6IN 0x000 //Pin 6 input mode #define GPIO_MODER_13BITS 0xC000000 //Pin 13 mode bits #define GPIO_MODER_13OUT 0x4000000 //Pin 13 output mode #define GPIO_MODER_14BITS 0x30000000 //Pin 14 mode bits #define GPIO_MODER_14OUT 0x10000000 //Pin 14 output mode //GPIOC_PUPDR register definition #define GPIOC_PUPDR (*(unsigned volatile long*)(0x4002080C)) //acces to GPIOC_PUPDR reg #define GPIOC_PUPDR_6BITS 0x3000 //PC6 bitfield #define GPIOC_PUPDR_6PU 0x1000 //PC6 pull-up enable //SYSCFG_EXTIx registers definitions #define SYSCFG_EXTICR1 (*(unsigned volatile long*)0x40013808) //SYSCFG_EXTICR1 acces #define SYSCFG_EXTICR1_0BITS 0xF //EXTI 0 bits #define SYSCFG_EXTICR1_0PA 0x0 //EXTI 0 - port A #define SYSCFG_EXTICR2 (*(unsigned volatile long*)0x4001380C) //SYSCFG_EXTICR2 acces #define SYSCFG_EXTICR2_6BITS 0xF00 //EXTI 6 bits #define SYSCFG_EXTICR2_6PC 0x200 //EXTI 6 - port C //EXTI definitions #define EXTI_IMR (*(unsigned volatile long*)0x40013C00) //EXTI_IMR reg acces #define EXTI_LINE0 0x1 //LINE 0 definition #define EXTI_LINE6 0x40 //LINE 6 definition #define EXTI_RTSR (*(unsigned volatile long*)0x40013C08) //EXTI_RTSR reg acces #define EXTI_FTSR (*(unsigned volatile long*)0x40013C0C) //EXTI_FTSR reg acces #define EXTI_PR (*(unsigned volatile long*)0x40013C14) //EXTI_PR reg acces //NVIC registers and bits definitions #define NVIC_ISER0_REG (*(unsigned volatile long*)0xE000E100) //NVIC_ISER0 reg acces #define NVIC_ISER0_6VECT 0x40 //vect 6 definition #define NVIC_ISER0_23VECT 0x800000 //vect 30 definition #define NVIC_IPR0_ADD (0xE000E400) #define NVIC_IPR23_REG (*(unsigned volatile char*)(NVIC_IPR0_ADD + 23)) #define NVIC_IPR6_REG (*(unsigned volatile char*)(NVIC_IPR0_ADD + 6))

Обратите внимание на то, что значения спецрегистров МК объявлены как volatile . Это необходимо, чтобы компилятор не пытался оптимизировать операции обращения к ним, поскольку это не просто участки памяти и их значения могут изменяться без участия ядра.

Настраиваем группирование приоритетов
В первую очередь стоит настроить группировку приоритетов прерываний: SCB_AIRCR = SCB_AIRCR_GROUP22; .Данное действие должно выполняться только один раз. В сложных проектах, использующих сторонние библиотеки стоит проверять данный факт. Изменение разбиения приоритетов на группы может привести к некорректной работе прошивки.
Включение тактирование используемой периферии
Напомню, что перед началом работы с периферийными блоками необходимо включить их тактирование:
//Enable SYSCFG , GPIO port A and D clocking RCC_AHB1_ENR |= RCC_AHB1_ENR_GPIOA|RCC_AHB1_ENR_GPIOC|RCC_AHB1_ENR_GPIOD; RCC_APB2_ENR |= RCC_APB2_ENR_SYSCFG;
Работать сразу с SYSCFG нельзя, нужно подождать несколько тактов. Но мы и не будем. Займемся инициализацией GPIO.
Инициализация GPIO
Светодиоды инициализируются так же как и в прошлый раз:
//LED3 and LED5 initialization GPIOD_MODER = (GPIOD_MODER & (~GPIO_MODER_13BITS)) | GPIO_MODER_13OUT; GPIOD_MODER = (GPIOD_MODER & (~GPIO_MODER_14BITS)) | GPIO_MODER_14OUT;
Кнопка PA0 и контакт PC7 инициализируются как входные:
//PA0 and PC6 pins initialization GPIOA_MODER = (GPIOA_MODER & (~GPIO_MODER_0BITS)) | GPIO_MODER_0IN; GPIOC_MODER = (GPIOC_MODER & (~GPIO_MODER_6BITS)) | GPIO_MODER_6IN;
Вот только для контакта PC6 необходимо включить подтяжку питания. Активация подтяжки производится с помощью регистра GPIOC_PUPDR:
//Enable PC6 pull-up GPIOC_PUPDR = (GPIOC_PUPDR & (~GPIOC_PUPDR_7BITS)) | GPIOC_PUPDR_6PU;
Настройка EXTI
И так, на нужно настроить следующие параметры - включить прерывания для линий 0 и 6, для линии 0 прерывание по растущему фронту, для линии 6 - прерывание по падающему фронту:
//Set up EXTI EXTI_RTSR |= EXTI_LINE0; EXTI_FTSR |= EXTI_LINE6; EXTI_IMR = EXTI_LINE0|EXTI_LINE6;
Осталось настроить пины каких портов подключены к линии EXTI (странное решение, например МК stellaris могут генерировать прерывание при любой комбинации пинов, у STM32 с этим сложнее):
//EXTI to port connection SYSCFG_EXTICR1 = (SYSCFG_EXTICR1&(~SYSCFG_EXTICR1_0BITS)) | SYSCFG_EXTICR1_0PA; SYSCFG_EXTICR2 = (SYSCFG_EXTICR2&(~SYSCFG_EXTICR2_6BITS)) | SYSCFG_EXTICR2_6PC;
Настройка NVIC
Осталось настроить приоритеты прерываний и маскировать их для инициации обработки. Обратите внимание, что регистры NVIC_IPR доступны для побайтового обращения, что значительно упрощает доступ только к необходимым байтам приоритетов отдельных векторов прерываний. Достаточно только сделать сдвиг на величину номера вектора прерывания (см. листинг определений). Еще раз напомним, что EXTI Line 0 имеет 6 номер в таблице векторов, а EXTI line 5_9 - номер 23. У STM32 значение имеют только старшие 4 бита приоритета:
//Set interrupts priority NVIC_IPR6_REG = 0xF0; NVIC_IPR23_REG = 0x0;
Для демонстрации приоритеты установлены различными.
Теперь можно включить прерывания:
//Enable interrupts NVIC_ISER0_REG |= NVIC_ISER0_6VECT | NVIC_ISER0_23VECT;
С этого момента нажатие на кнопку и закоротки PC6 и GND будет приводить к вызову функций обработчиков прерываний EXTI_Line0_IntHandler и EXTI_Line6_IntHandler соответственно.
Обработка прерываний
В функциях обработки прерываний в первую очередь необходимо очистить прерывание, после этого можно зажечь светодиоды. Для демонстрации приоритетов прерываний в один из обработчиков добавлен вечный цикл. Если приоритет прерывания с вечным циклом ниже приоритета второго - то оно не сможет быть вызвано. Иначе, оно сможет прервать первое. Я предлагаю вам самим попробовать различные знчения приоритетов прерываний и наглядно увидеть к чему это приводит (ВНИМАНИЕ - не забудьте про группы прерываний! ).
void EXTI_Line0_IntHandler(void) { //Clear interrupt EXTI_PR = EXTI_LINE0; //Turn on LED 3 GPIOD_ODR |= GPIO_ODR_13PIN; } void EXTI_Line6_IntHandler(void) { //Clear interrupt EXTI_PR = EXTI_LINE6; //Turn LED4 GPIOD_ODR |= GPIO_ODR_14PIN; while(1); }

Вместо заключения

На всякий случай приведу полный листинг получившейся программы.

Листинг

//Definitions for SCB_AIRCR register #define SCB_AIRCR (*(unsigned volatile long*)0xE000ED0C) //acces to SCB_AIRCR #define SCB_AIRCR_GROUP22 0x05FA0500 //change priority data //Definitions for RCC_AHB1_ENR register #define RCC_AHB1_ENR (*(unsigned volatile long *)(0x40023830)) //acces to RCC_AHB1ENR reg #define RCC_AHB1_ENR_GPIOA 0x1 //GPIOA bitfield #define RCC_AHB1_ENR_GPIOC 0x4 //GPIOC bitfield #define RCC_AHB1_ENR_GPIOD 0x8 //GPIOD bitfield //Definitions for RCC_APB2_ENR register #define RCC_APB2_ENR (*(unsigned volatile long *)(0x40023844)) //acces to RCC_APB2ENR reg #define RCC_APB2_ENR_SYSCFG 0x4000 //SYSCFG bitfield //Definitions for GPIO MODE registers #define GPIOA_MODER (*(unsigned volatile long*)(0x40020000)) //acces to GPIOA_MODER reg #define GPIOC_MODER (*(unsigned volatile long*)(0x40020800)) //acces to GPIOC_MODER reg #define GPIOD_MODER (*(unsigned volatile long*)(0x40020C00)) //acces to GPIOD_MODER reg //GPIO ODR register definition #define GPIOD_ODR (*(unsigned volatile long*)(0x40020C14)) //acces to GPIOD_MODER reg #define GPIO_ODR_13PIN 0x2000 #define GPIO_ODR_14PIN 0x4000 //Bitfields definitions #define GPIO_MODER_0BITS 0x3 //Pin 0 mode bits #define GPIO_MODER_0IN 0x0 //Pin 0 input mode #define GPIO_MODER_6BITS 0x300 //Pin 6 mode bits #define GPIO_MODER_6IN 0x000 //Pin 6 input mode #define GPIO_MODER_13BITS 0xC000000 //Pin 13 mode bits #define GPIO_MODER_13OUT 0x4000000 //Pin 13 output mode #define GPIO_MODER_14BITS 0x30000000 //Pin 14 mode bits #define GPIO_MODER_14OUT 0x10000000 //Pin 14 output mode //GPIOC_PUPDR register definition #define GPIOC_PUPDR (*(unsigned volatile long*)(0x4002080C)) //acces to GPIOC_PUPDR reg #define GPIOC_PUPDR_6BITS 0x3000 //PC6 bitfield #define GPIOC_PUPDR_6PU 0x1000 //PC6 pull-up enable //SYSCFG_EXTIx registers definitions #define SYSCFG_EXTICR1 (*(unsigned volatile long*)0x40013808) //SYSCFG_EXTICR1 acces #define SYSCFG_EXTICR1_0BITS 0xF //EXTI 0 bits #define SYSCFG_EXTICR1_0PA 0x0 //EXTI 0 - port A #define SYSCFG_EXTICR2 (*(unsigned volatile long*)0x4001380C) //SYSCFG_EXTICR2 acces #define SYSCFG_EXTICR2_6BITS 0xF00 //EXTI 6 bits #define SYSCFG_EXTICR2_6PC 0x200 //EXTI 6 - port C //EXTI definitions #define EXTI_IMR (*(unsigned volatile long*)0x40013C00) //EXTI_IMR reg acces #define EXTI_LINE0 0x1 //LINE 0 definition #define EXTI_LINE6 0x40 //LINE 6 definition #define EXTI_RTSR (*(unsigned volatile long*)0x40013C08) //EXTI_RTSR reg acces #define EXTI_FTSR (*(unsigned volatile long*)0x40013C0C) //EXTI_FTSR reg acces #define EXTI_PR (*(unsigned volatile long*)0x40013C14) //EXTI_PR reg acces //NVIC registers and bits definitions #define NVIC_ISER0_REG (*(unsigned volatile long*)0xE000E100) //NVIC_ISER0 reg acces #define NVIC_ISER0_6VECT 0x40 //vect 6 definition #define NVIC_ISER0_23VECT 0x800000 //vect 30 definition #define NVIC_IPR0_ADD (0xE000E400) #define NVIC_IPR23_REG (*(unsigned volatile char*)(NVIC_IPR0_ADD + 23)) #define NVIC_IPR6_REG (*(unsigned volatile char*)(NVIC_IPR0_ADD + 6)) void EXTI_Line0_IntHandler(void); void EXTI_Line6_IntHandler(void); void main() { //NVIC SCB_AIRCR = SCB_AIRCR_GROUP22; //Enable SYSCFG , GPIO port A,C and D clocking RCC_AHB1_ENR |= RCC_AHB1_ENR_GPIOA|RCC_AHB1_ENR_GPIOC|RCC_AHB1_ENR_GPIOD; RCC_APB2_ENR |= RCC_APB2_ENR_SYSCFG; //LED3 and LED5 initialization GPIOD_MODER = (GPIOD_MODER & (~GPIO_MODER_13BITS)) | GPIO_MODER_13OUT; GPIOD_MODER = (GPIOD_MODER & (~GPIO_MODER_14BITS)) | GPIO_MODER_14OUT; //PA0 and PC6 pins initialization GPIOA_MODER = (GPIOA_MODER & (~GPIO_MODER_0BITS)) | GPIO_MODER_0IN; GPIOC_MODER = (GPIOC_MODER & (~GPIO_MODER_6BITS)) | GPIO_MODER_6IN; //Enable PC7 pull-up GPIOC_PUPDR = (GPIOC_PUPDR & (~GPIOC_PUPDR_6BITS)) | GPIOC_PUPDR_6PU; //Set up EXTI EXTI_RTSR |= EXTI_LINE0; EXTI_FTSR |= EXTI_LINE6; EXTI_IMR = EXTI_LINE0|EXTI_LINE6; //EXTI to port connection SYSCFG_EXTICR1 = (SYSCFG_EXTICR1&(~SYSCFG_EXTICR1_0BITS)) | SYSCFG_EXTICR1_0PA; SYSCFG_EXTICR2 = (SYSCFG_EXTICR2&(~SYSCFG_EXTICR2_6BITS)) | SYSCFG_EXTICR2_6PC; //Set interrupts priority NVIC_IPR6_REG = 0xF0; NVIC_IPR23_REG = 0x00; //Enable interrupts NVIC_ISER0_REG |= NVIC_ISER0_6VECT | NVIC_ISER0_23VECT; while(1) { } } void EXTI_Line0_IntHandler(void) { //Clear interrupt EXTI_PR = EXTI_LINE0; //Turn on LED 3 GPIOD_ODR |= GPIO_ODR_13PIN; } void EXTI_Line6_IntHandler(void) { //Clear interrupt EXTI_PR = EXTI_LINE6; //Turn LED4 GPIOD_ODR |= GPIO_ODR_14PIN; while(1); }


Для проверки влияния приоритетов прерываний и приоритетов групп прерываний попробуйте менять приоритеты и наблюдать, что будет происходить (два бита - приоритет внутри группы, 2 бита - приоритет группы).

Теги:

  • STM32
  • Cortex-M
  • ARM
  • микроконтроллеры
Добавить метки

Помигаем светодиодом!

Поскольку микроконтроллеры STM32 - настоящие 32-битные ARM-ядра, сделать это будет непросто. Здесь всё сильно отличается от привычных методов в PIC или AVR, где было достаточно одной строкой настроить порт на выход, а второй строкой - вывести в него значение - но тем интереснее и гибче.

Архитектура STM32

Подробно архитектура микроконтроллеров расписана в статье, однако напомню основные положения, интересные нам сейчас.

Ядро тактируется кварцем, обычно через ФАПЧ. Это - тактовая частота ядра , или SYSCLK . На плате STM32VLDiscovery установлен кварц на 8 МГц, а ФАПЧ в большинстве случаев настраивается как умножитель на 3 - т.е. SYSCLK на плате STM32VLDiscovery обычно равен 24 МГц.

От ядра отходит шина AHB , имеющая свою тактовую частоту - ей можно установить некий прескалер относительно SYSCLK, однако можно оставить его равным единице. Эта шина подобна шине между процессором и северным мостом компьютера - точно так же она служит для связи ARM ядра и процессора периферии, а также на ней висит память и конечно, контроллер DMA.

К шине AHB подключены две периферийных шины - APB1 и APB2 . Они равнозначны, просто обслуживают разные контроллеры интерфейсов. Частоты обоих шин APB1 и APB2 можно задавать собственными прескалерами относительно AHB, но их тоже можно оставить равными единице. По умолчанию после запуска микроконтроллера вся периферия на шинах APB1 и APB2 отключена в целях экономии энергии.

Интересующие нас контроллеры портов ввода-вывода висят на шине APB2.

Модель периферии в STM32

Вся периферия микроконтроллеров STM32 настраивается по стандартной процедуре.

  1. Включение тактирования соответствующего контроллера - буквально, подача на него тактового сигнала от шины APB;
  2. Настройки, специфичные для конкретной периферии - что-то записываем в управляющие регистры;
  3. Выбор источников прерываний - каждый периферийный блок может генерировать прерывания по разным поводам. Можно выбрать конкретные «поводы»;
  4. Назначение обработчика прерываний;
  5. Запуск контроллера.

Если прерывания не нужны - шаги 3 и 4 можно пропустить.

Вот, к примеру, инициализация таймера (указаны шаги из последовательности):

/* 1 */ RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; /* 2 */ TIM6->PSC = 24000; TIM6->ARR = 1000; /* 3 */ TIM6->DIER |= TIM_DIER_UIE; /* 4 */ NVIC_EnableIRQ(TIM6_DAC_IRQn); /* 5 */ TIM6->CR1 |= TIM_CR1_CEN;

Контроллер портов ввода-вывода

Наконец-то подобрались к основной теме статьи.

Так устроена одна нога ввода-вывода микроконтроллера STM32F100:

Выглядит сложнее, чем в PIC или AVR Но на самом деле, ничего страшного.

На входе стоят защитные диоды, не дающие опустить потенциал ножки ниже земли или поднять его выше напряжения питания. Следом установлены управляемые подтягивающие резисторы - по желанию ножку можно подтянуть к земле или к питанию. Однако нужно помнить что эти подтяжки довольно слабые.

Вход

Рассмотрим «вход». Сигнал напрямую идёт в линию «Analog», и если ножка настроена как вход АЦП или компаратора - и если эти блоки есть на этой ножке - сигнал напрямую попадает в них. Для работы с цифровыми сигналами установлен триггер Шмитта (это тот, который с гистерезисом), и его выход попадает в регистр-защёлку входных данных - вот теперь состояние ножки можно считать в программе, читая этот регистр (кстати, он называется IDR - input data register). Для обеспечения работы не-GPIO-периферии, висящей на этой ножке как на входе - сделан отвод под именем «Alternate function input». В качестве этой периферии может выступать UART/USART, SPI, USB да и очень многие другие контроллеры.

Важно понимать, что все эти отводы одновременно включены и работают, просто к ним может быть ничего не подключено.

Выход

Теперь «выход». Цифровые данные, записанные в порт как в выход, лежат в регистре ODR - output data register. Он доступен как на запись, так и на чтение. Читая из ODR, вы не читаете состояние ножки как входа! Вы читаете то, что сами в него записали.

Здесь же - выход от не-GPIO-периферии, под названием «Alternate function output», и попадаем в Output driver. Режим работы выхода с точки зрения схемотехники настраивается именно здесь - можно сделать пуш-пулл выход (линия жёстко притягивается к земле или питанию), выход с открытым коллектором (притягиваем линию к питанию, а землю обеспечивает что-то внешнее, висящее на контакте) или вовсе отключить выход. После драйвера в линию входит аналоговый выход от ЦАП, компаратора или ОУ, и попадаем снова в подтягивающие резисторы и диоды.

Драйвер цифрового выхода имеет также контроль крутизны, или скорости нарастания напряжения. Можно установить максимальную крутизну, и получить возможность дёргать ногой с частотой 50 МГц - но так мы получим и сильные электромагнитные помехи из-за резких звенящих фронтов. Можно установить минимальную крутизну, с максимальной частотой «всего» 2 МГц - но и значительно уменьшить радиопомехи.

На картинке можно заметить ещё один регистр, «Bit set/reset registers». Дело в том, что можно писать напрямую в регистр ODR, а можно использовать регистры BRR/BSRR. На самом деле, это очень крутая фича, о которой я расскажу дальше.

Возможности

Сейчас всё стало похоже на хаос - неясно, как управлять всеми этими возможностями. Однако нет, контроллер порта отслеживает возможные режимы работы выхода, и исключает неверные комбинации - например, он не даст одновременно работать в одну выходную линию и драйверу цифрового выхода, и аналоговому выходу. Зато наличие такого количества настроек даёт обширные возможности.

Например, в более старших сериях можно настроить выход с открытым коллектором, и включить подтяжку к земле. Получается именно то, что нужно для шины 1-Wire. Правда, в серии STM32F1xx такой возможности нет, и нужно ставить внешний резистор подтяжки.

Атомарные операции

В старых микроконтроллерах часто возникала ситуация - если мы хотим изменить какие-то биты в порту (а на самом деле просто включить или выключить ножку) - нам приходилось читать весь регистр порта, устанавливать/сбрасывать в нём нужные биты и записывать обратно. Всё было хорошо до того момента, когда эту операцию посередине не прерывало прерывание. Если обработчик этого прерывания тоже что-то делал с этим же портом - возникала крайне трудноуловимая ошибка. С этим боролись разными средствами, например глобально запрещали прерывания на время обработки порта - но согласитесь, это какой-то костыльный вариант.

В STM32 эта проблема решена аппаратным путём - у вас есть регистры установки и сброса битов (BSRR и BRR), и здесь убиты сразу три зайца:

  1. не нужно читать порт для работы с ним
  2. для воздействия на конкретные пины нужно работать с конкретными битами, а не пытаться изменять весь порт
  3. эти операции атомарны - они проходят за один цикл, и их невозможно прервать посередине.

Подробнее про «конкретные биты» - каждый такт APB2 читаются регистры BSRR и BRR, и сразу же их содержимое применяется на регистр ODR, а сами эти регистры очищаются.Таким образом, если нужно установить 3 и 5 биты в порте - пишем в BSRR слово 10100, и всё успешно устанавливается.

Блокирование конфигурации

При желании, можно заблокировать конфигурацию любого пина от дальнейших изменений - любая попытка записи в регистр конфигурации окончится неуспехом. Это подойдёт для ответственных применений, где случайное переключение к примеру, выхода из режима open drain в push-pull выжжет всё подключенное к этому пину, или сам пин. Для включения блокирования предназначен регистр LCKR, только он снабжён защитой от случайной непреднамеренной записи - чтобы изменения вступили в силу, нужно подать специальную последовательность в бит LCKK.

Управляющие регистры

Всё управление контроллером GPIO сосредоточено в 32-битных регистрах GPIOx_RRR, где x - номер порта, а RRR - название регистра.

Младший конфигурационный регистр GPIOx_CRL

Настраивает первые 8 ножек, с номерами 0..7. У каждой ножки два параметра, MODE и CNF.

MODE отвечает за режим вход/выход и скорость нарастания сигнала.

00 - вход (режим по умолчанию)

01 - выход со скоростью 10 МГц

10 - выход со скоростью 2 МГц

11 - выход со скоростью 50 МГц

CNF отвечает за конфигурацию пина.

  • В режиме входа (MODE=00):

    00 - аналоговый режим

    01 - плавающий вход (дефолт)

    10 - вход с подтяжкой к земле или питанию

    11 - зарезервирован

  • В режиме выхода (MODE=01, 10 или 11):

    00 - выход GPIO Push-pull

    01 - выход GPIO Open drain

    10 - выход альтернативной функции Push-pull

    11 - выход альтернативной функции Open drain

Старший конфигурационный регистр GPIOx_CRH

Настраивает вторые 8 ножек, с номерами 8..15. Всё аналогично GPIOx_CRL.

Регистр входных данных GPIOx_IDR

Каждый бит IDRy содержит в себе состояние соответствующей ножки ввода-вывода. Доступен только для чтения.

Регистр входных данных GPIOx_ODR

Каждый бит ODRy содержит в себе состояние соответствующей ножки ввода-вывода. Можно записывать данные и они появятся на выходе порта, можно читать данные - читая предыдущее записанное значение.

Регистр атомарной установки/сброса битов выходных данных GPIOx_BSRR

Старшие 16 бит - для сброса соответствующих пинов в 0. 0 - ничего не делает, 1 - сбрасывает соответствующий бит. Младшие 16 бит - для установки битов в 1. Точно так же, запись «0» ничего не делает, запись «1» устанавливает соответствующий бит в 1.

Регистр атомарного сброса битов выходных данных GPIOx_BRR

Младшие 16 бит - для сброса соответствующих пинов. 0 - ничего не делает, 1 - сбрасывает соответствующий бит.

Регистр только для записи - он сбрасывается в ноль на каждом такте APB2.

Регистр блокирования конфигурации GPIOx_LCKR

Каждый бит LCKy блокирует соответствующие биты MODE/CNF регистров CRL/CRH от изменения, таким образом конфигурацию пина невозможно будет изменить вплоть до перезагрузки. Для активации блокирования необходимо записать блокирующую последовательность в бит LCKK: 1, 0, 1, читаем 0, читаем 1. Чтение бита LCKK сообщает текущий статус блокировки: 0 - блокировки нет, 1 - есть.

Работа в разных режимах

Режим входа

  • Отключается драйвер выхода
  • Резисторы подтяжек включаются по вашим настройкам, одно из трёх состояний - «вход, подтянутый к земле», «вход, подтянутый к питанию», или «плавающий вход»
  • Входной сигнал семплируется каждый такт шины APB2 и записывается в регистр IDR, и чтение этого регистра сообщает состояние ножки.

Режим выхода

  • Драйвер выхода включен, и действует так:

    В режиме «Push-Pull» работает как полумост, включая верхний транзистор в случае «1» и нижний в случае «0»,

    В режиме «Open drain» включает нижний транзистор в случае «0», а в случае «1» оставляет линию неподключенной (т.е. в третьем состоянии).

  • Входной триггер Шмитта включен
  • Отключаются резисторы подтяжек

Режим альтернативной функции (не-GPIO-периферия)

  • Выходной драйвер - в режиме Push-Pull (к примеру, так работает ножка TX модуля USART) или Open drain, в зависимости от требований контроллера
  • Выходной драйвер управляется сигналами периферии, а не регистром ODR
  • Входной триггер Шмитта включен
  • Резисторы подтяжки отключены
  • Выходной сигнал семплируется каждый такт шины APB2 и записывается в регистр IDR, и чтение этого регистра сообщает состояние ножки в режиме Open drain.
  • Чтение регистра ODR сообщает последнее записанное состояние в режиме Push-Pull.

Аналоговый режим

  • Выходной драйвер выключен
  • Триггер Шмитта полностью отключается, чтобы не влиять на напряжение на входе
  • Резисторы подтяжки отключены
  • В регистре IDR - постоянно 0.

Вся внутренняя аналоговая периферия имеет высокий входной импеданс, поэтому и сама ножка по отношению к остальной схеме будет иметь высокий входной импеданс.

Наконец-то включаем светодиод

Теперь мы знаем всё, чтобы включить этот светодиод! Пойдём с самого начала.

Нужно включить тактирование GPIO порта. Поскольку мы используем светодиод на плате Discovery, выберем зелёный - он подключен к порту PC9. То есть, необходимо включить тактирование GPIOC.

Теперь говорим про Push-pull выход. Это соответствует 00 в регистре CNF.

Ну вот, честно говоря и всё. Напоследок - листинг мигающего светодиода

#include "stm32f10x.h" int main(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; GPIOC->CRH &= !(GPIO_CRH_CNF9_0 | GPIO_CRH_CNF9_1); GPIOC->CRH |= GPIO_CRH_MODE9_1; uint32_t i, n=1000000; while(1) { GPIOC->BSRR |= GPIO_BSRR_BS9; i=0; while(i++BRR |= GPIO_BRR_BR9; i=0; while(i++

Библиотека itacone

И всё-таки ещё не всё. Ради упрощения всяческих настроек я делаю библиотеку itacone. На текущий момент в ней реализована работа с GPIO-пинами и пара функций общего применения - но работа продолжается.

Лучшие статьи по теме