В свое время, более 5 лет, при поиске информации о 32-разрядных микроконтроллерах, обратил внимание, что практически все примеры для STM32 подразумевали использование SPL (Standard Peripherals Library). Цитата из Википедии:
STM32F10x Standard Peripherals Library (сокр. STM32F10x SPL) — библиотека, созданная компанией STMicroelectronics на языке Си для своих микроконтроллеров семейства STM32F10x. Содержит функции, структуры и макросы для облегчения работы с периферией микроконтроллера."

В настоящее время, для снижения порога вхождения и ускорения разработки предлагается использовать STM32CUBE. Цитата с сайта STM:
STM32Cube embedded software libraries, including:

The HAL hardware abstraction layer, enabling portability between different STM32 devices via standardized API calls.
The Low-Layer (LL) APIs, a light-weight, optimized, expert oriented set of APIs designed for both performance and runtime efficiency.
A collection of Middleware components, like RTOS, USB library, file system, TCP/IP stack, Touch sensing library or Graphic Library (depending on the MCU series)

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

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

Инициализация периферии.


Порты

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

Включение порта: 1 строчка кода:

  //инициализация порта A*************************
  RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; 

Перевод 0 пина порта A в аналоговый режим 2 строчки:

  //PA0 - PA0/ADC1_ADC2_ADC3_IN0
  // GPIO_Pin_0 порта A аналоговый вход 
  GPIOA->MODER |= GPIO_MODER_MODER0_0; 
  GPIOA->MODER |= GPIO_MODER_MODER0_1; 

Перевод 2 пина порта A в режим выхода (пуш-пулл) 1 строчка:

GPIOA->MODER |= GPIO_MODER_MODER2_0; 

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

Определение выхода 8 порта А, как ШИМ выход CH1 счетчика TIM1 (3 строчки):

  //PA8/TIM1_CH1
  // Alternate function mode
  GPIOA->MODER &= ~GPIO_MODER_MODER8_0; //0
  GPIOA->MODER |= GPIO_MODER_MODER8_1;  //1
  //GPIO alternate function high register (GPIOx_AFRL)
  //AFR8[3:0] = 0001: AF1 
  GPIOA -> AFR[1] |= 0x00000001;

Определение выхода 13 порта B, как ШИМ выход CH1N счетчика TIM1:

//PB13/TIM1_CH1N
  // Alternate function mode
  GPIOB->MODER &= ~GPIO_MODER_MODER13_0; //0
  GPIOB->MODER |= GPIO_MODER_MODER13_1;  //1
  //GPIO alternate function high register (GPIOx_AFRL)
  //AFR13[3:0] = 0001: AF1 
  GPIOB -> AFR[1] |= 0x00100000;

Закономерный вопрос: где взять обозначения нужных регистров и битов? Ответ: в 3-ех документах (на примере 746).

1. Reference manual STM32F7.pdf
2. STM32F745xx.pdf
3. stm32f746xx.h

Этих 3 файлов полностью хватает для того, чтобы правильно обращаться ко всем регистрам микроконтроллера.

АЦП


Пример инициализации АЦП для работы в режиме DMA. В этом режиме 4 канала АЦП2 автоматически переключаются по кругу и передают данные контроллеру DMA, который складывает данные в массив.

void init_ADC1(void)
{
  RCC->APB2ENR |=  RCC_APB2ENR_ADC1EN;   //подаем тактирование АЦП 
  ADC1->CR2    |=  ADC_CR2_ADON;                //включить АЦП
  ADC1->CR1    |=  ADC_CR1_EOCIE; 
  ADC1->CR1    |=  ADC_CR1_SCAN;                 // Bit 8 SCAN: Scan mode
  ADC1->CR2    |=  ADC_CR2_EOCS;                 //Bit 10 EOCS: End of conversion selection
  ADC1->CR2    |=  ADC_CR2_DMA;                   //Bit 8 DMA: Direct memory access mode (for single ADC mode)
  ADC1->CR2    |=  ADC_CR2_DDS;   //Bit 9 DDS: DMA disable selection (for single ADC mode
  
  //Bits 23:20 L[3:0]: Regular channel sequence length (4)
  //0003: 4 conversion
  ADC1->SQR1   |= ADC_SQR1_L_0;     //1
  ADC1->SQR1   |= ADC_SQR1_L_1;     //1
  ADC1->SQR1   &= ~ADC_SQR1_L_2;  //0
  ADC1->SQR1   &= ~ADC_SQR1_L_3;  //0
  
  //Bits 4:0 SQ1[4:0]: 1st conversion in regular sequence PC0/ADC1_ADC2_ADC3_IN10
  ADC1->SQR3   &= ~ADC_SQR3_SQ1_0;   //0
  ADC1->SQR3   |=  ADC_SQR3_SQ1_1;     //1
  ADC1->SQR3   &= ~ADC_SQR3_SQ1_2;   //0
  ADC1->SQR3   |=  ADC_SQR3_SQ1_3;     //1
  ADC1->SQR3   &= ~ADC_SQR3_SQ1_4;   //0

  //Bits 4:0 SQ2[4:0]: 2st conversion in regular sequence PC1/ADC1_ADC2_ADC3_IN11
  ADC1->SQR3   |=  ADC_SQR3_SQ2_0;     //1
  ADC1->SQR3   |=  ADC_SQR3_SQ2_1;     //1
  ADC1->SQR3   &= ~ADC_SQR3_SQ2_2;   //0
  ADC1->SQR3   |=  ADC_SQR3_SQ2_3;     //1
  ADC1->SQR3   &= ~ADC_SQR3_SQ2_4;   //0

  //Bits 4:0 SQ3[4:0]: 3st conversion in regular sequence PC2/ADC1_ADC2_ADC3_IN12
  ADC1->SQR3   &= ~ADC_SQR3_SQ3_0;   //0
  ADC1->SQR3   &= ~ADC_SQR3_SQ3_1;   //0
  ADC1->SQR3   |=  ADC_SQR3_SQ3_2;   //1
  ADC1->SQR3   |=  ADC_SQR3_SQ3_3;   //1
  ADC1->SQR3   &= ~ADC_SQR3_SQ3_4;   //0

   //Bits 4:0 SQ4[4:0]: 4st conversion in regular sequence PC3/ADC1_ADC2_ADC3_IN13
  ADC1->SQR3   |=  ADC_SQR3_SQ4_0;   //1
  ADC1->SQR3   &= ~ADC_SQR3_SQ4_1;   //0
  ADC1->SQR3   |=  ADC_SQR3_SQ4_2;   //1
  ADC1->SQR3   |=  ADC_SQR3_SQ4_3;   //1
  ADC1->SQR3   &= ~ADC_SQR3_SQ4_4;   //0

 NVIC_EnableIRQ (ADC_IRQn);
}

ПДП


Инициализация одного потока ПДП для передачи данных из АЦП в массив (см. выше)

  //Тактирование DMA2
  RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; 
  
 //Stream3 Channel 1 DMA2 - в массив ADC2_array 2 каналa
  
  //Bits 27:25 CHSEL[2:0]: Channel selection (1)
  DMA2_Stream3->CR |= DMA_SxCR_CHSEL_0; //1
  DMA2_Stream3->CR &= ~DMA_SxCR_CHSEL_1; //0
  DMA2_Stream3->CR &= ~DMA_SxCR_CHSEL_2; //0
  
  //Bits 14:13 MSIZE[1:0]: Memory data size (16 bit)
  DMA2_Stream3->CR |=  DMA_SxCR_MSIZE_0; //1
  DMA2_Stream3->CR &= ~DMA_SxCR_MSIZE_1; //0
  
  //Bits 12:11 PSIZE[1:0]: Peripheral data size (16 bit)
  DMA2_Stream3->CR |=  DMA_SxCR_PSIZE_0; //1
  DMA2_Stream3->CR &= ~DMA_SxCR_PSIZE_1; //0
  
  //Bits 10 MINC: Memory increment mode
  DMA2_Stream3->CR |=  DMA_SxCR_MINC;
  
  //Bits 7:6 DIR[1:0]: Data transfer direction (00: Peripheral-to-memory)
  DMA2_Stream3->CR &=  ~DMA_SxCR_DIR_0; //0
  DMA2_Stream3->CR &=  ~DMA_SxCR_DIR_1; //0
  
  //Bits 4 TCIE: Transfer complete interrupt enable
  DMA2_Stream3->CR |=  DMA_SxCR_TCIE;
  
  //Bits 15:0 NDT[15:0]: Number of data items to transfer
  //1000 point x 4 channel
  DMA2_Stream3-> NDTR = 4000;
  
  //Bits 31:0 PAR[31:0]: Peripheral address
  DMA2_Stream3->PAR = (uint32_t) &(ADC2->DR);
  
  //Bits 31:0 M0A[31:0]: Memory 0 address
  DMA2_Stream3->M0AR = (uint32_t) ADC2_array;
  
  //Bits 0 EN: Stream enable / flag stream ready when read low
  DMA2_Stream3->CR |=  DMA_SxCR_EN;
  
  NVIC_EnableIRQ (DMA2_Stream0_IRQn);

Таймеры


Пример конфигурации таймера с комплементарными 12-разрядными ШИМ выходами для управления 3 полумостами (3-фазный инвертор)

  // TIM1 PWM
  RCC -> APB2ENR |= RCC_APB2ENR_TIM1EN; //тактирование TIM1

  TIM1->CR1   |=  TIM_CR1_CMS_0;    //Center-aligned mode 1
  TIM1->CR1   |=  TIM_CR1_ARPE; 
  
  //частота ШИМ
  //прескалер 8 и период 4000 - 3000 Гц
  //частота шины 108 МГц

  TIM1->PSC  = 8;
  TIM1->ARR  = 4000; 

  TIM1->CCR1 = 1000;  //начальные значения
  TIM1->CCR2 = 1000;  
  TIM1->CCR3 = 1000;

  TIM1->CCMR1 &=  ~TIM_CCMR1_OC1M_0; 
  TIM1->CCMR1 |=  TIM_CCMR1_OC1M_1;
  TIM1->CCMR1 |=  TIM_CCMR1_OC1M_2; //110: PWM mode 1
  
  TIM1->CCMR1 &=  ~TIM_CCMR1_OC2M_0;
  TIM1->CCMR1 |=  TIM_CCMR1_OC2M_1;
  TIM1->CCMR1 |=  TIM_CCMR1_OC2M_2; //110: PWM mode 1
  
  TIM1->CCMR2 &=  ~TIM_CCMR2_OC3M_0;
  TIM1->CCMR2 |=  TIM_CCMR2_OC3M_1;
  TIM1->CCMR2 |=  TIM_CCMR2_OC3M_2; //110: PWM mode 1
  
  TIM1->CCER  |=  TIM_CCER_CC1E;   // Capture/Compare 1 output enable
  TIM1->CCER  |=  TIM_CCER_CC1NE;  // Capture/Compare 1 complementary output enable
  TIM1->CCER  |=  TIM_CCER_CC2E;   // Capture/Compare 2 output enable
  TIM1->CCER  |=  TIM_CCER_CC2NE;  // Capture/Compare 2 complementary output enable
  TIM1->CCER  |=  TIM_CCER_CC3E;   // Capture/Compare 3 output enable
  TIM1->CCER  |=  TIM_CCER_CC3NE;  // Capture/Compare 3 complementary output enable

  //DTG[7:0]: Dead-time generator setup  1 mks
  TIM1->BDTR  |= TIM_BDTR_DTG_0;
  TIM1->BDTR  |= TIM_BDTR_DTG_1;
  TIM1->BDTR  |= TIM_BDTR_DTG_2;
  TIM1->BDTR  |= TIM_BDTR_DTG_3;
  TIM1->BDTR  |= TIM_BDTR_DTG_4;
  TIM1->BDTR  |= TIM_BDTR_DTG_5;
  TIM1->BDTR  |= TIM_BDTR_DTG_6;
  TIM1->BDTR  |= TIM_BDTR_DTG_7;

  TIM1->DIER |= TIM_DIER_CC1IE; //Capture/Compare 1 interrupt enable
  //TIM1->DIER |= TIM_DIER_CC2IE; //Capture/Compare 2 interrupt enable
  //TIM1->DIER |= TIM_DIER_CC3IE; //Capture/Compare 3 interrupt enable
  
  TIM1->CR1 |= TIM_CR1_CEN; //Bit 0 CEN: Counter enable
  TIM1->BDTR  |=  TIM_BDTR_MOE;     //MOE: Main output enable
    
  NVIC_EnableIRQ (TIM1_CC_IRQn); //разрешить прерывания от таймера 

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

  // TIM3 100 мсек
  RCC -> APB1ENR |= RCC_APB1ENR_TIM3EN; //TIM3 Timer clock enable
  
  TIM3->CR1 |= TIM_CR1_CEN;   //Bit 0 CEN: Counter enable
  TIM3->CR1 |= TIM_CR1_ARPE;  //Bit 7 ARPE: Auto-reload preload enable
  TIM3->DIER |= TIM_DIER_UIE; //Bit 0 UIE: Update interrupt enable
  
  TIM3->PSC = 2000;
  TIM3->ARR = 5400;
  NVIC_EnableIRQ (TIM3_IRQn); //разрешить прерывания от таймера 
 

Эти файлы конфигурации используются практически без изменения длительное время. Начиналось это все еще на 103 контроллере, сейчас используется на 7 серии :)

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

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


  1. GarryC
    11.09.2017 11:40
    +1

    Офигеть.


  1. datacompboy
    11.09.2017 11:57
    +3

    В чем смысл, Бэрримор?!
    Одноразовый вызов функции или "две строчки", кроме повышенной когнитивной нагрузки как на писателя, так и на будущего читателя — разницы нет.
    Так ради чего?


    1. Dima_Sharihin
      11.09.2017 21:13

      Как человек, недавно писавший bootloader, скажу, что тянуть STM32 HAL в загрузчик — форменное самоубийство. Я в итоге конфигурацию пинов свел последовательности записей в регистры.


      Кстати да, очень жаль, что у ST есть утилита генерации кода, но она не умеет в гиперминималистичный формат "лишь бы заработало"


      1. IronHead
        11.09.2017 21:43
        +1

        Недавно так же писал загрузчик для STM32F4. НА HAL он занял 5 килобайт. Вроде бы много, но вот самый минимальный сектор в начале памяти STM32F4 — 16 килобайт. А меньше 1 сектора — не стереть. Ну и зачем спрашивается экономить?


        1. Dima_Sharihin
          11.09.2017 21:47
          +1

          У STM32F030C8T6 размер страницы — 1 килобайт и 64к всего.
          Так что 5 кило HAL это овердохрена для загрузчика. Я влез в работу с I2C EEPROM, флешкой и tinyAES в 4к и там еще осталось место.


          Ну и зачем спрашивается экономить?

          Вообще моя претензия к HAL не в большом объеме кода, что он занимает(хотя и это тоже), а в чудовищно-отвратительном стиле работы с периферией, когда от тебя ее как бы прячут, но все равно заставляют лезть под капот, чтобы понять, как с ней работать.


          Ну и дескрипторы периферии отдельный прикол, когда они порядком занимают в оперативке, но все то же самое вычитывается из регистров статуса


          1. datacompboy
            11.09.2017 21:51

            Всё же суть HAL в переносимости, а не идиоматической абстрактности.


            1. Dima_Sharihin
              11.09.2017 21:57

              Переносимость путем лишения (порой очень нужного) функционала. К примеру прием по DMA, когда мы заранее не знаем объем пакета, который придет — без HAL это сделать куда проще.


              Ну и о какой переносимости кода работы с Flash можно говорить, когда у разных контроллеров разный размер страницы — насколько я помню, HAL так далеко не прячет периферию.


              Я сам пользуюсь HAL (когда другого нет), но это не тот опыт, которым я наслаждаюсь. STM32-LLL другое дело, ты знаешь как работает периферия и просто указываешь ей, что надо сделать


            1. WFrag
              12.09.2017 02:01
              +1

              Не знаю, то ли я совсем балбес, то ли что, но когда мне потребовалось настроить таймер не самым тривиальным образом (PWM, но с тонкостями типа правильно настроенного preload и прерываний), то я потратил кучу времени на анализ кода HAL, чтобы правильно настроить регистры. Причём я так и не нашёл магическую комбинацию вызовов, которая бы сделала ровно то, что мне нужно.

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

              Переносимость? Я какое-то время метался между F0 и F103 — довольно сильные различия, HAL не сильно помогает. Даже банальные GPIO отличаются.

              На мой взгляд, важнее иметь сильно типизированный API к регистрам периферии. Учитывая, что производитель публикует SVD файлы, которые (в идеале, конечно) описывают все нужные детали, этот типизированный API можно просто генерировать из этих SVD. При этом за счёт сильной типизации уже на этом уровне будут ловиться ошибки типа неправильных битовых масок, и.т.д, т.к все разрешённые значения будут иметь имена и привязку к конкретным позициям в конкретных регистрах.

              Вот, например, как выглядит использование подобного API на Rust. Да, этот код ни разу не переносим, но для одноразового переноса на другой чип мне быстрее будет прочитать документацию по новому чипу и переписать его (и компилятор мне в этом поможет), а для разных целевых платформ я просто сделаю свой HAL, на том уровне абстракции, на котором мне нужно. Как пример, HAL для LCD экрана.


      1. Hoksmur
        12.09.2017 12:43

        Есть ещё вариант. Трудоёмкий в начале. Генерировать своим скриптом, да хоть на том же Python или tcl/tk. Можно с графикой: мышкой расставил что куда надо и оно само тебе исходник выработает. В средах проектирования CPLD/FPGa используется и такой подход.


    1. imwode
      11.09.2017 21:42

      А какая функция в HAL включает, например, пилу на выходе DAC-a?


  1. IronHead
    11.09.2017 12:22
    +3

    Можно и через регистры, но какие будут плюсы?
    Я вижу только минусы:
    — Переносимость кода никакая, по сравнению с HAL. Вот прибежит завтра начальник и скажет: давай свой код, который ты писал по STM32F4 запусти на STM32F2, срок до вечера. И все, копай инициализацию по новой.
    — Читаемость регистрового кода — никакая, нужно постоянно смотреть в ДШ


    1. AlanDrakes
      11.09.2017 19:42

      > — Переносимость кода никакая…
      Иногда случается так, что кот в принципе нельзя впихнуть в кристалл поменьше. Как раз из-за этого самого HAL, который прилично раздувает код и достаточно сильно может его затормозить в критичных моментах. Ну и немного RAM.
      > — Читаемость регистрового кода — никакая
      Зависит от комментариев и предефайнов. Довольно неплохо использовать CMSIS + заголовочник от HAL'а, где значения регистров прописаны уже более понятно.
      Например: FLASH->ACR |= FLASH_ACR_ACC64 | FLASH_ACR_LATENCY | FLASH_ACR_PRFTEN; // Разрешаем читать по 64 бита, увеличиваем латентность доступа и разрешаем работу предвыборки данных

      > нужно постоянно смотреть в ДШ
      Зачастую работа даже с библиотекой HAL не исключает частого окунания в DS.


    1. Dima_Sharihin
      11.09.2017 21:17
      +2

      Внезапно даже с HAL надо смотреть в даташит, чтобы знать, почему не работает чтение RTC из прерывания будильника, или почему передаяа с суффиксом _IT не запускается, пока принудительно не включишь NVIC_EnableIRQ(...).


      Что до современных тенденций, то ST на хрензнаеткакойраз все-таки выкатила более-менее вменяемую библиотеку: STM32 Low-Level Library, которая почти полностью состоит из инлайновых функций модификаций регистровых флагов, которые позволят разрулить большую часть зависимостей на этапе компиляции. Вот только на STM32F4 я ее не нашел, обидно


      1. WFrag
        12.09.2017 02:06

        Да. У меня сильно больше времени ушло на попытку «правильно» использовать HAL, чем я то же самое потом повторил просто на регистрах.

        Простые вещи, типа GPIO, нормально переносятся (хотя я и там словил грабли, т.к не было привычки инициализировать структуры нулями), а вот сложная настройка PWM, master-slave у таймеров, да ещё чтоб правильные выходы были включены и прерывания — вот это ни разу не просто делается. Даже банальный энкодер у меня не завёлся чисто через HAL, какие-то не те параметры они выставляют.


  1. no111u3
    11.09.2017 12:23
    +3

    Помнить регистры между семействами это ещё та задача, а они разные, да да разные регистры, не говоря уже про биты. Удобство подобной манипуляции очень сомнительно ввиду того, что стандартный хидер от стм не даёт никакой информации о том что это к примеру за биты: почему их 3, зачем мне нужны именно они? нет никакой информации о запрещённых комбинациях (а их к слову очень много, и порой можно сделать так и так, но нельзя сделать вот так и ещё 10 раз как).

    Неудобно писать:

    // Alternate function mode
    GPIOA->MODER &= ~GPIO_MODER_MODER8_0; //0
    GPIOA->MODER |= GPIO_MODER_MODER8_1; //1
    


    Зато какое удовольствие писать вот так (код взят из реального проекта, и на выходе по ассемблерному выхлопу он даёт тоже самое что и выше, при одинаковых флагах оптимизации):
    // enable usart3 pins,
        // connect to alternative func 7 (usart)
        hal::gpiod::set_mode<hal::pin_mode::alt_func,
            hal::p8, hal::p9>();
        hal::gpiod::set_alt_func<hal::pin_alt::af7,
            hal::p8, hal::p9>();
    


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


  1. Ocelot
    11.09.2017 12:31
    +3

    Основной смысл библиотек SPL и HAL — не снизить порог вхождения (хотя и это тоже), а отделить друг от друга низкоуровневую и высокоуровневую части ПО.

    Хорошо, железо вы проинициализировали прямой записью в регистры. А обращаться к этому железу в процессе работы так же будете? Нужно в программе поднять ногу порта — пишем

    GPIOA->BSRR = (1<<15);
    в сотне разных мест. А потом понадобилось сменить ногу с 15 на 16, например. Искать все эти 100 мест в коде и менять везде? Конечно, нет. Надо написать функцию-обёртку (или макрос) для управления портом. И ещё пару для UART. И еще штуки три для таймеров. И несколько для DMA…

    Вот и написали собственную библиотеку для работы с периферией. От чего убегали, к тому и пришли.


    1. sanders1967 Автор
      11.09.2017 13:13
      +1

      Для управления пинами обычно использую такой дефайн:
      #define Work_Led_ON GPIOB->BSRR = GPIO_BSRR_BS_2; //Port B Set bit 2
      #define Work_Led_OFF GPIOB->BSRR = GPIO_BSRR_BR_2; //Port B Reset bit 2


      1. no111u3
        11.09.2017 13:22
        +1

        А чем это лучше библиотеки? Ответ: ничем. Так как это мало того что макрос (кстати без изоляции от побочных эффектов). Так и ещё макрос с кучей зафиксированных значений, что опять же снижает понятность кода при отладке и превращает в лапшу итоговый код.


        1. WFrag
          12.09.2017 09:45

          Не согласен.

          В примере выше — это специфичный «HAL на коленке» под задачу. А HAL от STM тебе выдаст вон тот самый HAL_GPIO_WritePin, к которому ты ещё и номер пина будешь таскать.

          И вот как раз использование такого макроса понятнее — «включить LED»/«выключить LED», всё ровно в терминах высокоуровневой задачи. И сам макрос тупее некуда.

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

          Не понимаю, как тут поможет HAL? Ну то есть можно прям в самом макросе HAL вызвать, но это не сильно поможет. Весь остальной код и так уже написан в терминах Work_Led_ON/Work_Led_OFF.


          1. sanders1967 Автор
            12.09.2017 09:49

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


          1. no111u3
            12.09.2017 10:12

            Хорошо, уплыл на другой порт ваш Work_Led, или сместился на другой пин? ваши дальнейшие действия?


            1. WFrag
              12.09.2017 10:14

              В данном случае, идёшь и меняешь код макроса.

              P.S. Плюс код инициализации, но это отдельная песня.


              1. no111u3
                12.09.2017 10:22

                Причём не одного + порт инициализации…
                Итого куча телодвижений. Если подходить к макросам более грамотно то можно добиться смены всего в паре мест, но тоже не то. В случае с библиотекой это будет ровно одно место.
                Код на выхлопе будет аналогичным (сам проверял), но при этом не нужно думать, а всё ли я поправил и на те ли значения поправил. Ещё раз — я не советую использовать HAL или SPL заместо CMSIS, я советую использовать правильные библиотеки которые не дают того оверхеда который вы постоянно везде видите.


                1. sanders1967 Автор
                  12.09.2017 10:32

                  Довольно часто на форумах я вижу вопросы типа: "… я использую такую-то функцию HAL, она выдает не то… что делать?". Вот и ответ… почитать исходные документы, почитать про регистры… :)


                  1. no111u3
                    12.09.2017 10:43

                    «Первое что делает человек узнавший о фьюзах это случайно лочит Атмегу...» То что у людей часто возникают вопросы подобного типа может говорить о многом:
                    1) Человек не читал документацию на библиотеку (а она есть и есть аппноты).
                    2) Человек не внимательно читал документацию и упустил какой-то момент.
                    3) Человек затребовал от библиотеки невозможное (см п 1, 2).
                    и много чего другого.

                    Таких случаев бывает много, также как и случаев когда человек напрямую работает с регистрами. Вот к примеру DMA — он в отличие от таймера не допускает работы в половинами своих регистров, можно только целыми записывать. Где это написано? Правильный ответ нигде — однако если взять HAL или SPL то можно увидеть что там этой проблемы не возникает. Второе — без определённого флага в RTC нельзя просто так побитого менять данные в регистрах времени и даты. Третье — не все NVIC позволяют выставлять прерывания ядра. И таких примеров на низком уровне много.


                    1. sanders1967 Автор
                      12.09.2017 10:45

                      Вы правы, конечно… Люди бывают разные...:)


                1. WFrag
                  12.09.2017 17:04

                  Ой, да прям куча телодвижений.

                  Вот что будет с HAL.

                  Делаешь, например, настройку имя порта + пины для вывдов DATA, EN, R/S LCD экрана и всё хорошо. А потом оказывается, что EN и R/S на другом порту, а ты это не предусмотрел. Придётся вводить ещё один параметр.

                  А потом что? Правильно, DATA пины (4 штуки) оказываются разбиты между двумя портами. И опять вся вот эта идея «меняем в одном месте» летим к чертям.

                  А потом ещё и не забыть нужные подсистемы включить (и, насколько я помню, у разных чипов раскладка периферии по подсистемам разная и HAL это не абстрагирует).

                  Я эти грабли все на своём проекте собирал, пока метался между F0 и F103, разными платами, языками (C -> C++ -> Rust), библиотеками (StdPeriph -> HAL -> Rust), подходами (C++ с шаблонами <-> C++ с классами).

                  Самая жизнеспособная в данном случае абстракция — это «интерфейс» с тремя методами «выставить EN», «выставить R/S», «выставить DATA». А уже что оно там внутри будет делать — пофиг, хоть HAL, хоть не HAL, там кода будет — кот наплакал.

                  P.S. А правильные библиотеки — согласен, как я уже говорил, я переехал на Rust и весьма счастлив. :)


                  1. no111u3
                    12.09.2017 19:20

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


                    1. sanders1967 Автор
                      13.09.2017 09:11

                      Интересно! Может расскажете в 2-ух словах?


                      1. no111u3
                        13.09.2017 10:06

                        Если уж совсем в двух словах — «используй метапрограммирование». А если не в двух то использование шаблонов с вычислением масок и констант во время компиляции. Небольшой фрагмент кода я приводил выше.


                        1. sanders1967 Автор
                          13.09.2017 10:08

                          Понятно :) Спасибо...:)


                        1. WFrag
                          13.09.2017 21:48

                          Тот кусочек кода не раскрывает, чем он принципиально лучше.

                          То есть я согласен, строгая типизация — это важно, я про это писал выше. По этому параметру он, конечно же, лучше.

                          Но меня это в меньшей степени беспокоит, и вот почему.

                          Например, я перенёс свой датчик Холла с TIM1 на TIM2. А порт взял такой, который на второй канал попадает. И может быть, это альтернативный порт для этого канала, и нужно переназначение каналов таймера настраивать. И поставил триггер Шмитта, то есть вход стал инвертированный.

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

                          И если мне и так приходится а) делать свой слой абстракции б) вникать, фактически, до уровня регистров, то в чём существенный плюс (кроме типизации, за которую я «за» обеими руками, о чём я уже писал)?


                          1. no111u3
                            13.09.2017 22:10

                            Ну для остальной периферии пока я пока ещё не реализовал настолько высокоуровневый API уж извините, к сожалению в компайл-тайм не влезает сильный ии. Тот API который я разрабатываю помогает случайно не записать не в тот регистр не те настройки, позволяет выбирать из заранее объявленных констант. Ну и самое главное не накладывает избыточного кода поверх того который может сгенерировать компилятор языка С. Пока более ничего сверхестественного моя библиотека не выполняет. Для части периферии отвязана от записи в регистры напрямую, а также настройки (EXTI, GPIO, USART), но от понимания того что такое USART оно не избавляет да и не должно. Компилятор за вас думать не будет. И не должен. А когда будет тогда не будете нужны вы. Кстати видел я ваш код, он нисколько не помогает оптимизировать ту работу которую делает программист. Потому, что сложные операции должны быть эффективными.


                            1. WFrag
                              13.09.2017 23:30

                              Я примерно про то и говорю. Я просто думаю в контексте Rust, где подобная библиотека генерируется автоматически по SVD файлам. И когда я ещё пытался делать свою прошивку на C/C++, мне именно такая библиотека и была нужна, а не вот этот HAL от STM, который пытается быть чуть более высокоуровневым, но у него это получается не особо.

                              Последний пункт, правда, не понял, про сложные операции. Код на Rust должен компилироваться в прямой доступ к регистрам.


                              1. no111u3
                                14.09.2017 10:36

                                Меня немного смущает фраза «должен компилироваться в прямой доступ к регистрам». Обычно битовые операции разделённые по времени не оптимизируются, поэтому я от них отказался в пользу свёртки во время компиляции набора битовых констант через вариативные шаблоны.


                                1. WFrag
                                  14.09.2017 18:38

                                  Оптимизироваться не будут операции с промежуточным записями в регистр (потому что регистры — это volatile память). А цепочки типа

                                  tim2.ccer.write(|w| w
                                    .cc1p().set_bit()
                                    .cc1e().set_bit());
                                  

                                  без проблем соптимизируются в одну запись константы в регистр (потому что запись делается один раз в конце ccer_write, а установка битов делается в лямбда-функции).

                                  Думаю, в C++ аналогичный код будет ровно так же соптимизирован.


            1. sanders1967 Автор
              12.09.2017 10:14

              Корректируется макрос в файле и все, программа как работала так будет успешно работать...:)
              if (Block_mode)
              {
              Power_Relay_OFF
              }

              Вот пример из реальной программы…


      1. IronHead
        11.09.2017 13:58

        А чем не устраивает HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET);
        ?
        Под капотом

        void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
        {
          /* Check the parameters */
          assert_param(IS_GPIO_PIN(GPIO_Pin));
          assert_param(IS_GPIO_PIN_ACTION(PinState));
        
          if(PinState != GPIO_PIN_RESET)
          {
            GPIOx->BSRR = GPIO_Pin;
          }
          else
          {
            GPIOx->BSRR = (uint32_t)GPIO_Pin << 16U;
          }
        }


        1. sanders1967 Автор
          12.09.2017 09:13

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


    1. WFrag
      12.09.2017 10:03
      +2

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

      HAL строит абстракции вокруг конкретной переферии. «Порты»/«Таймеры»/и.т.д.

      Это может нормально работает для случаев, когда подсистема сильно отделена от всего остального. Не знаю, USB какой-нибудь, что-ли (хотя всегда есть DMA который на себя всё подряд замыкает). Я с такими крупными подсистемами не работал — не знаю. С FLASH работал, там HAL более-менее был удобен.

      А вот когда у тебя подсистема — это хитрым образом сконфигурированный таймер и разные пины на разных портах, HAL тебе никак не поможет. Всё равно придётся писать свой слой абстракции, который будет предлагать API специфичный для подсистемы (например «выставить задержку на следующий цикл PWM»/«установить следующий цикл PWM последним»). И если ты такой слой пишешь, то по моему опыту, настроить регистрами проще и быстрее, чем пытаться найти комбинацию вызовов HAL, которая приведёт к нужному результату.

      И которая, в 90% случаев всё равно не будет работать на другом устройстве, или из-за разницы в чипах, или банально из-за существенной разницы в раскладке на переферию/пины (тут выход PWM был основной пин, там — альтернативный, и.т.д).


      1. sanders1967 Автор
        12.09.2017 10:04

        Согласен :)


  1. roman_vs
    11.09.2017 12:43

    Когда по какой-либо причине не использую библиотеки, для улучшения читаемости кода использую макросы, как, к примеру, предлагает уважаемый ZiB: ziblog.ru/2012/09/09/linii-vvoda-vyivoda-stm32-chast-3.html
    В этом случае мы работаем напрямую с регистрами, но пишем вполне прозрачный код:

    #define PIN_LED	D, 12, LOW, MODE_OUTPUT_PUSH_PULL_PULL_DOWN, SPEED_2MHZ, AF_NO
    // Здесь все просто:	D - порт, 12 - пин, режимы и т. д.
    
    void main(void)
    {
    	PIN_ON(PIN_LED);
    	// И т. д.
    }
    


  1. mkulesh
    11.09.2017 15:19
    +5

    Ни в коем случае не хочу обидеть автора, но меня несколько удивляет методология такого подхода. Автор берет STM32F7 (внимание, контроллер с мощнейней перефирией), показывает, что через регистры можно изменять состоние пинов и управлять таймерами и делает вывод, что таким образом можно делать все. Но ведь это не так. Если я беру STM32F7, то, очевидно, мне нужно много больше, чем мигать светодиодом. Например, RTC (включая все зубодробильную логику работы с датами и временем в разных часовых поясах и високосных/невисокосных годах), SDIO со всей логикой общения с карточкой (включая CRC контроль данных), USB-стек, внешняя память, и все это через DMA. Именно этот функционал и является основной частью HAL, а поморгать светодиодом — это так, базовый уровень. Вы высокоуровневый протокол работы SD карты, FatFS или USB-стек тоже сами будете с нуля переписывать? А если Вам этот слой HAL неинтересен (то есть нет потребности в этом функционале), то зачем Вы берете F7, а, скажем, не Atmega16?


    1. sanders1967 Автор
      11.09.2017 15:39
      +2

      Проекты бывают разные. В моих проектах главная нагрузка на проц — это графический цветной интерфейс. F7 использую, прежде всего, из-за приличного размера памяти и неплохого быстродействия и очень жду H7 ;). На нем смогу увеличить разрешение экрана. RTC, Fat, USB Host & Device давно реализованы и не вызывают никаких проблем. Аппаратный CRC16 не получилось использовать. написал свой для Modbus.
      Спасибо за развернутый комментарий. :)


  1. stml
    11.09.2017 16:23
    +1

    Не по теме, но может кому то пригодится. В очередной рассылке от ST написано что Low-Layer (LL) API теперь доступно для всего STM32 семейства. По сути это тот же SPL, которого нету для STM32F7, а LL есть. Так что интересующимся можно обратить внимание как на современный SPL.
    Более того, у них есть даже специальная утилита, которая берет файлы с кодом на SPL и переименовывает функции в LL аналоги. Недавно помогла в переносе старого SPL проекта с F4 на F7.


    1. Amomum
      11.09.2017 22:55

      Только я было обрадовался выходу LL (учитывая, что SPL больше не поддерживается), как напоролся на баг в функции LL_GPIO_Init для F1. Т.е. даже светодиодом спокойно не помигать.
      А багтрекера у STM так и не завелось.

      Вот за утилитку спасибо, не знал!


  1. Delsian
    11.09.2017 22:24

    Раз уж начали оптимизацию, то не ограничивайтесь половинчатыми решениями, используйте Asm вместо Си


    1. sanders1967 Автор
      12.09.2017 09:16

      А почему тогда не в машинных кодах? ;))) В свое время приходилось в машинных кодах для 8080 проца писать… :)


  1. Nick_Shl
    12.09.2017 07:03

    Да нормальная вещь CubeMX и HAL! Простые вещи — на ура. Нужно посложнее? Используй регистры! Причем регистры можно использовать вместе с HAL используя дескриптор: hadc1.instance->DR


  1. LampTester
    12.09.2017 09:14

    Собственно, я всегда писал и пишу под STM32 без всяких HAL, SPL и Cube. Код получается понятнее и компактнее, а главное — предсказуемее. Уровень абстракции от оборудования у меня всегда самописный.


  1. MasterElectric
    13.09.2017 09:47

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


  1. Make_Pic
    15.09.2017 13:01

    В развитии этой темы — сразу писать на ассемблере!


    1. sanders1967 Автор
      15.09.2017 13:02

      Не нужно останавливаться на полпути — писать нужно в машинных кодах. ;)))