STM32. Регистры портов GPIO. STM32 Порты GPIO

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

Поскольку микроконтроллеры 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-пинами и пара функций общего применения - но работа продолжается.

Во втором уроке цикла, посвященного работе с микроконтроллерами STM32, речь пойдет о портах ввода/вывода.
Порты микроконтроллера позволяют взаимодействовать с внешними устройствами, начиная от светодиода и кнопки и заканчивая более сложными устройствами: дисплеями, GPS и GSM модемами и так далее. Также порты позволяют организовать связь с другими устройствами, например с компьютером.

General Purpose Input/Output (GPIO). GPIO основной и часто применяемый способ связи с внешней средой. Порты могут работать в двух режимах: вход (прием сигнала) и выход (передача сигнала). Работают они только с логическими уровнями 0 (низкий уровень) или 1 (высокий уровень).
Например, если подключить к порту в режиме выхода светодиод, то при подаче сигнала высокого уровня светодиод будет светиться, а при подаче низкого – потухнет.
Если включить вывод в режим входа и подключить к нему кнопку, то с помощью микроконтроллера можно отслеживать ее состояние: нажатое или отпущенное.
По сути GPIO самый простой и примитивный способ организации работы с внешними устройствами, но использование обработки прерываний и таймеров значительно расширяет возможности. Речь о них пойдет немного позже.

Решим первую практическую задачу: управление светодиодами и считывание состояние кнопки.
Следует отметить очень важный момент – порты микроконтроллера могут выдать ток не более 20 мА. Хотя выдать он их может, но один раз и ненадолго, до хлопка и сизого дыма;). Для подключения более мощных нагрузок следует использовать силовые ключи.

Итак, начнем. Для работы возьмем плату STM32F4 Discovery. На ней изначально установлена пользовательская кнопка, подключенная к порту PA0 и 4 светодиода, подключенные к портам PD12-PD15.

Схема подключение кнопки и светодиодов показаны на рисунке.

Резистор R1 номиналом 10кОм – «подтяжка к земле», позволяет избежать ситуации, когда порт не подключен ни к «0», ни к «1» - этого необходимо избегать, а резистор решает эту проблему. Такую подтяжку можно включить и программно, но лучше обезопасить себя так.

Резисторы R2-R5 330Ом ограничивают ток, протекающий через светодиоды. Их можно выбрать в диапазоне от 200Ом до 1кОм, все зависит от необходимой яркости.

Теперь перейдем к написанию программы. В качестве среды разработки я использую . Среда бесплатная и, на мой взгляд, удобная. Как начинать в ней работать рассказывать не буду – в интернете по ней достаточно информации, для прошивки использую STM32 ST-LINK Utility.
Для начала включаем тактирование порта A, к которому подключена кнопка:

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

Теперь нужно правильно сконфигурировать порт:

//Структура содержащая настройки порта GPIO_InitTypeDef GPIO_InitStructure; //задаем номер вывода, если кнопка подключена, например к 6 порту, то пишем GPIO_Pin_6 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //порт будет работать как цифровой вход GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;

Существует несколько вариантов режима работы порта:
GPIO_Mode_IN – цифровой вход;
GPIO_Mode_OUT – цифровой выход;
GPIO_Mode_AF – альтернативная функция (UART и т.д.);
GPIO_Mode_AN – аналоговый режим.

//включаем подтяжку к «земле» GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;

Возможны следующие режимы «подтяжки»:
GPIO_PuPd_NOPULL – без подтяжки, вывод «болтается в воздухе»
GPIO_PuPd_UP – подтяжка к 3,3В
GPIO_PuPd_DOWN – подтяжка к «земле»

//вызов функции инициализации GPIO_Init(GPIOA, &GPIO_InitStructure);

Теперь сконфигурируем выводы, к которым подключены светодиоды:

//Включаем тактирование порта D RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE); //Выбираем нужные выводы GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12| GPIO_Pin_13| GPIO_Pin_14| GPIO_Pin_15; //Включаем режим выхода GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //вызов функции инициализации GPIO_Init(GPIOD, &GPIO_InitStructure);

Вот и все, порты сконфигурированы. Теперь напишем обработку в основном цикле программы:

While(1) { //Если кнопка нажата, то… if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1) { GPIO_SetBits(GPIOD, GPIO_Pin_12); //Подаем «1» на PD12 delay(); //Функция задержки GPIO_SetBits(GPIOD, GPIO_Pin_13); //Подаем «1» на PD13 delay(); GPIO_SetBits(GPIOD, GPIO_Pin_14); //Подаем «1» на PD14 delay(); GPIO_SetBits(GPIOD, GPIO_Pin_15); //Подаем «1» на PD15 delay(); GPIO_ResetBits(GPIOD, GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15); //Сбрасываем все пины в «0» delay(); } }

Вот и все, программа готова. Полная версия в архиве с проектом. Работа платы показана на видео.

Порты ввода/вывода GPIO в STM32 имеют по 16 линий, каждая из которых может быть настроена необходимым образом. Поддерживаются функции цифрового ввода, цифрового вывода, входа внешнего прерывания, а также функции ввода/вывода других модулей микроконтроллера. Программирование STM32 для работы с GPIO основано на использовании регистров конфигурации, чтения, записи, защиты конфигурации и регистра битового доступа.

Регистры конфигурации порта.

Port configuration register low (GPIOx_CRL) (x=A..G)

Port configuration register high (GPIOx_CRH) (x=A..G)

Для программирования режимов работы портов ввода/вывода STM32, используются два 32 разрядных регистра для каждого GPIO. Они позволяют произвольно настроить режим работы любой отдельной линии. Регистр GPIOx_CRL отвечает за линии с номерами от 0 до 7, GPIOx_CRH – за линии 8-15. Для каждой из них в регистре имеется два двухразрядных поля CNFy и MODEy. Первое определяет тип работы линии, второе – направление обмена по линии. все биты доступны для чтения/записи.

Регистр GPIOx_CRL

Бит регистра

Поле

Линия ввода/вывода

Бит регистра

Поле

Линия ввода/ вывода

Регистр GPIOX_CRH

Бит регистра

Поле

Линия ввода/вывода

Бит регистра

Поле

Линия ввода/вывода

Поле MODEy может принимать следующие значения:

  • 00 – линия работает на ввод. Данное состояние устанавливается после сброса.
  • 01 – линия работает на выход, с максимальной частотой переключения 10 МГц
  • 10 – линия работает на выход, с максимальной частотой переключения 20 МГц
  • 11 – линия работает на выход, с максимальной частотой переключения 50 МГц

Поле CNFy зависит от направления передачи. При работе на вход (MODEy=0) доступны следующие состояния:

  • 00 – аналоговый вход.
  • 01 – вход в третьем состоянии. (Устанавливается после сброса).
  • 10 – вход с подтягивающим резистором
  • 11 – зарезервировано для будущих применений.

При работе на выход (MODEy>0) поле CNFy может иметь следующие состояния:

  • 00 – цифровой выход
  • 01 – цифровой выход с открытым стоком
  • 10 – цифровой выход, подключенный специализированным блокам
  • 11 – цифровой выход, подключенный специализированным блокам с открытым стоком

Регистр защиты от изменения настроек

Port configuration lock register (GPIOx_LCKR) (x=A..G)

Поле

Поле

Установить блокируемый бит в GPIOx_LCKRДля невозможности изменения настроек порта в микроконтроллерах STM32 используется регистр GPIOx_LCKR. Его младщие 15 бит отвечают за соответсвующие линии порта ввода/вывода. Бит 16, установленный в 1, разрешает блокировку изменения настроек. все биты доступны на чтение/запись. Для усложнения жизни пользователям ;-) , используется специальный алгоритм установки защиты. Если он применен, то следующее изменение конфигурации доступно только после сброса. Алгоритм установки защиты выглядит следующим образом:

  1. Установить бит 16 GPIOx_LCKR.
  2. Сбросить бит 16 GPIOx_LCKR.
  3. Установить бит 16 GPIOx_LCKR.
  4. Прочитать GPIOx_LCKR
  5. Повторно прочитать GPIOx_LCKR

Регистры установки состояния линий

В отличие от привычных 8-ми битных моделей, в STM32 имеется несколько регистров, отвечающих за состояние линий порта ввода вывода. Условно они разделены на две группы – регистры порта и регистры установки отдельных битов.

Выходной регистр порта ввода/вывода

Port output data register (GPIOx_ODR) (x=A..G)

Поле

Поле

Данный регистр имеет разрядность 32, но используются только младшие 16 бит. Биты с 16 по 31 не используются. При записи в GPIOx_ODR какого-либо значения, это значение устанавливается на выходных линиях соответствующего порта. Биты регистра доступны только для чтения/записи.

Входной регистр

Port input data register (GPIOx_IDR) (x=A..G)

Бит 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16
Поле Резерв
Бит 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 1
Поле IDR15 IDR14 IDR13 IDR12 IDR11 IDR10 IDR9 IDR8 IDR7 IDR6 IDR5 IDR4 IDR3 IDR2 IDR1 IDR0

Аналогично регистру выхода, регистр входа имеет толь 16 младших действующих бит из 32. Чтение GPIOx_IDR возвращает значение состояния всех линий порта. Биты регистра доступны только для чтения.

Регистр битовых операций

Port bit set/reset register (GPIOx_BSRR) (x=A..G)

Бит 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16
Поле BR15 BR14 BR13 BR12 BR11 BR10 BR9 BR8 BR7 BR6 BR5 BR4 BR3 BR2 BR1 BR0
Бит 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 1
Поле BS15 BS14 BS13 BS12 BS11 BS10 BS9 BS8 BS7 BS6 BS5 BS4 BS3 BS2 BS1 BS0

Данный регистр позволяет обращаться к конкретной линии ввода вывода микроконтроллера STM32. Запись единицы в один из старших разрядов сбрасывает выход линии, а запись единицы в младшие разряды устанавливает высокий уровень сигнала на соответствующей линии. Запись в регистр производится в формате слова, при этом нулевые биты никакого действия не оказывают. Биты регистра доступны только для записи.

Регистр сброса

Port bit reset register (GPIOx_BRR) (x=A..G)

Бит 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16
Поле Резерв
Бит 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 1
Поле BR15 BR14 BR13 BR12 BR11 BR10 BR9 BR8 BR7 BR6 BR5 BR4 BR3 BR2 BR1 BR0

Данный регистр производит сброс высокого уровня линии, установленной в регистре GPIOx_ODR. Задействованы только младшие 16 бит, доступных только для записи.

