Урок 7. Управление портами ввода-вывода через регистры CMSIS.

Уроки STM32

Научимся управлять портами через прямое обращение к регистрам.

Предыдущий урок     Список уроков     Следующий урок

В уроке 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. Он содержит имена регистров микроконтроллера.

Файл 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 светодиод.

Схема подключение светодиода и кнопки к STM32

И напишем программу, которая управляет светодиодом с помощью кнопки. Кнопка нажата – светодиод светится, отжата – светодиод погашен.

 

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

В структуре 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;

Загрузить проект полностью можно по ссылке:

  Зарегистрируйтесь и оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!  

Многим покажется,  что работать с портами на STM32 очень сложно. Особенно если сравнивать c аналогичными операциями в системе Ардуино. Но на Ардуино мы используем не прямое обращение, а функции. В следующем уроке будем работать с функциями управления портами на STM32. Все будет намного проще.

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

 

В следующем уроке будем управлять портами STM32 с помощью библиотеки HAL.

Предыдущий урок     Список уроков     Следующий урок

0

Автор публикации

не в сети 5 часов

Эдуард

63
Комментарии: 1354Публикации: 152Регистрация: 13-12-2015

9 комментариев на «Урок 7. Управление портами ввода-вывода через регистры CMSIS.»

  1. Здравствуйте. Заранее прошу прощение за глупый вопрос. Строка : GPIOB->CRH |= (0b00 << GPIO_CRH_CNF13_Pos) | (0b10 << GPIO_CRH_MODE13_Pos) ; // устанавливаем биты
    Символ и b в 0b00 что значит?

    0
  2. Здравствуйте, заранее прошу прощения за свою невнимательность (возможно вы об этом писали или уже отвечали кому-то в комментариях). Я лишь начинаю изучать программирование STM32 и мне не совсем ясно, почем вы всезда записываете значения а регистры не простым присвоением: «_GPIOx->CRL = 100000000», а так: «_GPIOx->CRL |= 0b1000<IDR & GPIO_IDR_IDR12) == 0 ) {«, предположим, GPIOB->IDR это 0100100100, GPIO_IDR_IDR12 это 0100000000, и далее применив побитово & получит 010000000, что с 1 сравнить невозможно, только если побитово каждый бит, что -то я тут запутался. Ещё раз простите за глупые вопросы. Спасибо!

    0
    • Здравствуйте!
      При операции присвоения вы обязательно установите состояние всех битов. Для того, чтобы изменить только нужные биты используются 2 последовательные операции: логическое ИЛИ (устанавливает выбранные биты в 1) и логическое И (сбрасывает выбранные биты).
      При сравнении для выделения нужных битов используется логическое И.
      if( (GPIOB->IDR & (1 < < 12)) == 0 ) Если 12й бит будет равен 0, то и выражение в скобках будет равно 0. Если он равен 1, то выражение будет иметь не нулевое значение.

      0
  3. Часть второго вопроса удалилась, речь шла о строчке if( (GPIOB->IDR & GPIO_IDR_IDR12) == 0 ) { Ещё был вопрос: почему так: if flag ==0 или flag!=0 , а не так: if flag ==0 или flag==1?

    0
    • Разницы особой нет. В первом случае значение истинно трактуется, как не нулевое. А во втором, как равное 1.

      0
  4. «И всю эту конструкцию в цикл while(1) в файле main.c.»
    Под «Всю конструкцию» подразумевается и та часть, где мы настраивали наши порты на Вход\выход? (работа с регистром CRH)
    Если да, то зачем эту часть помещать в цикл? CRH же не меняется в ходе работы программы. Т.е. достаточно один раз где-то задать и все
    Если нет, то, собственно, где задать эту часть с установкой СRH? Выше блока while в самом блоке Int main?

    0
    • Здравствуйте!
      Имеется в виду блок проверки состояния кнопки и управления светодиодом.
      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; // установка бита
      }

      0

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *