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 настраивается по стандартной процедуре.
- Включение тактирования соответствующего контроллера - буквально, подача на него тактового сигнала от шины APB;
- Настройки, специфичные для конкретной периферии - что-то записываем в управляющие регистры;
- Выбор источников прерываний - каждый периферийный блок может генерировать прерывания по разным поводам. Можно выбрать конкретные «поводы»;
- Назначение обработчика прерываний;
- Запуск контроллера.
Если прерывания не нужны - шаги 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), и здесь убиты сразу три зайца:
- не нужно читать порт для работы с ним
- для воздействия на конкретные пины нужно работать с конкретными битами, а не пытаться изменять весь порт
- эти операции атомарны - они проходят за один цикл, и их невозможно прервать посередине.
Подробнее про «конкретные биты» - каждый такт 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++ И всё-таки ещё не всё. Ради упрощения всяческих настроек я делаю библиотеку itacone. На текущий момент в ней реализована работа с GPIO-пинами и пара функций общего применения - но работа продолжается. Во втором уроке цикла, посвященного работе с микроконтроллерами STM32, речь пойдет о портах ввода/вывода. General Purpose Input/Output (GPIO). GPIO основной и часто применяемый способ связи с внешней средой. Порты могут работать в двух режимах: вход (прием сигнала) и выход (передача сигнала). Работают они только с логическими уровнями 0 (низкий уровень) или 1 (высокий уровень). Решим первую практическую задачу: управление светодиодами и считывание состояние кнопки. Итак, начнем. Для работы возьмем плату STM32F4 Discovery. На ней изначально установлена пользовательская кнопка, подключенная к порту PA0 и 4 светодиода, подключенные к портам PD12-PD15. Схема подключение кнопки и светодиодов показаны на рисунке. Резистор R1 номиналом 10кОм – «подтяжка к земле», позволяет избежать ситуации, когда порт не подключен ни к «0», ни к «1» - этого необходимо избегать, а резистор решает эту проблему. Такую подтяжку можно включить и программно, но лучше обезопасить себя так. Резисторы R2-R5 330Ом ограничивают ток, протекающий через светодиоды. Их можно выбрать в диапазоне от 200Ом до 1кОм, все зависит от необходимой яркости. Теперь перейдем к написанию программы. В качестве среды разработки я использую . Среда бесплатная и, на мой взгляд, удобная. Как начинать в ней работать рассказывать не буду – в интернете по ней достаточно информации, для прошивки использую STM32 ST-LINK Utility. 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_InitStructure.GPIO_PuPd = 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. Первое определяет тип работы линии, второе – направление обмена по линии. все биты доступны для чтения/записи. Бит
регистра
Поле
Линия ввода/вывода
Бит регистра
Поле
Линия ввода/
вывода
Бит регистра
Поле
Линия ввода/вывода
Бит регистра
Поле
Линия ввода/вывода
Поле MODEy может принимать следующие значения:
Поле CNFy зависит от направления передачи. При работе на вход (MODEy=0) доступны следующие состояния: При работе на выход (MODEy>0) поле CNFy может иметь следующие состояния: Port configuration lock register (GPIOx_LCKR) (x=A..G) Поле
Поле
Установить блокируемый бит в GPIOx_LCKRДля невозможности изменения настроек порта в микроконтроллерах STM32 используется регистр GPIOx_LCKR. Его младщие 15 бит отвечают за соответсвующие линии порта ввода/вывода. Бит 16, установленный в 1, разрешает блокировку изменения настроек. все биты доступны на чтение/запись. Для усложнения жизни пользователям ;-)
, используется специальный алгоритм установки защиты. Если он применен, то следующее изменение конфигурации доступно только после сброса. Алгоритм установки защиты выглядит следующим образом: В отличие от привычных 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)
Аналогично регистру выхода, регистр входа имеет толь 16 младших действующих бит из 32. Чтение GPIOx_IDR возвращает значение состояния всех линий порта. Биты регистра доступны только для чтения. Port bit set/reset register (GPIOx_BSRR) (x=A..G)
Данный регистр позволяет обращаться к конкретной линии ввода вывода микроконтроллера STM32. Запись единицы в один из старших разрядов сбрасывает выход линии, а запись единицы в младшие разряды устанавливает высокий уровень сигнала на соответствующей линии. Запись в регистр производится в формате слова, при этом нулевые биты никакого действия не оказывают. Биты регистра доступны только для записи. Port bit reset register (GPIOx_BRR) (x=A..G)
Данный регистр производит сброс высокого уровня линии, установленной в регистре GPIOx_ODR. Задействованы только младшие 16 бит, доступных только для записи. При возникновении, некоторого события контроллер прерываний автоматически прерывает выполнение основной программы, и вызывает соответствующую функцию обработки прерываний. После выхода из функции обработчика прерываний программа продолжает выполнение с того места, где произошло прерывание. Все происходит автоматически (при правильной настройке NVIC, но об этом ниже). Из самого названия видно, что контроллер NVIC поддерживает вложенность прерываний и приоритеты. Каждому прерыванию при настройке NVIC присваивается свой приоритет. Если во время обработки низкоприоритетного прерывания возникает высокоприоритетное, то оно, в свою очередь, прервет обработчик низкоприоритетного прерывания. В стек перемещается регистр регистр статуса программы ( Program Status Register (PSR)
), счетчик программы (Program Counter (PC)
) и регистр связи (Link Register (LR)
). Описание регистров ядра приведено в Cortex-M4 Generic User Guide . Благодаря этому, запоминается состояние, в котором находилось ядро перед переходом в режим обработки прерываний. Также сохраняются регистры R0 - R3 и R12. Эти регистры используются в инструкциях для передачи параметров, поэтому, помещение в стек делает возможным их использование в функции обработки прерывания, а R12 часто выступает в роли рабочего регистра программы. По завершении обработки прерывания все действия выполнятся в обратном порядке: извлекается содержимое стека и, параллельно с этим, осуществляется выборка адреса возврата. С момента инициации прерывания до выполнения первой команды обработчика прерывний проходит 12 тактов, такое же время необходимо для возобновления основной программы после завершения обработки прерывания. 1. Приостановка низкоприоритетного прерывания
2. Непрерывная обработка прерываний
3. Запаздывание высокприоритетного прерывания
Значение приоритета прерывания задается в регистрах Interrupt Priority Registers
(см. Cortex-M4 Generic User Guide). При этом, часть бит отвечает за приоритет группы, в которой находится прерывание, а часть - за приоритет внутри группы. Как вы, наверно, заметили, в Cortex-M4 Generic User Guide сказано, что настройка приоритетов и группировки приоритетов зависят от конкретной реализации implementation defined
. Содержимое файла
// 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)
{
}
}
Использование Сама секция задается в начале ROM памяти. Адреса можно посмотреть (документ, в котором описана адресация флеш памяти STM32): Комбинация директивы IAR и спецфункции IAR: Саму таблицу заполняют адреса функций, реализующий вечный цикл. Исключение сделано только для интересующих нас функций: Листинг
//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
. Это необходимо, чтобы компилятор не пытался оптимизировать операции обращения к ним, поскольку это не просто участки памяти и их значения могут изменяться без участия ядра. Листинг
//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);
}
Теги:
Мы рассмотрели работу с битовыми операциями и двоичными числами, тем самым заложив основу для рассмотрения новой темы. В этом уроке мы с Вами рассмотрим очередной вопрос: что такое регистры и как с ними работать
? В целом, регистр - это особый вид памяти внутри микроконтроллера, который используется для управления процессором и периферийными устройствами. Каждый регистр в архитектуре ARM представляет собой ячейку памяти и имеет длину в 32 бита, где каждый бит можно представить в виде крошечного выключателя с помощью которого осуществляется управление тем или иным параметром микроконтроллера. Каждый из регистров имеет свой порядковый номер – адрес. Адрес регистра обозначается 32-битным числом представленным в шестнадцатеричной системе счисления. Путём записи по адресу регистра определённой комбинации единиц и нулей, которые обычно представлены в шестнадцатеричном виде, осуществляется настройка и управление тем или иным узлом в МК. Вспомним, что в программе для работы с битовыми операциями, мы могли представить в виде шестнадцатеричного числа произвольный набор единиц и нулей. В целом стоит отметить, что существует два вида регистров: регистры общего назначения и специальные регистры. Первые расположены внутри ядра МК, а вторые являются частью RAM-памяти. Так же стоит отметить, что Reference Manual
, который мы скачивали в первом уроке , это один большой справочник по регистрам, содержащимся в целевом микроконтроллере, а библиотека CMSIS позволяет нам оперировать символьными именами регистров вместо числовых адресов. Например, к регистру 0x40011018
мы можем обратиться просто, используя символьное имя GPIOC_BSSR
. Конкретные примеры конфигурирования мы рассмотрим в ходе разбора нашей программы из . Итак, обычно структура регистра описывается в виде небольшой таблицы с указанием: Давайте еще разок взглянем на код программы, которую мы использовали для того, чтобы заставить наш МК дрыгать двумя ногами на которых расположены наши светодиоды: Код 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++){} // Искусственная задержка
}
}
Внимание! Вопрос касательно системы тактирования, её настройки и использования мы подробно рассмотрим в отдельной статье.
Найти к какой шине подключен наш порт GPIOC можно найти в Datasheet"е на наш МК в разделе Memory Mapping в Таблице 16. STM32F051xx peripheral register boundary addresses. Основываясь на материалах прошлого урока, мы знаем что для того чтобы выставить определенный бит нужно используя логическую операцию «ИЛИ» сложить текущее значение регистра с маской которая содержит те биты которые необходимо включить. Например, сложим значение регистра RCC->AHBENR по умолчанию, т.е. 0x14 и число 0x80000 тем самым включим тактирование GPIOC путем установки 19 бита: Каким образом мы можем это сделать из программы? Всё достаточно просто. В данном случае у нас два варианта: То есть, мы могли бы обращаться к адресам регистров напрямую по адресу и написать так: IO uint32_t * register_address = (uint32_t *) 0x40021014U; // Адрес нашего регистра в памяти
*(__IO uint32_t *)register_address |= 0x80000; // Включаем 19 бит с нашим параметром
Предлагаю открыть наш проект, который мы сделали в первом занятии, или скачайте предварительно подготовленый отсюда и удалите все содержимое программы оставив только подключенный заголовочный файл, функцию main() и инструкцию для включения тактирования (она нам понадобится для подробного разбора кода). Наш код будет выглядеть следующим образом: /* Заголовочный файл для нашего семейства микроконтроллеров*/
#include "stm32f0xx.h"
/* Тело основной программы */
int main(void)
{
/* Включаем тактирование на порту GPIO */
RCC->AHBENR|=RCC_AHBENR_GPIOCEN;
}
Для того, чтобы быстро перейти к месту где объявлена та или иная константа или переменная в Keil реализована удобная функция. Кликаем правой кнопкой по необходимой нам константе, например, на RCC: Помимо мнемонического обозначения регистров есть так же обозначения конкретных битов. Если мы провалимся к объявлению параметра RCC_AHBENR_GPIOCEN
из нашей программы, то так же увидим объявление всех параметров: /* Включаем тактирование на порту GPIO */
RCC->AHBENR|=RCC_AHBENR_GPIOCEN;
Первым делом нам нужно перевести режим работы пинов PC8 и PC9 в режим Output. Остальные параметры порта можно оставить по умолчанию. Переходим в Reference Manual в раздел 9. General-purpose I/Os (GPIO)
и открываем пункт отвечающий за режим работы пинов порта GPIO и видим что за этот параметр отвечает регистр MODER: Это можно сделать через прямую установку с помощью числовых значений: /* Включаем тактирование на порту GPIO */
GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER9_0;
Сделать это мы можем через прямое присвоение значений регистру: 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++){} // Искусственная задержка
Библиотека itacone
Порты микроконтроллера позволяют взаимодействовать с внешними устройствами, начиная от светодиода и кнопки и заканчивая более сложными устройствами: дисплеями, GPS и GSM модемами и так далее. Также порты позволяют организовать связь с другими устройствами, например с компьютером.
Например, если подключить к порту в режиме выхода светодиод, то при подаче сигнала высокого уровня светодиод будет светиться, а при подаче низкого – потухнет.
Если включить вывод в режим входа и подключить к нему кнопку, то с помощью микроконтроллера можно отслеживать ее состояние: нажатое или отпущенное.
По сути GPIO самый простой и примитивный способ организации работы с внешними устройствами, но использование обработки прерываний и таймеров значительно расширяет возможности. Речь о них пойдет немного позже.
Следует отметить очень важный момент – порты микроконтроллера могут выдать ток не более 20 мА. Хотя выдать он их может, но один раз и ненадолго, до хлопка и сизого дыма;). Для подключения более мощных нагрузок следует использовать силовые ключи.
Для начала включаем тактирование порта A, к которому подключена кнопка:
GPIO_Mode_IN – цифровой вход;
GPIO_Mode_OUT – цифровой выход;
GPIO_Mode_AF – альтернативная функция (UART и т.д.);
GPIO_Mode_AN – аналоговый режим.
GPIO_PuPd_NOPULL – без подтяжки, вывод «болтается в воздухе»
GPIO_PuPd_UP – подтяжка к 3,3В
GPIO_PuPd_DOWN – подтяжка к «земле»Регистры конфигурации порта.
Регистр GPIOx_CRL
Регистр GPIOX_CRH
Регистр защиты от изменения настроек
Регистры установки состояния линий
Выходной регистр порта ввода/вывода
Входной регистр
Бит
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
Регистр битовых операций
Бит
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
Регистр сброса
Бит
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
Как это работает?
Данный пост не претендует на абсолютную полноту, я советую изучить раздел прерываний в Cortex™-M3 Technical Reference Manual . Поскольку эта часть ядра не претерпела изменений, ее описание дано в первой ревизии r1p1 на ядро Cortex-M3.Вход в прерывание и выход из него
При инициации прерывания NVIC переключает ядро в режим обработки прерывания. После перехода в режим обработки прерывания регистры ядра помещаются в стек. Непосредственно во время записи значения регистров в стек осуществляется выборка начального адреса функции обработки прерывания.Вложенность прерываний
Как было сказано выше NVIC поддерживает прерывания с различными приоритетами, которые могут прерывать друг друга. При этом, могут возникнуть различные ситуации, обработка которых по разному оптимизирована.
В этой ситуации, обработка низкоприоритетного прерывания прекращается. Следующие 12 циклов выполняется сохранение в стек нового набора данных и запускается обработка высокоприоритетного прерывания. После его обработки, содержимое стека автоматически извлекается и возобновляется обработка низкоприоритетного прерывания.
Больших отличий от прерывания основной программы не наблюдается.
Эта ситуация может возникнуть в двух случаях: если два прерывания имеют одинаковый приоритет и возникают одновременно, если низкоприоритетное прерывание возникает во время обработки высокоприоритетного.
В этом случае, промежуточные операции над стеком не производятся. Происходит только загрузка адреса обработчика низкоприоритетного прерывания и переход к его выполнению. Отказ от операций над стеком экономит 6 тактов. Переход к следующему прерыванию происходит не за 12 тактов, а всего за 6.
Ситуация возникает, если высокоприоритетное прерывание происходит во перехода к обработке низкоприоритетного (за те самые 12 тактов). В этом случае переход к высокоприоритетному прерыванию будет происходить не менее 6 тактов с момента его возникновения (время необходимое для загрузки адреса обработчика прерывания и перехода к нему). Возврат в низкоприоритетное уже описан выше.Приоритеты прерываний
Помимо простой установки приоритета прерываний, NVIC реализует возможность группировки приоритетов.
Прерывания в группе с более высоким приоритетом могут прерывать обработчики прерываний группы с более низким приоритетом. прерывания из одной группы, но с разным приоритетом внутри группы не могут прерывать друг друга. Приоритет внутри группы определяет только порядок вызова обработчика, когда были активизированы оба события.
Настройка распределение бит на приоритет группы или приоритет внутри группы осуществляется с помощью регистра Application Interrupt and Reset Control Register
(ВНИМАТЕЛЬНО!!! см. Cortex-M4 Generic User Guide).
А вот дальше не очень приятная вещь. В к МК 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.
@ ".intvec"
Располагает таблицу __vector_table в начале секции, объявленной в файле линкера. Сам файл можно посмотреть тут:
#pragma segment="CSTACK"
__sfe("CSTACK")
Записывает в начале флеша указатель на верхушку стека.
extern void EXTI_Line0_IntHandler(void);
extern void EXTI_Line6_IntHandler(void);
В функции, вызываемой при старте, просто производится переход к
extern void __iar_program_start(void);
Это функция - main(). Сам символ можно переопределить, если возникнет желание:Переходим к основному файлу
Для начала выпишем и переопределим все адреса и битовые поля, которые нам понадобятся.Настраиваем группирование приоритетов
В первую очередь стоит настроить группировку приоритетов прерываний: 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);
}
Вместо заключения
На всякий случай приведу полный листинг получившейся программы.
Для проверки влияния приоритетов прерываний и приоритетов групп прерываний попробуйте менять приоритеты и наблюдать, что будет происходить (два бита - приоритет внутри группы, 2 бита - приоритет группы).
Добавить метки
Память и регистры
Одним из самых важных навыков необходимых при работе с микроконтроллерами является умение взаимодействовать с регистрами. Давайте для себя разберемся, что же это такое
?
Давайте рассмотрим пример работы с регистрами в конкретной ситуации, чтобы получить общее представление о принципах настройки микроконтроллера.Разбор кода из первого занятия
Итак, давайте вспомним задачу, которую мы решили на используя готовый код примера: нам было необходимо написать программу, которая бы обеспечила попеременное включение двух светодиодов на плате Discovery (возможно и не двух, если у вас другая версия платы Discovery) с временным интервалом.
Первым делом, при работе с STM32, даже для такой простой задачи как включение и выключение светодиода нам необходимо предварительно ответить на ряд вопросов:
Ответим на них по порядку. Куда подключены наши светодиоды? К какому выводу микроконтроллера?
Для того, чтобы посмотреть где что находится на плате Discovery, а в частности, нужные нам светодиоды - нужно открыть Schematic-файл, либо тот который мы скачали с сайта ST , либо прямо из Keil:
Открыв Schematic мы увидим схему всего того, что есть на плате - схему ST-Link, обвязку всей периферии и многое другое. На текущий момент нас интересуют два светодиода, ищем их обозначение:
Как мы видим, наши светодиоды подключены к порту GPIOC на 8 и 9 пин. Как включить тактирование на нужный порт GPIO?
В целом, любая работа с периферией в микроконтроллерах STM32 сводится к стандартной последовательности действий:
То есть, для начала работы нам нужно запустить тактирование на порт GPIOC. Это делается напрямую через обращение к регистру RCC отвечающему за тактирование всего и вся и включению тактового сигнала с шины, к которой подключен наш порт GPIO.
Как вы уже успели заметить, необходимая нам шина именуется как AHB2. Для того чтобы подробнее ознакомиться с регистром, в котором включается тактирование на нужный нам порт GPIO на шине AHB, надо перейти в соответствующий раздел в Reference Manual. По названию регистров мы можем определить тот, который нужен нам:
Переходим в этот пункт, и мы видим наш 32-битный регистр, его адрес смещения, значение по умолчанию, способ доступа к регистру и перечисление того, за что отвечает каждый бит в регистре.
Смотрим на таблицу и видим нечто напоминающее опции включения тактирования на портах GPIO. Переходим к описанию и находим нужную нам опцию:
Соответственно если мы установим 19 бит в значение «1» то это обеспечит включение тактирования на порт I/O C – то есть на наш GPIOC. К тому же - нам нужно включить отдельно один бит из группы, не затрагивая остальные т.к. мы не должны мешать и изменять без надобности другие настройки.
В записи значения в регистр напрямую нет особых проблем, но есть пара существенных недостатков. Во-первых, такой код становится не читабельным и во-вторых мы не можем сходу определить на какой регистр ссылается тот или иной адрес в памяти.
Второй вариант мне кажется наиболее привлекательным, т.к. библиотека CMSIS организована таким способом, что регистру можно обращаться, используя только его название. Препроцессор в ходе обработки текста программы перед компиляцией подставит все цифровые значения адреса регистра автоматически. Давайте разберем этот вопрос чуть подробнее.
Давайте для ознакомления копнём вглубь библиотеки CMSIS.
И мы переносимся в глубины библиотеки CMSIS, в которой увидим, что все регистры доступные для управления программным способом имеют вид TypeDef-структур, в том числе и наш RCC:
Провалившись подобным образом в RCC_TypeDef мы увидим структуру в которой описаны все поля нашего регистра:
Соответственно, мы можем спокойно обращаться к нужному нам регистру записью вида PERIPH_MODULE->REGISTER
и присваивать ему определенное значение.
Таким образом, используя библиотеку CMSIS у нас получается лаконичная читаемая запись нужного нам параметра в регистр, через установку которого мы запускаем тактирование на нужный нам порт:
В качестве задания:
определите используя возможности Keil, каким образом получился адрес регистра RCC->AHBENR как 0x40021014.Как настроить нужные нам пины GPIO для того чтобы можно было включить светодиод?
Итак, мы знаем что нужные нам светодиоды подключены к порту GPIOC к пинам PC8 и PC9. Нам нужно настроить их в такой режим, чтобы загорался светодиод. Хотелось бы сразу же сделать оговорку, что порты GPIO мы рассмотрим подробнее в другой статье и тут мы сконцентрируемся именно на работе с регистрами.
Судя по описанию, для установки пинов PC8 и PC9 в режим Output мы должны записать 01 в соответствующие поля регистра GPIOC.
Или через использование определений из библиотеки:
После данной инструкции наши пины PC8 и PC9 перейдут в режим Output.Как включить светодиод?
Если мы обратим внимание на список доступных регистров для управления портом GPIO то можем увидеть регистр ODR:
Каждый из соответствующих битов отвечает за один из пинов порта. Его структуру вы можете увидеть ниже:
Для того, чтобы обеспечить попеременную смену состояний светодиодов надо с определенным временным интервалом включать/выключать 8 и 9 биты. То есть попеременно присваивать регистру значение 0x100 и 0x200.
Можем через использование определений из библиотеки:
Но так как микроконтроллер работает очень быстро - мы не будем замечать смены состояний светодиодов и визуально будет казаться что они оба горят постоянно. Для того чтобы они действительно моргали попеременно мы внесем искусственную задержку в виде цикла который займет МК бесполезными вычислениями на некоторое время. Получится следующий код:
На этом первоначальное знакомство с регистрами и методами работы с ними мы можем закончить.Проверка результатов работы нашего кода
Небольшое приятное дополнение в конце статьи: в Keil имеется отличный Debug-инструмент с помощью которого мы можем пошагово выполнить нашу программу и просмотреть текущее состояние любого периферийного блока. Для этого после загрузки прошивки после компиляции мы можем нажать кнопку Start Debug Session: