company_banner

Низкоуровневое программирование STM32: от включения питания до «Hello, World»

Автор оригинала: Maya Posch
  • Перевод
В этом материале я хочу рассказать о том, как писать программы для микроконтроллеров (Microcontroller Unit, MCU) Cortex-M, вроде STM32, используя лишь набор инструментов ARM и документацию, подготовленную STMicroelectronics. У некоторых читателей может появиться вопрос о том, почему кому-то это может понадобиться. Если вам эта идея, на первый взгляд, не показалась очень уж страшной, то, возможно, вам будет интересно то, о чём пойдёт речь в этом материале. И, кстати, подумаем о том, кому и зачем это может пригодиться.

Конечно, разрабатывать программы для MCU STM32 можно с помощью существующих фреймворков. Это может быть ST HAL, обычный CMSIS, или даже что-то, более близкое к Arduino. Но… что тут увлекательного? Ведь, в итоге, тот, кто пользуется каким-то фреймворком, полностью зависим от документации к нему и от его разработчиков. И, с другой стороны, если документация к STM32 кажется кому-то, работающему с этой платформой, так сказать, бредом сивой кобылы, то можно ли говорить о том, что этот человек по-настоящему понимает данную платформу?



Поэтому давайте поговорим о низкоуровневом программировании STM32 и доберёмся от включения питания STM32 до «Hello, World».

STM32 очень похож на компьютер


На низком уровне микроконтроллер не особенно сильно отличается от полнофункционального компьютера, основанного на процессоре от Intel или AMD. Тут имеется как минимум одно процессорное ядро, инициализирующееся после подачи и стабилизации внешнего питания. В этот момент производится считывание загрузчика, код которого находится по адресу, заранее известному микроконтроллеру. А в обычных компьютерах подобную роль играет BIOS. В случае с MCU это — код, находящийся по определённому смещению в (обычно) интегрированной памяти, предназначенной только для чтения. То, что происходит потом, полностью зависит от этого кода.

В целом, этот код решает основные задачи по подготовке системы к работе. Например, задаёт таблицу векторов прерываний и записывает определённые данные в некие регистры. Очень важной задачей, кроме того, является инициализация указателя стека (Stack Pointer, SP). В начале работы системы некоторые данные из ROM копируются в RAM. В итоге вызывается функция main(), что похоже на запуск операционной системы обычного компьютера, выполняемый после завершения подготовки системы к работе средствами BIOS.

Пример Pushy


Возможно, аналогом «Hello, World» для STM32 можно назвать пример из моего фреймворка для STM32 Nodate, который я ласково называю Pushy. Он ещё проще, чем традиционный пример Blinky, так как он использует лишь регистры управления тактированием и сбросом (Reset & Clock Control, RCC) и базовые возможности интерфейса ввода/вывода общего назначения (General-Purpose Input/Output, GPIO). Код этого примера считывает входной регистр GPIO-пина и подстраивает значение на выходном пине в соответствии с входным. Благодаря этому можно, с помощью кнопки, включать и выключать светодиод. Вот код этого примера:

#include <gpio.h>

int main () {
  //const uint8_t led_pin = 3; // Nucleo-f042k6: Port B, pin 3.
  //const GPIO_ports led_port = GPIO_PORT_B;
  //const uint8_t led_pin = 13; // STM32F4-Discovery: Port D, pin 13 (оранжевый)
  //const GPIO_ports led_port = GPIO_PORT_D;
  //const uint8_t led_pin = 7; // Nucleo-F746ZG: Port B, pin 7 (синий)
  //const GPIO_ports led_port = GPIO_PORT_B;
  const uint8_t led_pin = 13; // Blue Pill: Port C, pin 13.
  const GPIO_ports led_port = GPIO_PORT_C;
  
  //const uint8_t button_pin = 1; // Nucleo-f042k6 (PB1)
  //const GPIO_ports button_port = GPIO_PORT_B;
  //const uint8_t button_pin = 0; // STM32F4-Discovery (PA0)
  //const GPIO_ports button_port = GPIO_PORT_A;
  //const uint8_t button_pin = 13; // Nucleo-F746ZG (PC13)
  //const GPIO_ports button_port = GPIO_PORT_C;
  const uint8_t button_pin = 10; // Blue Pill
  const GPIO_ports button_port = GPIO_PORT_B;
  
  // Установить режим вывода на пине, к которому подключён светодиод
  GPIO::set_output(led_port, led_pin, GPIO_PULL_UP);
  GPIO::write(led_port, led_pin, GPIO_LEVEL_LOW);
  
  // Установить режим ввода на пине, к которому подключена кнопка
  GPIO::set_input(button_port, button_pin, GPIO_FLOATING);
  
  // Если кнопка нажата (переход от высокого состояния к низкому), то 'button_down' будет в низком состоянии в том случае, если кнопка будет нажата.
  // Если кнопка не нажата (переход от низкого состояния к высокому, к Vdd), то 'button_down' будет в высоком состоянии в том случае, если кнопка будет нажата.
  uint8_t button_down;
  while (1) {
    button_down = GPIO::read(button_port, button_pin);
    if (button_down == 1) {
      GPIO::write(led_port, led_pin, GPIO_LEVEL_HIGH);
    }
    else {
      GPIO::write(led_port, led_pin, GPIO_LEVEL_LOW);
    }
  }
  
  return 0;
}

Тут можно сразу обратить внимание на два самых заметных элемента. Первый — это функция main(), которую вызывает система. Второй — это подключения модуля GPIO. Этот модуль содержит статический C++-класс, возможности которого используются для записи данных в GPIO-выход, к которому подключён светодиод. Его же возможности применяются и при чтении данных с входа, к которому подключена кнопка. Тут можно видеть ещё и упоминание имён пинов платы Blue Pill (STM32F103C8), но в примере имеются предустановки и для других плат, которые можно активировать, раскомментировав соответствующие строки.

Где именно в этом примере используются регистры группы RCC? В названии этих регистров содержится намёк на то, что они позволяют управлять тактовой частотой MCU. Их можно сравнить с переключателями, которые могут пребывать в двух состояниях — «включено» или «выключено», включая и отключая соответствующие возможности MCU. Если посмотреть, например, на описание регистра RCC_AHBENR в разделе 6.4 руководства по STM32F0xx, то мы увидим бит, маркированный как IOPAEN (Input/Output Port A ENable, включение порта ввода/вывода A), который управляет частотой для периферии, подключённой к GPIO A. То же касается и других портов.


Раздел 6.4.6 руководства по STM32F0xx, описание регистра RCC_AHBENR

Как можно видеть на вышеприведённой иллюстрации, RCC_AHBENR — это регистр, отвечающий за включение AHB. Это — одна из шин внутри MCU, к которой подключены процессорное ядро, SRAM, ROM и периферийные устройства.

Шины AHB (Advanced High-performance Bus) и APB (Advanced Peripheral Bus) описаны в спецификации AMBA фирмы Arm.


Раздел 2.1 руководства по STM32F0xx, архитектура STM32F0xx

В целом можно отметить, что AHB — это более быстрая шина, соединяющая процессорное ядро со SRAM, ROM и с высокоскоростной периферией. Более медленная периферия подключается к более медленной шине APB. Между AHB и APB имеется мост, позволяющий устройствам, подключённым к ним, взаимодействовать друг с другом.

Низкоуровневое программирование


Как уже было сказано, первым при включении STM32 запускается код загрузчика. В случае с MCU STM32F042x6 универсальный код загрузчика, написанный на ассемблере Thumb, можно найти здесь. Это — обычный код, предоставляемый STMicroelectronics (например, для STM32F0xx) вместе с CMSIS-пакетом. Он инициализирует MCU и вызывает функцию SystemInit(), объявленную в низкоуровневом C-коде CMSIS (вот — пример для STM32F0xx).

Функция SystemInit() сбрасывает системные регистры, отвечающие за частоту, что приводит к использованию стандартной частоты HSI (High Speed Internal oscillator, высокоскоростной внутренний генератор). После выполнения процедур настройки libc (в данном случае используется Newlib — вспомогательная C/C++-библиотека) она, наконец, вызывает функцию main() следующей командой:

bl main

Эта инструкция, название которой расшифровывается как Branch with Link (переход с сохранением адреса возврата), приводит к переходу к заданной метке. В этот момент мы оказываемся в функции main() нашего примера Pushy. После этого в дело вступают возможности класса GPIO.

Класс GPIO


Первый вызываемый нами метод класса — это GPIO::set_output(). Он позволяет сделать указанный пин (с подключённым к нему повышающим резистором) выходным. Именно здесь мы встречаемся с первым различием между семействами MCU STM32. Дело в том, что более старые MCU, основанные на Cortex-M3 F1, имеют GPIO-периферию, очень сильно отличающуюся от той, которая используется в их более новых собратьях семейств F0, F4 и F7. Это выражается в том, что при работе с пинами STM32F1xx нужно записывать в единственный регистр множество опций:

  // Управление вводом/выводом распределено между двумя комбинированными регистрами (CRL, CRH).
  if (pin < 8) {
    // Установим регистр CRL (CNF & MODE).
    uint8_t pinmode = pin * 4;
    uint8_t pincnf = pinmode + 2;
    
    if (speed == GPIO_LOW) {    instance.regs->CRL |= (0x2 << pinmode); }
    else if (speed == GPIO_MID) {  instance.regs->CRL |= (0x1 << pinmode);  }
    else if (speed == GPIO_HIGH) {  instance.regs->CRL |= (0x3 << pinmode);  }
  
    if (type == GPIO_PUSH_PULL) {    instance.regs->CRL &= ~(0x1 << pincnf);  }
    else if (type == GPIO_OPEN_DRAIN) {  instance.regs->CRL |= (0x1 << pincnf);  }
  }
  else {
    // Установим регистр CRH.
    uint8_t pinmode = (pin - 8) * 4;
    uint8_t pincnf = pinmode + 2;
    
    if (speed == GPIO_LOW) {    instance.regs->CRH |= (0x2 << pinmode); }
    else if (speed == GPIO_MID) {  instance.regs->CRH |= (0x1 << pinmode);  }
    else if (speed == GPIO_HIGH) {  instance.regs->CRH |= (0x3 << pinmode);  }
  
    if (type == GPIO_PUSH_PULL) {    instance.regs->CRH &= ~(0x1 << pincnf);  }
    else if (type == GPIO_OPEN_DRAIN) {  instance.regs->CRH |= (0x1 << pincnf);  }
  }

А в других упомянутых семействах MCU имеются отдельные регистры для каждой опции (режим, скорость, повышающий или понижающий резистор, тип):

  uint8_t pin2 = pin * 2;
  instance.regs->MODER &= ~(0x3 << pin2);
  instance.regs->MODER |= (0x1 << pin2);
  
  instance.regs->PUPDR &= ~(0x3 << pin2);
  if (pupd == GPIO_PULL_UP) {
    instance.regs->PUPDR |= (0x1 << pin2);
  }
  else if (pupd == GPIO_PULL_DOWN) {
    instance.regs->PUPDR |= (0x2 << pin2);
  }
  
  if (type == GPIO_PUSH_PULL) {
    instance.regs->OTYPER &= ~(0x1 << pin);
  }
  else if (type == GPIO_OPEN_DRAIN) {
    instance.regs->OTYPER |= (0x1 << pin);
  }
  
  if (speed == GPIO_LOW) {
    instance.regs->OSPEEDR &= ~(0x3 << pin2);
  }
  else if (speed == GPIO_MID) {
    instance.regs->OSPEEDR &= ~(0x3 << pin2);
    instance.regs->OSPEEDR |= (0x1 << pin2);
  }
  else if (speed == GPIO_HIGH) {
    instance.regs->OSPEEDR &= ~(0x3 << pin2);
    instance.regs->OSPEEDR |= (0x3 << pin2);
  }

Запись опций в регистры выполняется с помощью побитовых операций, используемых для записи значений с применением битовых масок. Имена регистров обычно хорошо описывают их предназначение. Например — PUPDR расшифровывается как Pull-Up Pull-Down Register — «регистр управления подтяжкой к плюсу питания или к земле».

То, в каком именно стиле работать, зависит от программиста. Однако, в случае с настройкой входных пинов, я предпочитаю более современный способ настройки GPIO. В результате получается компактный и аккуратный код, напоминающий следующий, а не тот ужас, который характерен для STM32F1xx:

  uint8_t pin2 = pin * 2;
  instance.regs->MODER &= ~(0x3 << pin2);
  instance.regs->PUPDR &= ~(0x3 << pin2);
  if (pupd == GPIO_PULL_UP) {
    instance.regs->PUPDR |= (0x1 << pin2);
  }
  else {
    instance.regs->PUPDR |= (0x2 << pin2);
  }

Для чтения данных с входного пина мы пользуемся IDR (Input Data Register, регистр входных данных) для банка GPIO, с которым работаем:

  uint32_t idr = instance.regs->IDR;
  out = (idr >> pin) & 1U;  // Прочитать нужный бит.

Аналогично выглядит и использование ODR (Output Data Register, регистр выходных данных), с помощью которого осуществляется вывод данных на пин:

  if (level == GPIO_LEVEL_LOW) {
    instance.regs->ODR &= ~(0x1 << pin);
  }
  else if (level == GPIO_LEVEL_HIGH) {
    instance.regs->ODR |= (0x1 << pin);
  }

И, наконец, в вышеприведённом коде имеется сущность instance, которая представляет собой ссылку на запись в структуре std::vector. Она была статически создана в ходе запуска MCU. В ней зарегистрированы свойства периферии:

std::vector<GPIO_instance>* GPIO_instances() {
  GPIO_instance instance;
  static std::vector<GPIO_instance>* instancesStatic = new std::vector<GPIO_instance>(12, instance);
  
#if defined RCC_AHBENR_GPIOAEN || defined RCC_AHB1ENR_GPIOAEN || defined RCC_APB2ENR_IOPAEN
  ((*instancesStatic))[GPIO_PORT_A].regs = GPIOA;
#endif

#if defined RCC_AHBENR_GPIOBEN || defined RCC_AHB1ENR_GPIOBEN || defined RCC_APB2ENR_IOPBEN
  ((*instancesStatic))[GPIO_PORT_B].regs = GPIOB;
#endif
  
  [..]
  
  return instancesStatic;
}

static std::vector<GPIO_instance>* instancesStatic = GPIO_instances();

Если периферийное устройство существует (то есть — имеется в CMSIS-заголовке для конкретного MCU, например, для STM32F042), то в структуре GPIO_instance создаётся запись, указывающая на память, соответствующую регистрам этого устройства (regs). К этим записям, равно как и к мета-информации, содержащейся в них, потом можно обращаться. Например, можно узнать о состоянии устройства:

  GPIO_instance &instance = (*instancesStatic)[port];
  
  // Проверяем, является ли порт активным. Если это не так -  активируем его.
  if (!instance.active) {
    if (Rcc::enablePort((RccPort) port)) {
      instance.active = true;
    }
    else {
      return false;
    }
  }

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

Класс RCC


Класс RCC тоже интересуется тем, существует ли то или иное периферийное устройство. Делается это для того чтобы избежать разного рода сюрпризов. При этом используются те же определения препроцессора CMSIS. После проверки существования устройства включить его довольно просто:

bool Rcc::enable(RccPeripheral peripheral) {
  uint8_t perNum = (uint8_t) peripheral;
  RccPeripheralHandle &ph = (*perHandlesStatic)[perNum];
  
  if (ph.exists == false) {
    return false;
  }
  
  // Проверка состояния текущего периферийного устройства.
  if (ph.count > 0) {
    if (ph.count >= handle_max) {
      return false;
    }
    
    // Увеличение количества обработчиков на 1.
    ph.count++;
  }
  else {
    // Активация периферийного устройства.
    ph.count = 1;
    *(ph.enr) |= (1 << ph.enable);
  }
  
  return true;
}

В дополнение к изменению значения соответствующего бита (ph.enable) мы подсчитываем ссылки. Это делается для того чтобы случайно не выключить периферийное устройство, которое используется в другом месте кода.

Запуск примера


После того, как мы разобрались с вышеприведённым материалом, у нас должно появиться некоторое понимание того, как пример Pushy работает на низком уровне. Теперь мы можем его собрать и запустить. Для этого нам понадобится, как уже было сказано, набор инструментов ARM и фреймворк Nodate. Первый можно установить с помощью используемого вами менеджера пакетов (речь идёт о пакете arm-none-eabi-gcc) или — загрузив его с сайта Arm. Фреймворк Nodate можно установить с GitHub. После этого путь к корневой папке фреймворка нужно записать в глобальную системную переменную NODATE_HOME.

После того, как эти задачи решены, нужно перейти в папку Nodate, а потом — в подпапку examples/stm32/pushy. В ней надо открыть файл Makefile и указать предустановки, рассчитанные на используемую плату (там сейчас есть предустановки для Blue Pill, Nucleo-F042K6, STM32F4-Discovery, Nucleo-746ZG). Далее, надо открыть файл src/pushy.cpp и раскомментировать строки, имеющие отношение к целевой плате.

Далее, в папке, в которой находится Makefile, нужно собрать проект командой make. Целевая плата должна быть подключена к компьютеру с использованием ST-Link, на компьютере должна быть установлена программа OpenOCD. Если это так — MCU можно прошить командой make flash. После этого соответствующий образ будет записан в память устройства.

Когда кнопка подключена к используемому в коде пину и к Vdd, нажатие на эту кнопку должно зажигать соответствующим образом подключенный к плате светодиод.

Здесь продемонстрирован простой пример низкоуровневого программирования STM32. Освоив его, вы, фактически, находитесь лишь немного «ниже» уровня примера Blinky.

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

Какие инструменты вы используете при написании программ для STM32?



RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Комментарии 19

    +5
    Отлично! новый велосипед, да еще какой!
    Задавать номер пина GPIO для чтения как число, а не как битовую маску — сразу видно почерк не bare metal кодера. Да и все равно, в конкретном проекте пины МК зачастую выполняют предельно конкретные функции и имеют четкие имена, из за чего их все равно лучше обьявлять константами или прости господи дефайнами.
    Даже во всеми ругаемом медленном и глючном st HAL так не делают.
    Ну, чо тут такого, скажете вы, всего на пару машинных операций больше.
    Но поверьте, даже в наше время когда самый вшивый МК мощнее железок, что летали на луну бывает необходимость экономии на вот таких вот мелочах.
      +4
      Так в Arduino делают.
      А от статьи с названием «низкоуровневое программирование» ожидаешь скорее вот такого habr.com/ru/post/490474 чем подробного описания очередного фреймворка.
        0
        всю жизнь считал низкоуровневым программированием — програмирование как минимум на ассемблере…
        хотя сейчас асм не в моде :-)
      +3
      Мне кажется, что это немного «странный» фреймворк. С одной стороны, написано на c++, но используется только ключевое слово class и оператор :: — разрешение контекста.

      Очень много дублирования кода и неоптимального обращения к регистрам контроллера, например:
      Исходник
        if (pin < 8) {
          // Установим регистр CRL (CNF & MODE).
          uint8_t pinmode = pin * 4;
          uint8_t pincnf = pinmode + 2;
          
          if (speed == GPIO_LOW) {    instance.regs->CRL |= (0x2 << pinmode); }
          else if (speed == GPIO_MID) {  instance.regs->CRL |= (0x1 << pinmode);  }
          else if (speed == GPIO_HIGH) {  instance.regs->CRL |= (0x3 << pinmode);  }
                                        // Две записи подряд в один и тот же регистр
          if (type == GPIO_PUSH_PULL) {    instance.regs->CRL &= ~(0x1 << pincnf);  }
          else if (type == GPIO_OPEN_DRAIN) {  instance.regs->CRL |= (0x1 << pincnf);  }
        }
        else {
          // Установим регистр CRH.
          uint8_t pinmode = (pin - 8) * 4;
          uint8_t pincnf = pinmode + 2;
          
          if (speed == GPIO_LOW) {    instance.regs->CRH |= (0x2 << pinmode); }
          else if (speed == GPIO_MID) {  instance.regs->CRH |= (0x1 << pinmode);  }
          else if (speed == GPIO_HIGH) {  instance.regs->CRH |= (0x3 << pinmode);  }
        
          if (type == GPIO_PUSH_PULL) {    instance.regs->CRH &= ~(0x1 << pincnf);  }
          else if (type == GPIO_OPEN_DRAIN) {  instance.regs->CRH |= (0x1 << pincnf);  }
        }
      


      Как минимум, можно сократить в 3 раза:
      Модификация
      volatile uint32_t * const reg = pin < 8 ? &instance.regs->CRL : &instance.regs->CRH;
      
      uint8_t pinmode = (pin & 7) * 4;
      uint8_t pincnf = pinmode + 2;
      uitn32_t mask = 0xF << pinmode;
      uint32_t speed = (uint32_t)speed << pinmode;
      uint32_t type = (uint32_t)type << pincnf;
      
      *reg = (*reg & (~mask)) | speed | type;  
      


      Очень много проверок в рантайме, наподобие таких:
      ...
      if (pin > 15){}
      ...
      if (pin < 8){}
      ...
      

      которых, думаю, лучше избегать.
        0
        Скажите, использование препроцессора является эффективным в такой реализации — http://we.easyelectronics.ru/blog/STM32/3191.html?
          0
          Относительно, зависит от того с чем сравнивать. Ну и от цели, которую перед собой поставили. Настройка и работа с периферией в МК это процентов 5-10% от объема всего кода. Остальное логика, обработка данных. При проектировании да, стоит подумать над тем как эффективней использовать, а не настраивать.

          Если сравнивать настройку через SPL\HAL (без оптимизации), то препроцессор может дать выигрыш в производительности. Т.к. большая часть кода будет запускаться не в рантайме.

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

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

          ИМХО, Ну и сейчас тенденция миграции на С++ при программирование под МК стала более заметной. Так что использование препроцессора, это скорее как один из этап развития.
        +8
        Поздравляю, вы только презентовали очередную надстройку над CMSIS. Да, все как и было обещано:
        В этом материале я хочу рассказать о том, как писать программы для микроконтроллеров (Microcontroller Unit, MCU) Cortex-M, вроде STM32, используя лишь набор инструментов ARM и документацию, подготовленную STMicroelectronics.


        Но, если уж на то пошло. Почему тогда полноценно не брать готовые битовые маски\значения для битовых полей регистров? Диссонанс прям возникает от такого:
        uint8_t pin2 = pin * 2;
        instance.regs->MODER &= ~(0x3 << pin2);
        instance.regs->MODER |= (0x1 << pin2);


        Структуру с описанием порта значит из стандарта берем (обращение к полю MODER структуры GPIOx), а маску и битовое значение посчитаем сами. Иначе видимо не получится «низкоуровневое программирование»

        Опять же, есть определенный нюанс с порядком настройки GPIO. Он описан не совсем явно (это уже к авторам вопрос), но регистр MODER прописывается в последнюю очередь, уже после всех основных настроек. Пример представлен в секции с описанием настройки альтернативной функции GPIO. Вы же пишите универсальную функцию инициализации? Значит это нужно учитывать. Вот фрагмент reference Manual, раздел GPIO, с указанием (неявно, но подтверждено на практике) порядка инициализации:
        Peripheral alternate function:
        – Connect the I/O to the desired AFx in one of the GPIOx_AFRL or GPIOx_AFRH
        register.
        – Select the type, pull-up/pull-down and output speed via the GPIOx_OTYPER,
        GPIOx_PUPDR and GPIOx_OSPEEDER registers, respectively.
        – Configure the desired I/O as an alternate function in the GPIOx_MODER register.

        Более того, если регистр будет настроек на выход, то значение в ODR (лучше через BSRR) должно быть записано заранее. Иначе на линии запросто возникает короткий импульс.

        Аналогично выглядит и использование ODR (Output Data Register, регистр выходных данных), с помощью которого осуществляется вывод данных на пин:

        Для вывода лучше использовать BSRR. Это опять же рекомендовано в документации. В добавок доступ будет атомарным, код проще, время исполнения меньше. Что-то типа этого (упрощенно, чтобы показать смысл):
        port->BSSR = 1 << pin; // для установки 1
        port->BSSR = 1 << pin << 16; // для сброса в 0

        Это позволит, в том числе, работать и с битовой маской, если нужно изменить сразу несколько пинов одного порта. Назначение регистра ODR ровно такое же, как и IDR — узнать состояние порта. Т.е. его следует использовать только для контроля, что значение действительно выставило в выходном регистре.

        Конечно, разрабатывать программы для MCU STM32 можно с помощью существующих фреймворков. Это может быть ST HAL, обычный CMSIS, или даже что-то, более близкое к Arduino. Но… что тут увлекательного?

        Использование фреймворка без чтения документации вредно. Все равно придется рано или поздно лезть в потроха для исправления ошибок авторов фреймворка. HAL именно за это и ругают чаще всего. Те ошибки, что я указал вам, были (а может и до сих пор есть) в HAL от ST. Даже в CMSIS от arm попадаются ошибки, от этого никто не застрахован. Не верите, посмотрите их github.

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

        Простите, но вы сами видимо не совсем понимаете данную платформу. Заметьте, это всего лишь GPIO, простой как 3 копейки модуль, но и в нем у вас ошибки.
        А что говорить про I2C\SPI, где таких мелочей вагон и маленькая тележка?

          0
          uint8_t pin2 = pin * 2;
          instance.regs->MODER &= ~(0x3 << pin2);
          instance.regs->MODER |= (0x1 << pin2);

          У меня вообще от такого происходит «короткое зависание». Вы правильно заметили, что для удобства, тогда уж, лучше заранее «задефайнить» битовые маски и где надо, подставлять…
          –5

          Низкоуровневое программирование МК на крестах. Рука-лицо.

            +4

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

              +3
              Во-первых плашку перевод надо сделать позаметней, слишком много кто её не замечает при первом прочтении.

              Во-вторых, люди то будут читать перевод. Сам перевод критиковать не буду, ибо я, как говорится, в данной теме «не копенгаген». Но указать на наличествующие ошибки я думаю стоит. Хотя бы для тех, кто решит пойти тем же путем что и автор.

              edit: посмотрел комментарии под оригиналом. Большая часть восторженная, статья волшебна, автор маг. Но есть и полезные, со справедливой критикой. Например, вот фрагмент ветки:

              -> Looks like your example is “How to write your own GPIO library” instead of writing a code in bare metal. i.e. If you need to call a function with a dozen lines of code trying to parse what your I/O function is, it is by definition not bare metal.

              -> Well, still, at some point doing JUST “register transfer” gets rather … tricky.


              ¯\_(ツ)_/¯
                0

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

                  +1
                  Maya Posch довольно специфичный автор и, по моему мнению, к ее произведениям стоит относиться скептически. (черт меня дернул купить ее книжку Hands-On Embedded Programming with C++17, правда немного успокаивает, что я это сделал с огромной скидкой :) )
                    0
                    Дон Бокс (один из авторов DCOM, SOAP, WCF и еще кучи технологий) как-то сказал по поводу своего очередного ухода из Майкрософт — «я с удивлением обнаружил, что писать книги гораздо выгоднее и приятнее чем программы».
                  +1
                  мне кажется не важно перевод это или нет
                  критика адресована не лично в адрес автору статьи, а тому, кто по неопытности подумает что это полезные знания, не разберется в плюсах и минусах и слепо будет применять это на практике
                  И в отличие от ошибок при рендеринге сайта или другого «высокоуровневого» айти, это может быть программист умной розетки, ошибка работы которой может дорого обойтись.
                  +1
                  Автор, вы что такое говорите? По вашему CMSIS это высокоуровневый «фреймворк» (в компании ARM все поперхнулись кофем), а ваше «изобретение» на «плюсах» это чистейший хардкор? Побойтесь Бога.

                  Зачем вводить людей в заблуждение подобными заголовками? Хочется вам порекламировать свой велосипед своё творение, ну и озаглавили бы — «Еще один „фреимворк“ для stm32». И ещё хорошо бы без долгих и нудных прологов.
                    0
                    По-моему, здесь ошибка:
                    // Если кнопка нажата (переход от высокого состояния к низкому), то 'button_down' будет в низком состоянии в том случае, если кнопка будет нажата.
                    // Если кнопка не нажата (переход от низкого состояния к высокому, к Vdd), то 'button_down' будет в высоком состоянии в том случае, если кнопка будет нажата.

                    Та же ошибка в оригинале статьи:
                    // If the button pulls down to ground (high to low), 'button_down' is low when pushed.
                    // If the button is pulled up to Vdd (low to high), 'button_down' is high when pushed.

                    «Если кнопка не нажата… если кнопка будет нажата» — такого быть не может.
                      +2
                      Тьху, ты. Видя заголовок: «Низкоуровневое программирование STM32» ожидаешь ну хотябы асеблер и написание своего загрузчика, а тут тебе подсовывают самопальный CMSIS
                        +1
                        Это вообще будет работать — при входе кнопки в «плавающем режиме»? Или автор бездумно скопировала это из документации, не разобравшись — типа «экономит энергию, это хорошо для меня (с)» — а наводки она предлагает фильтровать благодарным читателям?

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

                        Самое читаемое