Научимся управлять портами через прямое обращение к регистрам.
Предыдущий урок Список уроков Следующий урок
В уроке 4 я писал, что главное преимущество прямого обращения к регистрам это скорость выполнения операций. Как удачный вариант я отметил способ, при котором начальная конфигурация формируется с помощью STM32CubeMX и библиотеки HAL, а данные в ходе выполнения программы передаются через регистры CMSIS. Начальные установки редко приходится менять при выполнении программы.
Но с портами ввода-вывода все не так. Мало того, что часто приходится выводить на них высокочастотные сигналы, но и бывают случаи, когда необходимо оперативно изменять режимы работы. Например, разворачивать порт на вход или выход при двунаправленных сигналах.
Поэтому для портов ввода-вывода управление через прямое обращение к регистрам довольно распространенный способ. Но ничего сложного в этом нет. Просто надо понять принцип.
Для примера мы собираемся установить режимы работы:
- для вывода PB12 – вход с подтягивающим резистором к шине питания;
- для вывода PB13 – активный выход.
Позже подключим к этим выводам кнопку и светодиод, напишем управляющую программу.
Для конфигурации порта GPIOB необходимо выполнить определенную последовательность действий.
Прежде всего, разрешить работу порта в регистре Разрешения тактирования периферийных устройств шины APB2 (APB2 peripheral clock enable register) RCC_APB2ENR. За порт B отвечает бит IOPBEN. Установим его в 1.
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
В принципе, эту операцию уже сделал за нас конфигуратор STM32CubeMX, когда мы выбрали активными выводы порта B.
Биты конфигурации и режима для вывода PB12 расположены в регистре:
GPIOB_CRH, биты CNF12[1:0] и MODE12[1:0].
Смотрим по таблицам режима и конфигурации портов. Надо установить состояние:
CNF12[1:0], MODE12[1:0] = 10, 00 (режим вход).
Теперь, как установить это состояние в регистрах конфигурации. Я на практике еще раз продемонстрирую то, о чем рассказывал в уроке 4.
В IDE открываем файл stm32f103xb.h. Он содержит имена регистров микроконтроллера.
Структура для регистров портов в нем описана так.
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
В ней все наши регистры для управления портами ввода-вывода, о которых мы говорили в предыдущем уроке.
Мы должны установить в регистре GPIOB_CRH, биты 19-16 в состояние 1000. Можно просто загрузить в регистр данное.
GPIOB->CRH = 0x00080000;
В этом случае мы сбросим все остальные биты. Что-то перестанет работать. Но если в загружаемом числе сформировать все биты этого регистра сразу, то способ вполне допустимый. Более того это самый быстрый и короткий вариант.
Если мы хотим затронуть только нужные биты, то можно сделать так.
GPIOB->CRH &= 0xfff0ffff; // сбрасываем биты 16-19
GPIOB->CRH |= 0x00080000; // устанавливаем биты
Вариант быстрый, но запутанный. Надо вычислять расположение битов в слове, большая вероятность ошибки.
Можно несколько упростить, используя операцию сдвига.
GPIOB->CRH &= ~ ( 0b1111 << 16 ); // сбрасываем биты 16-19
GPIOB->CRH |= 0b1000 << 16; // устанавливаем биты
Все вычисления с константами компилятор сделает на этапе трансляции и в правой части операций получатся точно такие числа, как и в предыдущем варианте.
В файле stm32f103xb.h есть имена и для битов регистра GPIOx_CRH. Вот, что касается 12го разряда.
/******************* Bit definition for GPIO_CRH register *******************/
#define GPIO_CRH_MODE12_Pos (16U)
#define GPIO_CRH_MODE12_Msk (0x3U << GPIO_CRH_MODE12_Pos) /*!< 0x00030000 */
#define GPIO_CRH_MODE12 GPIO_CRH_MODE12_Msk /*!< MODE12[1:0] bits (Port x mode bits, pin 12) */
#define GPIO_CRH_MODE12_0 (0x1U << GPIO_CRH_MODE12_Pos) /*!< 0x00010000 */
#define GPIO_CRH_MODE12_1 (0x2U << GPIO_CRH_MODE12_Pos) /*!< 0x00020000 */
#define GPIO_CRH_CNF12_Pos (18U)
#define GPIO_CRH_CNF12_Msk (0x3U << GPIO_CRH_CNF12_Pos) /*!< 0x000C0000 */
#define GPIO_CRH_CNF12 GPIO_CRH_CNF12_Msk /*!< CNF12[1:0] bits (Port x configuration bits, pin 12) */
#define GPIO_CRH_CNF12_0 (0x1U << GPIO_CRH_CNF12_Pos) /*!< 0x00040000 */
#define GPIO_CRH_CNF12_1 (0x2U << GPIO_CRH_CNF12_Pos) /*!< 0x00080000 */
Установка регистра конфигурации с использованием имен битов выглядит так.
GPIOB->CRH &= ~ (GPIO_CRH_CNF12 | GPIO_CRH_MODE12); // сбрасываем биты полей CNF и MODE
GPIOB->CRH |= (0b10 << GPIO_CRH_CNF12_Pos) | (0b00 << GPIO_CRH_MODE12_Pos) ; // устанавливаем биты
Это предпочтительный, общепринятый способ доступа к отдельным полям регистров.
Установим таким способом вывод PB13 в режим выхода. Для этого надо в поля CNF13[1:0], MODE13[1:0] установить = 00, 10 (режим выход).
GPIOB->CRH &= ~ (GPIO_CRH_CNF13 | GPIO_CRH_MODE13); // сбрасываем биты полей CNF и MODE
GPIOB->CRH |= (0b00 << GPIO_CRH_CNF13_Pos) | (0b10 << GPIO_CRH_MODE13_Pos) ; // устанавливаем биты
Теперь попробуем считать и установить состояния выводов. Давайте подключим к выводу PB12 кнопку, а к выводу PB13 светодиод.
И напишем программу, которая управляет светодиодом с помощью кнопки. Кнопка нажата – светодиод светится, отжата – светодиод погашен.
Считать состояние вывода можно через регистр ввода данных.
В структуре GPIO_TypeDef это регистр IDR. Проверяем 12й бит.
if( (GPIOB->IDR & (1 << 12)) == 0 ) {
// бит 12 = 0, кнопка нажата
}
else {
// бит 12 = 1, кнопка отжата
}
Для бита 12 регистра IDR есть имя, но оно не сильно улучшает читаемость программы.
/*!<****************** Bit definition for GPIO_IDR register *******************/
#define GPIO_IDR_IDR12_Pos (12U)
#define GPIO_IDR_IDR12_Msk (0x1U << GPIO_IDR_IDR12_Pos) /*!< 0x00001000 */
#define GPIO_IDR_IDR12 GPIO_IDR_IDR12_Msk /*!< Port input data, bit 12 */
Используя имя бита можно написать:
if( (GPIOB->IDR & GPIO_IDR_IDR12) == 0 ) {
Теперь об установке логического состояния выхода. Есть несколько вариантов.
Первый – через регистр вывода.
Установка бита 13:
GPIOB->ODR |= 1 << 13;
или
GPIOB->ODR |= GPIO_ODR_ODR13;
Сброс бита 13:
GPIOB->ODR &= ~(1 << 13);
или
GPIOB->ODR &= ~ GPIO_ODR_ODR13;
Вставляем в логические блоки включение и выключение светодиода.
if( (GPIOB->IDR & GPIO_IDR_IDR12) == 0 ) {
// бит 12 = 0, кнопка нажата
GPIOB->ODR &= ~ GPIO_ODR_ODR13; // зажечь светодиод
}
else {
// бит 12 = 1, кнопка отжата
GPIOB->ODR |= GPIO_ODR_ODR13; // погасить светодиод
}
И всю эту конструкцию в цикл while(1) в файле main.c.
Компилируем, загружаем, проверяем. У меня работает.
Для установки состояния вывода порта мы использовали последовательность: чтение регистра вывода данных, установку бита и запись в этот же регистр. В предыдущем уроке я писал про регистр установки/сброса битов, с помощью которого можно выполнить изменение состояния выхода одним обращением к регистру.
Если нам надо установить вывод PB13 в 1, то:
GPIOB -> BSRR = GPIO_BSRR_BS13; // установка бита
Для сброса бита надо выполнить команду:
GPIOB -> BSRR = GPIO_BSRR_BR13; // сброс бита
Можно сбросить или установить сразу несколько битов одним обращением к регистру BSRR. Это будет выглядеть так.
GPIOB -> BSRR = GPIO_BSRR_BS10 | GPIO_BSRR_BS15 | GPIO_BSRR_BR11 | GPIO_BSRR_BR12; // установить биты 10 и 15, сбросить биты 11 и 12
Проверяем в программе.
if( (GPIOB->IDR & GPIO_IDR_IDR12) == 0 ) {
// бит 12 = 0, кнопка нажата
// GPIOB->ODR &= ~ GPIO_ODR_ODR13; // зажечь светодиод
GPIOB -> BSRR = GPIO_BSRR_BR13; // сброс бита
}
else {
// бит 12 = 1, кнопка отжата
// GPIOB->ODR |= GPIO_ODR_ODR13; // погасить светодиод
GPIOB -> BSRR = GPIO_BSRR_BS13; // установка бита
}
Еще для завершения конфигурации портов надо установить 1 в регистр вывода данных для бита PB12, настроенного на вход. Тем самым мы подключим подтягивающий резистор к шине питания, а не к земле.
GPIOB -> BSRR = GPIO_BSRR_BS12; // подтягивающий резистор к + 3 В
Добавим эту строку в блок начальной конфигурации портов.
С регистром защиты LCKR разберетесь сами. Установка и сброс битов:
GPIOB -> LCKR |= GPIO_LCKR_LCK12 | GPIO_LCKR_LCK13;
GPIOB -> LCKR &= ~ (GPIO_LCKR_LCK110 | GPIO_LCKR_LCK11);
GPIOB -> LCKR |= GPIO_LCKR_LCKK;
GPIOB -> LCKR &= ~ GPIO_LCKR_LCKK;
Загрузить проект полностью можно по ссылке:
Многим покажется, что работать с портами на STM32 очень сложно. Особенно если сравнивать c аналогичными операциями в системе Ардуино. Но на Ардуино мы используем не прямое обращение, а функции. В следующем уроке будем работать с функциями управления портами на STM32. Все будет намного проще.
Но когда-нибудь вам потребуется использовать порты микроконтроллера с максимальным быстродействием. Тогда вы наверняка вынуждены будете вернуться к этому уроку.
В следующем уроке будем управлять портами STM32 с помощью библиотеки HAL.
Здравствуйте. Заранее прошу прощение за глупый вопрос. Строка : GPIOB->CRH |= (0b00 << GPIO_CRH_CNF13_Pos) | (0b10 << GPIO_CRH_MODE13_Pos) ; // устанавливаем биты
Символ и b в 0b00 что значит?
Здравствуйте!
Так записывается число в двоичной форме. Вместо 0b10 можно было написать десятичное число 2.
Здравствуйте, заранее прошу прощения за свою невнимательность (возможно вы об этом писали или уже отвечали кому-то в комментариях). Я лишь начинаю изучать программирование STM32 и мне не совсем ясно, почем вы всезда записываете значения а регистры не простым присвоением: «_GPIOx->CRL = 100000000», а так: «_GPIOx->CRL |= 0b1000<IDR & GPIO_IDR_IDR12) == 0 ) {«, предположим, GPIOB->IDR это 0100100100, GPIO_IDR_IDR12 это 0100000000, и далее применив побитово & получит 010000000, что с 1 сравнить невозможно, только если побитово каждый бит, что -то я тут запутался. Ещё раз простите за глупые вопросы. Спасибо!
Здравствуйте!
При операции присвоения вы обязательно установите состояние всех битов. Для того, чтобы изменить только нужные биты используются 2 последовательные операции: логическое ИЛИ (устанавливает выбранные биты в 1) и логическое И (сбрасывает выбранные биты).
При сравнении для выделения нужных битов используется логическое И.
if( (GPIOB->IDR & (1 < < 12)) == 0 ) Если 12й бит будет равен 0, то и выражение в скобках будет равно 0. Если он равен 1, то выражение будет иметь не нулевое значение.
Спасибо за ответы.
Часть второго вопроса удалилась, речь шла о строчке if( (GPIOB->IDR & GPIO_IDR_IDR12) == 0 ) { Ещё был вопрос: почему так: if flag ==0 или flag!=0 , а не так: if flag ==0 или flag==1?
Разницы особой нет. В первом случае значение истинно трактуется, как не нулевое. А во втором, как равное 1.
«И всю эту конструкцию в цикл while(1) в файле main.c.»
Под «Всю конструкцию» подразумевается и та часть, где мы настраивали наши порты на Вход\выход? (работа с регистром CRH)
Если да, то зачем эту часть помещать в цикл? CRH же не меняется в ходе работы программы. Т.е. достаточно один раз где-то задать и все
Если нет, то, собственно, где задать эту часть с установкой СRH? Выше блока while в самом блоке Int main?
Здравствуйте!
Имеется в виду блок проверки состояния кнопки и управления светодиодом.
while (1)
{
if( (GPIOB->IDR & GPIO_IDR_IDR12) == 0 ) {
// бит 12 = 0, кнопка нажата
// GPIOB->ODR &= ~ GPIO_ODR_ODR13; // зажечь светодиод
GPIOB -> BSRR = GPIO_BSRR_BR13; // сброс бита
}
else {
// бит 12 = 1, кнопка отжата
// GPIOB->ODR |= GPIO_ODR_ODR13; // погасить светодиод
GPIOB -> BSRR = GPIO_BSRR_BS13; // установка бита
}
Эдуард, спасибо огромное. Ваши уроки это просто праздник какой-то. Вот здесь наверное очепятка:
Еще для завершения конфигурации портов надо установить 1 в регистр вывода данных для бита PB12, настроенного на вход.
……..
GPIOB -> BSRR = GPIO_BSRR_BS12; // подтягивающий резистор к + 3 В
Видимо имелся в виду регистр ODR?
Здравствуйте!
Нет, все правильно. Нам надо установить только один бит в регистре ODR и делаем это с помощью регистра BSRR. Если мы применим такую операцию к ODR, то мы не только установим 12 бит, но и сбросим все остальные биты.
Да, конечно! Прошу пардону, сразу не догнал.
Здравствуйте, подскажите пожалуйста, обязательно ли в теле программы конфигурировать порты следующим кодом:
// начальная конфигурация портов
GPIOB->CRH &= ~ (GPIO_CRH_CNF12 | GPIO_CRH_MODE12); // сбрасываем биты полей CNF и MODE
GPIOB->CRH |= (0b10 << GPIO_CRH_CNF12_Pos) | (0b00 <CRH &= ~ (GPIO_CRH_CNF13 | GPIO_CRH_MODE13); // сбрасываем биты полей CNF и MODE
GPIOB->CRH |= (0b00 << GPIO_CRH_CNF13_Pos) | (0b10 < BSRR = GPIO_BSRR_BS12; // подтягивающий резистор к + 3 В
Или будет достаточным сделать это только в в CubeMX?
Здравствуйте!
Если в ходе программы конфигурация портов не меняется, то достаточно установить ее в CubeMX. Вы можете найти эти строчки в программе после Cube.
А можете конкретнее указать в каком файле они находятся, после того как проект был сгенерирован в кубе? Спасибо.
В main.c.
Здравствуйте. Подскажите пожалуйста, почему вы выбрали именно такой способ подключения светодиода, можно ли в этом проекте было подключить анод в выводу 13 а катод к земле (как в проектах ардуино)?
Здравствуйте!
Да, конечно. Можно подключить светодиод на вытекающий ток. Считается, что втекающий ток идет по более короткому пути в микроконтроллере, вызывает меньший нагрев. Если число нагрузок (подключенных выходов) достаточно большое, то лучше использовать схему, которую применил я. Еще, у некоторых микросхем максимально допустимый втекающий ток больше, чем вытекающий.
Здравствуйте!
У меня в файле stm32f407xx.h я нашёл «структуру для регистров портов» такого вида:
typedef struct
{
__IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */
__IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */
__IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */
__IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */
__IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
__IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */
__IO uint32_t BSRR; /*!< GPIO port bit set/reset register, Address offset: 0x18 */
__IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */
__IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */
} GPIO_TypeDef;
Открыл даташит(190 страниц или это не он?) stm32f407 и там не нашёл регистров вот так красиво нарисованных как у Вас по 16, 32 бита, думал там хоть по-английски что-то будет написано.
Я так понимаю, что уже в stm32f407 обращение к регистру CRL не получится так как его там нет? и вместо него уже MODER,OTYPER,OSPEEDR регистры? PUPDR- отдельный для подтяжки к питанию/земле…еще есть новый регистр AFR[2] и отсутствует BRR..
Где искать описание для каждого регистра конкретного микроконтроллера STM(а то в рекламе была хорошая уловка о совместимости этих МК)?
И есть ли это описание в программе STM32CubeMX? если есть то где там искать? (мне бы хотелось верить что должно быть, так-как при установке STM32CubeMX он подгрузил Гигабайт информации конкретно к моему МК).
Здравствуйте!
Я с этими микроконтроллерами не работал. Надо читать описание линейки STM32 F4. На официальном сайте должно быть.
Для тех кто тоже начнёт искать на st com. Найти нереально.
файл называется RM0090: STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 advanced Arm®-based 32-bit MCUs
В поиске нужно искать: DM00031020
Reference manual называется, там 1749 страниц.
Нормально все излагаете, доступно.
хотя новичкам в контроллерах и С наверняка непросто.
GPIOB -> BSRR = GPIO_BSRR_BS12; // подтягивающий резистор к + 3 В
Здесь похоже ошибка. Подтягиваем в регистре GPIOB -> ODR.
Хочу поблагодарить за очень основательные пошаговые инструкции по работе с STM32, скажем, самые лучшие и понятные. Но у меня вопрос: почему все обходят USB порт, которій находится на борту отладочных плат, что с ним не так? Помоему только у Ардуинщиков я встречал его использование.
Здравствуйте!
Спасибо за добрые слова.
Что касается встроенного USB, то с ним надо уметь работать. Часто проще использовать UART и конвертер интерфейсов.
Здравствуйте подскажите пожалуйста вот в этом куске кода:
‘Установка регистра конфигурации с использованием имен битов выглядит так.
GPIOB->CRH &= ~ (GPIO_CRH_CNF12 | GPIO_CRH_MODE12); // сбрасываем биты полей CNF и MODE
GPIOB->CRH |= (0b10 << GPIO_CRH_CNF12_Pos) | (0b00 <CRH» 0x80000, а разве мы не получим в итоге в «GPIOB->CRH» 0x20000?
Здравствуйте!
Да, но мы установим только нужные нам биты. Остальные оставим без изменения.
Доброе время суток! Такой вопрос. Когда то давным-давно я активно занимался программированием 8 разрядных микроконтроллеров на макро ассемблере. Для установки одного или нескольких бит в регистре по маске использовал обычный макрос, и подобный макрос (по той же маске) использовал для сброса битов. И программировать удобно и исходники были очень читабельны (при подавлении тела macro). Маски всегда можно было найти в include файлах или определить самому.
Что то подобное на C можно забодяжить?