При возникновении, некоторого события контроллер прерываний автоматически прерывает выполнение основной программы, и вызывает соответствующую функцию обработки прерываний. После выхода из функции обработчика прерываний программа продолжает выполнение с того места, где произошло прерывание. Все происходит автоматически (при правильной настройке 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
  • микроконтроллеры
Добавить метки

Мы рассмотрели работу с битовыми операциями и двоичными числами, тем самым заложив основу для рассмотрения новой темы. В этом уроке мы с Вами рассмотрим очередной вопрос: что такое регистры и как с ними работать ?

Память и регистры

Одним из самых важных навыков необходимых при работе с микроконтроллерами является умение взаимодействовать с регистрами. Давайте для себя разберемся, что же это такое ?

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

Каждый из регистров имеет свой порядковый номер – адрес. Адрес регистра обозначается 32-битным числом представленным в шестнадцатеричной системе счисления. Путём записи по адресу регистра определённой комбинации единиц и нулей, которые обычно представлены в шестнадцатеричном виде, осуществляется настройка и управление тем или иным узлом в МК. Вспомним, что в программе для работы с битовыми операциями, мы могли представить в виде шестнадцатеричного числа произвольный набор единиц и нулей. В целом стоит отметить, что существует два вида регистров: регистры общего назначения и специальные регистры. Первые расположены внутри ядра МК, а вторые являются частью RAM-памяти.

Так же стоит отметить, что Reference Manual , который мы скачивали в первом уроке , это один большой справочник по регистрам, содержащимся в целевом микроконтроллере, а библиотека CMSIS позволяет нам оперировать символьными именами регистров вместо числовых адресов. Например, к регистру 0x40011018 мы можем обратиться просто, используя символьное имя GPIOC_BSSR . Конкретные примеры конфигурирования мы рассмотрим в ходе разбора нашей программы из .

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

  1. Названия регистра и описания его назначения
  2. Адреса регистра или смещением относительно базового адреса
  3. Значения по умолчанию после сброса
  4. Типа доступа к ячейкам регистра (чтение, запись, чтение/запись)
  5. Значения и описания параметров записываемых битов
Давайте рассмотрим пример работы с регистрами в конкретной ситуации, чтобы получить общее представление о принципах настройки микроконтроллера.

Разбор кода из первого занятия

Итак, давайте вспомним задачу, которую мы решили на используя готовый код примера: нам было необходимо написать программу, которая бы обеспечила попеременное включение двух светодиодов на плате Discovery (возможно и не двух, если у вас другая версия платы Discovery) с временным интервалом.

Давайте еще разок взглянем на код программы, которую мы использовали для того, чтобы заставить наш МК дрыгать двумя ногами на которых расположены наши светодиоды:

Код main.c

/* Заголовочный файл для нашего семейства микроконтроллеров*/ #include "stm32f0xx.h" /* Тело основной программы */ int main(void) { /* Включаем тактирование на порту GPIO */ RCC->AHBENR |= RCC_AHBENR_GPIOCEN; /* Настраиваем режим работы портов PC8 и PC9 в Output*/ GPIOC ->MODER = 0x50000; /* Настраиваем Output type в режим Push-Pull */ GPIOC->OTYPER = 0; /* Настраиваем скорость работы порта в Low */ GPIOC->OSPEEDR = 0; while(1) { /* Зажигаем светодиод PC8, гасим PC9 */ GPIOC->ODR = 0x100; for (int i=0; i<500000; i++){} // Искусственная задержка /* Зажигаем светодиод PC9, гасим PC8 */ GPIOC->ODR = 0x200; for (int i=0; i<500000; i++){} // Искусственная задержка } }


Первым делом, при работе с STM32, даже для такой простой задачи как включение и выключение светодиода нам необходимо предварительно ответить на ряд вопросов:
  1. Как настроить, нужные нам, пины порта GPIO для того чтобы можно было включить светодиод?
  2. Как включить и выключить светодиод?
Ответим на них по порядку.

Куда подключены наши светодиоды? К какому выводу микроконтроллера?

Для того, чтобы посмотреть где что находится на плате Discovery, а в частности, нужные нам светодиоды - нужно открыть Schematic-файл, либо тот который мы скачали с сайта ST , либо прямо из Keil:


Открыв Schematic мы увидим схему всего того, что есть на плате - схему ST-Link, обвязку всей периферии и многое другое. На текущий момент нас интересуют два светодиода, ищем их обозначение:


Как мы видим, наши светодиоды подключены к порту GPIOC на 8 и 9 пин.

Как включить тактирование на нужный порт GPIO?

В целом, любая работа с периферией в микроконтроллерах STM32 сводится к стандартной последовательности действий:
  1. Включение тактирования соответствующего периферийного модуля. Осуществляется это через регистр RCC путем подачи тактового сигнала напрямую с шины на которой находится данный модуль. По умолчанию тактирование всей периферии отключено для минимизации энергопотребления.
  2. Настройка через управляющие регистры, путем изменения параметров специфичных для конкретного периферийного устройства
  3. Непосредственный запуск и использование результатов работы модуля
То есть, для начала работы нам нужно запустить тактирование на порт GPIOC. Это делается напрямую через обращение к регистру RCC отвечающему за тактирование всего и вся и включению тактового сигнала с шины, к которой подключен наш порт GPIO.

Внимание! Вопрос касательно системы тактирования, её настройки и использования мы подробно рассмотрим в отдельной статье.

Найти к какой шине подключен наш порт GPIOC можно найти в Datasheet"е на наш МК в разделе Memory Mapping в Таблице 16. STM32F051xx peripheral register boundary addresses.


Как вы уже успели заметить, необходимая нам шина именуется как AHB2. Для того чтобы подробнее ознакомиться с регистром, в котором включается тактирование на нужный нам порт GPIO на шине AHB, надо перейти в соответствующий раздел в Reference Manual. По названию регистров мы можем определить тот, который нужен нам:


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


Смотрим на таблицу и видим нечто напоминающее опции включения тактирования на портах GPIO. Переходим к описанию и находим нужную нам опцию:


Соответственно если мы установим 19 бит в значение «1» то это обеспечит включение тактирования на порт I/O C – то есть на наш GPIOC. К тому же - нам нужно включить отдельно один бит из группы, не затрагивая остальные т.к. мы не должны мешать и изменять без надобности другие настройки.

Основываясь на материалах прошлого урока, мы знаем что для того чтобы выставить определенный бит нужно используя логическую операцию «ИЛИ» сложить текущее значение регистра с маской которая содержит те биты которые необходимо включить. Например, сложим значение регистра RCC->AHBENR по умолчанию, т.е. 0x14 и число 0x80000 тем самым включим тактирование GPIOC путем установки 19 бита:

Каким образом мы можем это сделать из программы? Всё достаточно просто. В данном случае у нас два варианта:

  1. Запись в регистр напрямую численного значения регистра напрямую через его адрес.
  2. Настройка с использованием библиотеки CMSIS
В записи значения в регистр напрямую нет особых проблем, но есть пара существенных недостатков. Во-первых, такой код становится не читабельным и во-вторых мы не можем сходу определить на какой регистр ссылается тот или иной адрес в памяти.

То есть, мы могли бы обращаться к адресам регистров напрямую по адресу и написать так:

IO uint32_t * register_address = (uint32_t *) 0x40021014U; // Адрес нашего регистра в памяти *(__IO uint32_t *)register_address |= 0x80000; // Включаем 19 бит с нашим параметром
Второй вариант мне кажется наиболее привлекательным, т.к. библиотека CMSIS организована таким способом, что регистру можно обращаться, используя только его название. Препроцессор в ходе обработки текста программы перед компиляцией подставит все цифровые значения адреса регистра автоматически. Давайте разберем этот вопрос чуть подробнее.

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

Наш код будет выглядеть следующим образом:

/* Заголовочный файл для нашего семейства микроконтроллеров*/ #include "stm32f0xx.h" /* Тело основной программы */ int main(void) { /* Включаем тактирование на порту GPIO */ RCC->AHBENR|=RCC_AHBENR_GPIOCEN; }
Давайте для ознакомления копнём вглубь библиотеки CMSIS.

Для того, чтобы быстро перейти к месту где объявлена та или иная константа или переменная в Keil реализована удобная функция. Кликаем правой кнопкой по необходимой нам константе, например, на RCC:


И мы переносимся в глубины библиотеки CMSIS, в которой увидим, что все регистры доступные для управления программным способом имеют вид TypeDef-структур, в том числе и наш RCC:


Провалившись подобным образом в RCC_TypeDef мы увидим структуру в которой описаны все поля нашего регистра:


Соответственно, мы можем спокойно обращаться к нужному нам регистру записью вида PERIPH_MODULE->REGISTER и присваивать ему определенное значение.

Помимо мнемонического обозначения регистров есть так же обозначения конкретных битов. Если мы провалимся к объявлению параметра RCC_AHBENR_GPIOCEN из нашей программы, то так же увидим объявление всех параметров:


Таким образом, используя библиотеку CMSIS у нас получается лаконичная читаемая запись нужного нам параметра в регистр, через установку которого мы запускаем тактирование на нужный нам порт:

/* Включаем тактирование на порту GPIO */ RCC->AHBENR|=RCC_AHBENR_GPIOCEN;
В качестве задания: определите используя возможности Keil, каким образом получился адрес регистра RCC->AHBENR как 0x40021014.

Как настроить нужные нам пины GPIO для того чтобы можно было включить светодиод?

Итак, мы знаем что нужные нам светодиоды подключены к порту GPIOC к пинам PC8 и PC9. Нам нужно настроить их в такой режим, чтобы загорался светодиод. Хотелось бы сразу же сделать оговорку, что порты GPIO мы рассмотрим подробнее в другой статье и тут мы сконцентрируемся именно на работе с регистрами.

Первым делом нам нужно перевести режим работы пинов PC8 и PC9 в режим Output. Остальные параметры порта можно оставить по умолчанию. Переходим в Reference Manual в раздел 9. General-purpose I/Os (GPIO) и открываем пункт отвечающий за режим работы пинов порта GPIO и видим что за этот параметр отвечает регистр MODER:


Судя по описанию, для установки пинов PC8 и PC9 в режим Output мы должны записать 01 в соответствующие поля регистра GPIOC.

Это можно сделать через прямую установку с помощью числовых значений:


Или через использование определений из библиотеки:

/* Включаем тактирование на порту GPIO */ GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER9_0;
После данной инструкции наши пины PC8 и PC9 перейдут в режим Output.

Как включить светодиод?

Если мы обратим внимание на список доступных регистров для управления портом GPIO то можем увидеть регистр ODR:


Каждый из соответствующих битов отвечает за один из пинов порта. Его структуру вы можете увидеть ниже:


Для того, чтобы обеспечить попеременную смену состояний светодиодов надо с определенным временным интервалом включать/выключать 8 и 9 биты. То есть попеременно присваивать регистру значение 0x100 и 0x200.

Сделать это мы можем через прямое присвоение значений регистру:

GPIOC->ODR = 0x100; // Зажигаем PC8, гасим PC9 GPIOC->ODR = 0x200; // Зажигаем PC9, гасим PC8
Можем через использование определений из библиотеки:

GPIOC->ODR = GPIO_ODR_8; // Зажигаем PC8, гасим PC9 GPIOC->ODR = GPIO_ODR_9; // Зажигаем PC9, гасим PC8
Но так как микроконтроллер работает очень быстро - мы не будем замечать смены состояний светодиодов и визуально будет казаться что они оба горят постоянно. Для того чтобы они действительно моргали попеременно мы внесем искусственную задержку в виде цикла который займет МК бесполезными вычислениями на некоторое время. Получится следующий код:

/* Зажигаем светодиод PC8, гасим PC9 */ GPIOC->ODR = GPIO_ODR_8; for (int i=0; i<500000; i++){} // Искусственная задержка /* Зажигаем светодиод PC9, гасим PC8 */ GPIOC->ODR = GPIO_ODR_9; for (int i=0; i<500000; i++){} // Искусственная задержка
На этом первоначальное знакомство с регистрами и методами работы с ними мы можем закончить.

Проверка результатов работы нашего кода

Небольшое приятное дополнение в конце статьи: в Keil имеется отличный Debug-инструмент с помощью которого мы можем пошагово выполнить нашу программу и просмотреть текущее состояние любого периферийного блока. Для этого после загрузки прошивки после компиляции мы можем нажать кнопку Start Debug Session: