EasySTM32 - Порты микроконтроллера. Сигнал какой частоты можно увидеть? Как это работает

Каждый порт STM32 состоит из 16 выводов, а каждый вывод может быть сконфигурирован одним из 8 способов.

Ниже изображена структура порта ввода-вывода.


Для того чтобы порт заработал его надо подключить к шине APB2 , установив соответствующий бит IOPxEN , в регистре разрешения тактирования периферийных блоков RCC_APB2ENR .
RCC->APB2ENR |= RCC_APB2ENR_IOPxEN; // Разрешить тактирование PORTx.
После включения все выводы находятся в состоянии плавающего входа , он же высокоимпедансный вход , он же Hi-Z , он же третье состояние .
  • Выходной драйвер выключен
  • Триггер Шмитта отключён
  • Подтягивающие резисторы отключены
  • В регистре IDR всегда “0”

В режиме входа

  • Выходной драйвер выключен
  • В зависимости от настройки, включаются резисторы подтяжки
  • Каждый такт шины APB2 данные с входа поступают в регистр IDR, считав этот регистр можно узнать состояние ножки

В режиме выхода

  • В режиме Open Drain при записи “0” открывается нижний транзистор, при записи “1” линия остаётся не подключённой
  • В режиме Push Pull при записи “1” открывается верхний транзистор, при записи “0” - нижний
  • Входной Триггер Шмитта включён
  • Резисторы подтяжки отключены

В режиме альтернативной функции

  • Драйвер включается в режиме Push Pull или Open Drain, в зависимости от конфигурации
  • Выходной драйвер управляется сигналами периферии, а не регистром ODR
  • Входной триггер Шмитта включён
  • Резисторы подтяжки отключены
  • По каждому такту шины APB2 данные c выхода передаются в регистр IDR, оттуда же их можно считать в режиме Open Drain
  • Чтение регистра ODR возвращает последнее записанное значение в режиме Push Pull

Из таблицы видно, что возможны два варианта конфигурации, в режиме альтернативной функции: Push Pull и Open Drain . Например, мы хотим, настроить в режим альтернативной функции ножку, отвечающую за приём данных по USART. Для этого в Reference Manual RM0008, начиная с 161 страницы, идут таблицы, в которых можно посмотреть как cконфигурировать вывод, для разной периферии.


Нам подойдет Input floating или Input pull-up .

Конфигурация выводов задаётся в регистрах GPIOx_CRL , GPIOx_CRH , в этих регистрах для конфигурации каждого вывода отведено 4 бита, MODE и CNF . В GPIOx_CRL конфигурируются выводы с 0 по 7, а в GPIOx_CRH с 8 по 15.



Если MODE = 00 , то вывод настроен на вход, конфигурация входа в таком случае задаётся в регистрах CNF . Если MODE не равен 00, в таком случае вывод настроен как выход, а значение MODE задаёт максимальную частоту, с которой может он переключаться.
//Полагаем что выводы после сброса в режиме плавающего входа //разрешаем тактирование порта A RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //вход с подтяжкой к + GPIOA->CRL &= ~GPIO_CRL_CNF0; GPIOA->CRL |= GPIO_CRL_CNF0_1; GPIOA->ODR |= GPIO_ODR_ODR0; //вход с подтяжкой к - GPIOA->CRL &= ~GPIO_CRL_CNF1; GPIOA->CRL |= GPIO_CRL_CNF1_1; GPIOA->ODR &= ~GPIO_ODR_ODR1; //аналоговый режим GPIOA->CRL &= ~GPIO_CRL_CNF2; //выход с открытым стоком 2MHz GPIOA->CRL &= ~GPIO_CRL_CNF3; GPIOA->CRL |= GPIO_CRL_CNF3_0; GPIOA->CRL |= GPIO_CRL_MODE3_1; //двухтактный выход 10MHz GPIOA->CRL &= ~GPIO_CRL_CNF4; GPIOA->CRL |= GPIO_CRL_MODE4_0; //альтернативная ф-ция, двухтактный выход, 50 MHz GPIOA->CRL &= ~GPIO_CRL_CNF5; GPIOA->CRL |= GPIO_CRL_CNF5_1; GPIOA->CRL |= GPIO_CRL_MODE5; //альтернативная ф-ция, выход с открытым стоком, 50 MHz GPIOA->CRL |= GPIO_CRL_CNF6; GPIOA->CRL |= GPIO_CRL_MODE6;
Считать состояние входа можно с помощью Port input data register или коротко GPIOx_IDR , где x – название порта, может быть от A до G. Считать состояние любого вывода можно из 16 младших бит, старшие 16 бит не используются.


//проверяем значение нулевого вывода порта А if (GPIOА->IDR & GPIO_IDR_IDR0)
Если порт настроен на выход, управлять его состоянием можно с помощью регистра Port output data register или GPIOx_ODR . Значение, которое мы запишем в этот регистр, появится на соответствующих выводах порта. Для установки состояния порта, выделены 16 младших бит, старшие 16 бит не используются.


//если вывод в режиме входа то активируется подтяжка к питанию GPIOA->ODR |= GPIO_ODR_ODR0; //или к земле GPIOA->ODR &= ~GPIO_ODR_ODR0; //если в режиме выхода, то на нём установится соответствующий лог.уровень //например так можно установить все выходы порта в 1 GPIOA->ODR = 0xFFFF;
В STM32 возможно атомарно управлять отдельными битами порта с помощью регистров GPIOx_BSRR (Port Bit Set/Reset Register) и GPIOx_BRR (Port Bit Reset Register).
Для установки отдельного бита порта вручную, надо считать значение порта, изменить нужный бит с помощью маски и результат вернуть обратно в GPIOx_ODR . Так как действий целых три, то возникшее между ними прерывание, может подпортить данные. С помощью описанных выше регистров, это делается в одно действие.
Для сброса бита надо в нулевой бит GPIOx_BRR записать единичку, при этом в нулевой бит GPIOx_ODR запишется 0, для этой операции выделены младшие 16 бит, старшие 16 бит не используются.


//сбросить нулевой бит порта А GPIOA->BRR = GPIO_BRR_BR0;
С GPIOx_BSRR всё чуть интереснее, младшие 16 бит отвечают за установку 1, старшие 16 бит за сброс в 0. Чтобы установить 1 в нулевой бит, надо в нулевой бит GPIOx_BSRR записать 1. Чтобы установить 0 в нулевой бит, надо в 16 бит установить 1.


//сбросить нулевой бит GPIOA->BSRR = GPIO_BSRR_BR0; //установить нулевой бит GPIOA->BSRR = GPIO_BSRR_BS0;
У STM32 есть возможность защитить конфигурацию порта от изменения, для этого выделен регистр GPIOx_LCKR . Младшие 16 бит используются для выбора вывода, который хотим заблокировать (выбор бита осуществляется установкой единицы), затем специальной последовательностью записей в 16 бит(LCKK ) осуществляется блокировка.


Последовательность следующая: записать в LCKK 1 , записать 0 ,записать 1, затем из регистра LCKR считать 0, считать 1. Последняя считанная единица говорит о том, что вывод заблокирован. Разблокировка вывода произойдёт только после перезагрузки контроллера.
#include "stm32f10x.h" uint32_t temp; int main(void) { //разрешаем тактирование порта RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //настраиваем как двухтактный выход GPIOA->CRL &= ~GPIO_CRL_CNF0; //с максимальной частотой 50MHz GPIOA->CRL |= GPIO_CRL_MODE0; //выбираем вывод который хотим залочить GPIOA->LCKR |= GPIO_LCKR_LCK0; //записываем 1 GPIOA->LCKR |= GPIO_LCKR_LCKK; //записываем 0 GPIOA->LCKR &= ~GPIO_LCKR_LCKK; //записываем 1 GPIOA->LCKR |= GPIO_LCKR_LCKK; //считываем 2 раза temp = GPIOA->LCKR; temp = GPIOA->LCKR; }
Для получения более подробной информации можно обратиться Reference Manual RM0008 , к разделу General-purpose and alternate-function I/Os (GPIOs and AFIOs) .

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

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

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

В целом, регистр - это особый вид памяти внутри микроконтроллера, который используется для управления процессором и периферийными устройствами. Каждый регистр в архитектуре 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:

Рабочая среда Keil переключится в режим отладки. Мы можем управлять ходом программы с помощью данных кнопок:

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

Если вы кликните по одному из пунктов данного меню, вы увидите адрес регистра и его краткое описание. Так же можно просмотреть описание к каждому отдельному параметру регистра:

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

До встречи в следующих статьях!

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

GPIO (general-purpose input/output,Интерфейс ввода/ вывода общего назначения)- Интерфейс связывающий микроконтроллер с внешними устройствами (Кнопками, светодиодами, датчиками и так далее).

Как и любой другой микроконтроллер, микроконтроллер STM 32, имеет в своем составе интерфейс ввода/вывода. Данный интерфейс позволяет управлять внешними устройствами путем передачи сигналов низкого и высокого уровня через контакты GPIO , а так же принимать данные с них, путем приема сигналов низкого или высокого уровня. Контакты GPIO , группируются в порты(GPIOA ,GPIOB ,GPIOC …). Каждый контакт может работать на прием или передачу данных.

Рассмотрим режимы работы контакта GPIO :

    Hi Z вход - В таком состоянии сопротивление входа стремится к бесконечности, и он ведет себя как отключенный от схемы.

    Вход с подтяжкой к питанию - Контакт работает на прием данных. Когда данных нет, контакт подтягивается к логической “1” (напряжению питания). Это делается, что бы избежать помех и искажения данных.

    Вход с подтяжкой к земле - Контакт работает на прием данных. Когда данных нет, контакт подтягивается к логическому “0” (земле). Это делается, что бы избежать помех и искажения данных.

    Аналоговый вход - Контакт работает в аналоговом режиме, что позволяет выводить на него сигнал ЦАП или считывать сигнал для АЦП.

    Выход с открытым коллектором .

    Двухтактный выход - Контакт работает на передачу данных. Контакт может быть установлен в логическую “1” либо в логический “ ”, соответствующим регистром.

    Альтернативный режим с подтяжкой .

    Альтернативный режим с открытым коллектором .

Альтернативные режимы пока рассматривать не будем, равно как выход с открытым коллектором и аналоговый вход. Рассмотрим подробнее режимы работы “Вход с подтяжкой к питанию/земле” и “Двухтактный выход”.

Как и любую другую периферию, перед использованием GPIO необходимо настроить. Так как периферия микроконтроллеров STM 32 очень богата и разнообразна, разработчики потрудились облегчить жизнь пользователям и избавить их от ручной правки регистров, путем создания библиотек HAL и SPL . Разумеется, никто не запрещает вам вручную править регистры. Хоть ручная правка регистров и сложна, однако позволяет лучше понять работу микроконтроллера и оптимизировать прошивку.

Но мы не будет сильно углубляться в дебри даташитов и регистров. Возьмем библиотеку SPL и с её помощью инициализируем порты ввода/вывода нашей платы STM 32 F 3 DISCOVERY .

Работать будем в среде Keil uVision 5.

Как создать свой первый проект в данной среде смотрите здесь:

Добавим следующий код:

#include "stm32f30x_gpio.h" #include "stm32f30x_rcc.h" void InitGPIO(void) { GPIO_InitTypeDef PORTE; GPIO_InitTypeDef PORTA; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOE,ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA,ENABLE); GPIO_StructInit (&PORTE); PORTE.GPIO_Mode = GPIO_Mode_OUT; PORTE.GPIO_OType = GPIO_OType_PP; PORTE.GPIO_Pin = GPIO_Pin_10; PORTE.GPIO_PuPd = GPIO_PuPd_DOWN; PORTE.GPIO_Speed = GPIO_Speed_50MHz; GPIO_StructInit (&PORTA); PORTA.GPIO_Mode = GPIO_Mode_IN; PORTA.GPIO_Pin = GPIO_Pin_0; PORTA.GPIO_PuPd = GPIO_PuPd_DOWN; PORTA.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init (GPIOE,&PORTE); GPIO_Init (GPIOA,&PORTA); }; int main(void) { InitGPIO(); while (1) { if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1) { GPIO_SetBits(GPIOE,GPIO_Pin_10); } else { GPIO_ResetBits(GPIOE,GPIO_Pin_10); } } }

#include "stm32f30x_gpio.h"

#include "stm32f30x_rcc.h"

void InitGPIO (void )

GPIO_InitTypeDef PORTE ;

GPIO_InitTypeDef PORTA ;

