Продолжаем рассмотрение базовых вопросов


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



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


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

В целом, регистр — это особый вид памяти внутри микроконтроллера, который используется для управления процессором и периферийными устройствами. Каждый регистр в архитектуре 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. Куда подключены наши светодиоды? К какому выводу микроконтроллера?
  2. Как включить тактирование на нужный порт GPIO?
  3. Как настроить, нужные нам, пины порта GPIO для того чтобы можно было включить светодиод?
  4. Как включить и выключить светодиод?

Ответим на них по порядку.

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


Для того, чтобы посмотреть где что находится на плате 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.

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

  1. Формируем число для записи:

  2. Присваиваем это значение нашему регистру:

    /* Настраиваем режим работы портов PC8 и PC9 в Output*/
    GPIOC->MODER |= 0x50000;
    


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

/* Включаем тактирование на порту 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 переключится в режим отладки. Мы можем управлять ходом программы с помощью данных кнопок:


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

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


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


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

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

Комментарии (36)


  1. oWart
    30.09.2017 22:06

    Хорошее дело делаете, так держать. Всегда положительно отношусь к авторскому материалу в отличие от большинства корпоративных статей


    1. megalloid Автор
      01.10.2017 20:10

      Будем дальше держать планку, спасибо!)


  1. x893
    30.09.2017 22:30

    Мне кажется так удобнее в Keil смотреть GPIO
    image


    1. megalloid Автор
      01.10.2017 11:53

      Не видел такое меню, копну по глубже при рассмотрении вопросов по GPIO)) спасибо))))


      1. Goron_Dekar
        01.10.2017 11:59
        +1

        Не стоит привязывать такие статьи к конкретному инструменту разработки. Они ценны именно подробностью и понятностью, когда всё делаем сами, а не делает за нас кайло/куб/etc.


      1. x893
        02.10.2017 15:06

        Не только GPIO
        image
        image
        image
        image


  1. Mogwaika
    01.10.2017 00:25

    Сразу возник вопрос, всегда ли отдельные биты конфигурационных регистров отвечают за отдельные настройки или бывают «номерные» режимы переключающие режим на основании совокупности нескольких бит (т.е. mode 0,1,2,3,4,5,6,7,8 задают 9 валидных режимов, остальные сочетания бит байта не используются, а не 256 сочетаний 8 двоичных режимов)?


    1. megalloid Автор
      01.10.2017 11:54
      +1

      Для тех кто научился ориентироваться в Reference manual — думаю не составит труда правильно интерпретировать настройки находящиеся в регистре. Думаю по прочтению этого урока у начинающих будет заложена основа при работе с документацией и регистрами))


      1. QwertyOFF
        01.10.2017 17:40

        На мой взгляд было бы круто в таких статьях описывать сначала как что-то делать "на регистрах", а потом то же самое на библиотеках HAL и с помощью CubeMX. Сейчас, как мне кажется, у статей высокий порог вхождения. Т.е. человек который ранее плотно работал с микроконтроллерами, сможет разобраться сам по даташиту и рефмануалу, а ваша статья для него лишь немного сократит время изучения. Далекий же человек, который только ардуино шил, испугается обилия наименований регистров и необходимости искать их в тысячестраничном документе. А с использованием CubeMX STM32 становится не сильно сложнее ардуино.


  1. x893
    01.10.2017 00:58
    +1

    Конечно, как и в жизни, бывают многобитовые настройки.
    Но всё эти нюансы отражены в RM и в файле имя_процессора_или_серии.h


  1. Dark_Purple
    01.10.2017 09:18
    -7

    Свет клином что ли сошолся на stm32, полно ведь других мк гораздо лучше/выше/быстрее/сильнее.


    1. masai
      01.10.2017 10:28
      +4

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


    1. megalloid Автор
      01.10.2017 11:52
      +1

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


      1. Dark_Purple
        02.10.2017 01:35

        kinetis и design studio любые кубы и стм32 заткнет за пояс например.


        1. megalloid Автор
          02.10.2017 06:44

          Я бы на вашем месте не делал столь категоричных высказываний.
          На текущий момент Kinetis при всех своих достоинствах явно проигрывает по трём, критичным для рядового радиолюбителя, качествам:
          — цена;
          — наличие в рознице;
          — наличие простых корпусов;
          Мои же статьи, в свою очередь, ориентированны именно на рядового радиолюбителя. Так что это еще как посмотреть…


  1. Arcpool
    01.10.2017 11:06

    Сам вот сел его изучать. Спасибо за статьи, добавьте пожалуйста немного HAL в дальнейшем.


    1. megalloid Автор
      01.10.2017 11:47
      +1

      Приму к сведению Ваши пожелания, могу так же посоветовать вам посмотреть по HAL много хороших видеоуроков на канале narod stream в Youtube.
      Тут: www.youtube.com/channel/UCXgs4exdtMpz4ccBZS3Yp4g


      1. Arcpool
        01.10.2017 12:45

        Спасибо, обязательно ознакомлюсь.


      1. kurojneko
        02.10.2017 09:18

        Товарищ рассказывает многое, но без основ, дает голую инструкцию, редко опускаясь в глубь на тему — почему так. Обычно ссылается на свой же курс по атмеловским железкам. И смотреть его надо последовательно, тоже из за постоянных ссылок — вот в прошлом уроке…
        Хотя ему тоже отдельное спасибо )


        1. megalloid Автор
          02.10.2017 09:21

          У него по большей части простой how-to, когда не знаешь с чего начать — можно посмотреть, а в глубинных вопросах уже копаться и разбираться самому.


          1. BigBeaver
            02.10.2017 09:25

            Не знаешь с чего начать — RTFM=)


            1. megalloid Автор
              02.10.2017 09:27

              Как вариант)))


  1. BigBeaver
    01.10.2017 11:11
    +3

    Я, конечно извиняюсь, но логика изложения очень странная. Тем, кто уже разбирается в микроконтроллерах это не нужно, а новичку будет очень тяжело. Как-то резво мы перещли от двоичной алгебры к тактированию переферии, настройке GPIO и тд. Если бы я заранее не знал, что это и зачем, то схватился бы в ужасе за голову.

    С другой стороны, даже такой материал намного лучше, чем анбоксинг очередного сомнительного гаджета. Так что впечатления двойственные.


    1. megalloid Автор
      01.10.2017 11:50

      Возможно и так, но в целом я ставил себе целью данной статьи не рассмотрение вопросов тактирования и GPIO а работу с регистрами на конкретном примере.


      1. BigBeaver
        01.10.2017 12:18

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


  1. zillant
    01.10.2017 11:48

    Пишите именно то, чего не хватает во всех остальных курсах. Огромное вам спасибо!


  1. brn
    01.10.2017 12:42

    А почему значения в регистры пишутся сразу, без предварительного считывания? Написано что не будем менять остальные настройки. Или у STM меняется только тот бит, который был записан?


    1. BigBeaver
      01.10.2017 12:44

      Там со считыванием.


    1. VerdOrr
      01.10.2017 15:41
      +3

      (x |= y) эквивалентно (x = x | y)


  1. da-nie
    01.10.2017 16:19
    +1

    Компоновщик в ходе обработки текста программы

    Компоновщик? Препроцессор! :)

    __IO

    Настоятельно советую показать определение этого макроса и расписать, зачем он нужен. :)


    1. megalloid Автор
      01.10.2017 18:01

      Спасибо за исправление!) поправил в статье)))


  1. uvavan
    01.10.2017 20:08

    Для полноты описания не хватает упоминание регистров BSRR и BRR.


    1. megalloid Автор
      01.10.2017 20:08

      Это всё в статье про GPIO рассмотрим.


  1. Sergey78
    02.10.2017 14:32
    +2

    А не удобнее для вычисления значений регистров использовать операции сдвига, а не калькулятор и hex-значение? При чтении потом не понятно, какой бит изменяется.
    Я имею ввиду, что вместо:


    GPIOC->MODER |= 0x50000;

    использовать:


    GPIOC->MODER |= (1<<16)|(1<<18);

    Конкретно в этом месте, в "настоящем" коде наверное используют буквенное обозначение, а я про использование калькулятора вцелом.


    1. megalloid Автор
      02.10.2017 19:53

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


  1. n-krd
    02.10.2017 19:19

    Спасибо автору!!! Поддерживанию стиль изложения, я последний раз использовал язык С в далекие институтские времена, запустил (поморгал св. диодом) давно пылящуюся синюю китайскую плату. Контроллер там STM32f103C8.