RCC_AHBPeriphClockCmd (RCC_AHBPeriph_GPIOE , ENABLE ) ;

RCC_AHBPeriphClockCmd (RCC_AHBPeriph_GPIOA , ENABLE ) ;

GPIO_StructInit (& PORTE ) ;

PORTE . GPIO_Mode = GPIO_Mode_OUT ;

PORTE . GPIO_OType = GPIO_OType_PP ;

PORTE . GPIO_Pin = GPIO_Pin_10 ;

PORTE . GPIO_PuPd = GPIO_PuPd_DOWN ;

PORTE . GPIO_Speed = GPIO_Speed_50MHz ;

GPIO_StructInit (& PORTA ) ;

PORTA . GPIO_Mode = GPIO_Mode_IN ;

PORTA . GPIO_Pin = GPIO_Pin_0 ;

PORTA . GPIO_PuPd = GPIO_PuPd_DOWN ;

PORTA . GPIO_Speed = GPIO_Speed_50MHz ;

GPIO_Init (GPIOE , & PORTE ) ;

GPIO_Init (GPIOA , & PORTA ) ;

int main (void )

InitGPIO () ;

while (1 )

if (GPIO_ReadInputDataBit (GPIOA , GPIO_Pin_0 ) == 1 )

GPIO_SetBits (GPIOE , GPIO_Pin_10 ) ;

else

GPIO_ResetBits (GPIOE , GPIO_Pin_10 ) ;

Итак, разберем подробнее, что же происходит в данном коде.

Точка входа - функция main , именно с неё начинается работа нашей прошивки. Сначала вызывается функция InitGPIO ,в которой мы настраиваем работу наших портов. Остановимся на ней подобробнее.

Вначале создаются две структуры GPIO_InitTypeDef инициализации портов GPIOE ,GPIOA , названные мною как PORTA ,PORTE . Данные структуры являются частью библиотеки SPL , и содержат в себе параметры работы соответствующих портов.

Затем, происходит включение тактирование портов GPIOA ,GPIOE командами RCC_AHBPeriphClockCmd. Следует помнить что, изначально тактирование периферии контроллера отключено, в целях снижения энергопотребления. Данной командой мы включаем или выключаем тактирование периферии шины AHB . Подробнее о тактировании поговорим в следующих уроках.

После этого мы заполняем поля структуры GPIO_InitTypeDef , для портов GPIOE ,GPIOA . Предварительно структуру можно проинициализировать (записать в неё стандартные значения) командой GPIO_StructInit. Рассмотрим поля структуры GPIO_InitTypeDef:

  • GPIO _Mode - Режим работы порта.
  • GPIO_OType - При настройке порта на выход, данным полем задается его тип (С открытым стоком или двухтактный).
  • GPIO_Pin - Данным полем мы выбираем, какие ножки порта настраивать.
  • GPIO_PuPd - Выбираем режим подтяжки к напряжению питания или к земле.
  • GPIO_Speed - Скорость работы порта.

Итак, поля заполнены, теперь инициализируем порты согласно тому, что прописано в соответствующей структуре командой GPIO_Init.

На этом функция InitGPIO заканчивается.

После вызова функции InitGPIO , наступает основной цикл программы while . В нем мы считываем входящее значение 0 ножки порта GPIOA (К которой подключена кнопка USER платы STM 32 F 3 DISCOVERY ). Делаем мы это командой GPIO_ReadInputDataBit, которая возвращает входное значение выбранной ножки порта. В соответствии с состоянием ножки 0 порта GPIOA , выставляем значение 10 ножки порта GPIOE командами GPIO_ResetBits и GPIO_S etBits. Команда GPIO _ResetBits устанавливает логический ноль на выбранной ножке порта, а команда GPIO _SetBits устанавливает логическую единицу на выбранной нами ножке.

Когда вы загрузите данную программу в плату STM 32 F 3 DISCOVERY , не забудьте нажать кнопку RESET , которая сбросит микроконтроллер. После этого нажимайте кнопку USER и смотрите за результатом.

Любое копирование, воспроизведение, цитирование материала, или его частей разрешено только с письменного согласия администрации MKPROG .RU . Незаконное копирование, цитирование, воспроизведение преследуется по закону!

Доброго времени суток! Сегодня мы займемся изучением 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’а для отладки.



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: