Русский
Русский
English
Статистика
Реклама

Iar

Кому в микроконтроллере жить хорошо?

04.12.2020 00:04:03 | Автор: admin

В каком году рассчитывай, в какой земле угадывай, задачился вопросами. Насколько ARM быстрее AVR? Какая разновидность протокола Modbus более быстрая? ASCII или RTU?

Под быстротой, в данном случае, будем понимать количество машинных циклов процессора необходимых для исполнения всех действий протокола.
Исследование быстродействия будем проводить на, широко известной в узких кругах, библиотеке ModBus Slave RTU/ASCII, портированной на микроконтроллеры ATMega48 и STM32L052. Вывод информации будем осуществлять по протоколу Modbus в эмулятор панели Weintek. Тестирование будем производить на демонстрационном примере. Помимо результатов теста на панель выводятся состояния регистров Modbus: дискретных входов, дискретных выходов, регистров для чтения и регистров для чтения/записи. Также средствами панели производится подсчет количества ошибок обмена данными с микроконтроллером. Внешний вид тестового окна приведен на рисунке.



Оценку быстродействия будем проводить измеряя время выполнения функции обработки сообщений протокола. Измерение времени выполнения будем проводить рабоче-крестьянским способом. Перед запуском функции обнуляем аппаратный таймер, частота счета которого равна тактовой частоте микроконтроллера, после выполнения функции считываем значения таймера и проводим обработку результатов измерений. Вычисляем минимальное, максимальное и среднее значение времени выполнения функции обработки сообщений протокола Modbus.
while(1)    {    TIM6->CNT=0;    ModBusRTU();    //ModBusASCII();    tcurent=TIM6->CNT;    if(tcurent<tmin)tmin=tcurent;    if(tcurent>tmax)tmax=tcurent;    avg32=avg32-(avg32>>16)+tcurent;    tavg=avg32>>alfa;    ...

Результаты исследований, сведены в таблицу. Исследование проводились при различных опциях библиотеки:
  • ModBusUseTableCRC Использовать расчет CRC по таблице;
  • ModBusUseErrMes Использовать сообщения о логических ошибках протокола;

А также при различных стратегиях оптимизации компилятора.
Библиотека ModBus Slave RTU/ASCII поддерживает, в некоторых случаях, важную функцию пауза между получением запроса от Modbus Master и ответом Modbus Slave. Исследование проводились при значениях паузы 2 милисекунды и 0 (то есть без паузы), эти значения указаны в графе таблицы Пауза П/П. В графе Размер указан размер модуля, который включает в себя обе функции обработки сообщений Modbus (ModBusRTU(), ModBusASCII()).



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

Глубоко задумавшись над результатами исследований, можно сделать следующие выводы:
  1. AVR не такой уж медленный!!! В среднем он в полтора раза медленнее ARM при той же тактовой частоте. А в случае оптимизации по размеру (например варианты 13 и 15) практически приближается к ARM.
  2. Протокол ASCII, по сравнению c RTU, не только более медленный по скорости передачи, но и занимает гораздо больше ресурсов микроконтроллера.
  3. Использование сообщений о логических ошибках протокола, никак не влияет на быстродействие.
  4. Табличный метод вычисления CRC позволяет более чем в полтора раза снизить использование вычислительных ресурсов микроконтроллера.
  5. Использование паузы между приемом запроса и передачей ответа, позволяет не только избежать конфликтов на шине RS-485, но и уменьшить блокирующие действие функции обработки сообщений протокола Modbus.


Какие выводы еще можно сделать?

Проект на GitHub


Скачать одним файлом

Подробнее..

Очередная статья STM32 для начинающих

12.09.2020 20:07:24 | Автор: admin
Всех приветствую!
Это моя первая статья на хабре, поэтому прошу не кидаться тяжелыми предметами. Заранее спасибо.
Начнем с предыстории. Когда-то мне пришлось перейти на микроконтроллеры ARM фирмы ST. Это было связано с тем, что PIC и AVR уже не хватало и хотелось новых приключений. Из доступного в хлебобулочных магазинах и большого количества статей о быстром старте выбор пал именно на STM32F100.
Я привык работать в IAR. Да, есть другие IDE, но мне хватает возможности IAR: относительно удобный редактор, не плохой отладчик и достаточно удобно работать с регистрами во время отладки.
Когда я попытался сделать первый проект меня ждало разочарование CMSIS! Кому как, но для меня это было (и остается) ужасом: много буков, длинные и для меня не понятные структуры. Вникать во все это было не интересно. Попытался скомпилировать пару примеров и понял это не наш метод.

Неужели нет других вариантов? Есть. Тот, встроенный в IAR: iostm32f10xx4.h и подобные инклудники. Вполне не плохо:
RCC_APB2ENR_bit.ADC1EN = 1; // включить тактирование ADC

Оставалось это запихнуть в классы и пользоваться. Так и сделал. Через какое-то время потребовалось сделать код для STM32f4xx. И тут снова засада нет инклудиков. Что делать? писать самому. Проанализировал имеющиеся самописные библиотеки решил немного сделать по другому. Вот об этом и будет рассказ.

Начало


Про установку IAR и драйверов для отладчика рассказывать не буду, т.к. здесь ничего нового. У меня стоит IAR 8 с ограниченем кода в 32кБ. Для работы выбран контроллер STM32F103, установленный на плате plue pill.
Запускаем IAR, создаем проект c++, выбираем нужный контроллер
image
Следующий шаг изучение документации. Нас будет интересовать Reference manual RM0008. Там главное внимательно читать.
Вообще, когда я обучал своих работников программированию контроллеров, я давал задание включить светодиод (подключенный к ножке контроллера), использую дебагер, редактирую регистры и читая документацию.

Модуль RCC. Такирование


Про этот модуль обычно забывают. Вспоминают только тогда, когда не получается мигнуть светодиодом.
Запомните! Что бы включить какую-либо периферию, на нее надо подать тактовые импульсы! Без этого никак.
Порты ввода-вывода сидят на шине APB2. Находим в документации регист для упрвления тактированием этой шины, это RCC_APB2ENR:

Чтобы включить тактирование порта C (светодиод как раз припаян к PC13), требуется записать в бит IOPCEN единичку.
Теперь найдем адрес регистра RCC_APB2ENR. Смещение у него 0x18, базовый адрес для регистров RCC 0x40021000.
Чтобы удобно было работать с битами, создадим структуру:
typedef struct{  uint32_t  AFIOEN         : 1;  uint32_t                 : 1;  uint32_t  IOPAEN         : 1;  uint32_t  IOPBEN         : 1;  uint32_t  IOPCEN         : 1;  uint32_t  IOPDEN         : 1;  uint32_t  IOPEEN         : 1;  uint32_t                 : 2;  uint32_t  ADC1EN         : 1;  uint32_t  ADC2EN         : 1;  uint32_t  TIM1EN         : 1;  uint32_t  SPI1EN         : 1;  uint32_t                 : 1;  uint32_t  USART1EN       : 1;  uint32_t                 :17;} RCC_APB2ENR_b;

Чтобы потом не мучаться, сразу перечислим все адреса регистров
enum AddrRCC{  RCC_CR          = 0x40021000,  RCC_CFGR        = 0x40021004,  RCC_CIR         = 0x40021008,  RCC_APB2RSTR    = 0x4002100C,  RCC_APB1RSTR    = 0x40021010,  RCC_AHBENR      = 0x40021014,  RCC_APB2ENR     = 0x40021018,  RCC_APB1ENR     = 0x4002101C,  RCC_BDCR        = 0x40021020,  RCC_CSR         = 0x40021024};

теперь остается написать код для включения периферии
static void EnablePort(uint8_t port_name){  volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);  switch (port_name)  {    case 'A': apb2enr->IOPAEN = 1; break;    case 'a': apb2enr->IOPAEN = 1; break;    case 'B': apb2enr->IOPBEN = 1; break;    case 'b': apb2enr->IOPBEN = 1; break;    case 'C': apb2enr->IOPCEN = 1; break;    case 'c': apb2enr->IOPCEN = 1; break;    case 'D': apb2enr->IOPDEN = 1; break;    case 'd': apb2enr->IOPDEN = 1; break;    case 'E': apb2enr->IOPEEN = 1; break;    case 'e': apb2enr->IOPEEN = 1; break;  }}

При работе с регистрами не забываем про volatile, иначе после оптимизации компилятором долго будем искать ошибки и ругать разработчиков компилятора.
Тоже самое делаем для включения тактирвания другой периферии.
В итоге получился такой класс (не все перечислено)
STM32F1xx_RCC.h
#pragma once#include "stdint.h"namespace STM32F1xx{  class RCC  {  protected:    enum AddrRCC    {      RCC_CR          = 0x40021000,      RCC_CFGR        = 0x40021004,      RCC_CIR         = 0x40021008,      RCC_APB2RSTR    = 0x4002100C,      RCC_APB1RSTR    = 0x40021010,      RCC_AHBENR      = 0x40021014,      RCC_APB2ENR     = 0x40021018,      RCC_APB1ENR     = 0x4002101C,      RCC_BDCR        = 0x40021020,      RCC_CSR         = 0x40021024    };        typedef struct {      uint32_t  HSION          : 1;      uint32_t  HSIRDY         : 1;      uint32_t                 : 1;      uint32_t  HSI_TRIM       : 5;      uint32_t  HSI_CAL        : 8;      uint32_t  HSEON          : 1;      uint32_t  HSERDY         : 1;      uint32_t  HSEBYP         : 1;      uint32_t  CSSON          : 1;      uint32_t                 : 4;      uint32_t  PLLON          : 1;      uint32_t  PLLRDY         : 1;      uint32_t                 : 6;    } RCC_CR_b;    typedef struct {      uint32_t  SW             : 2;      uint32_t  SWS            : 2;      uint32_t  HPRE           : 4;      uint32_t  PPRE1          : 3;      uint32_t  PPRE2          : 3;      uint32_t  ADC_PRE        : 2;      uint32_t  PLLSRC         : 1;      uint32_t  PLLXTPRE       : 1;      uint32_t  PLLMUL         : 4;      uint32_t  USBPRE         : 1;      uint32_t                 : 1;      uint32_t  MCO            : 3;      uint32_t                 : 5;    } RCC_CFGR_b;    typedef struct    {      uint32_t  TIM2EN         : 1;      uint32_t  TIM3EN         : 1;      uint32_t  TIM4EN         : 1;      uint32_t                 : 8;      uint32_t  WWDGEN         : 1;      uint32_t                 : 2;      uint32_t  SPI2EN         : 1;      uint32_t                 : 2;      uint32_t  USART2EN       : 1;      uint32_t  USART3EN       : 1;      uint32_t                 : 2;      uint32_t  I2C1EN         : 1;      uint32_t  I2C2EN         : 1;      uint32_t  USBEN          : 1;      uint32_t                 : 1;      uint32_t  CANEN          : 1;      uint32_t                 : 1;      uint32_t  BKPEN          : 1;      uint32_t  PWREN          : 1;      uint32_t                 : 3;    } RCC_APB1ENR_b;    typedef struct    {      uint32_t  AFIOEN         : 1;      uint32_t                 : 1;      uint32_t  IOPAEN         : 1;      uint32_t  IOPBEN         : 1;      uint32_t  IOPCEN         : 1;      uint32_t  IOPDEN         : 1;      uint32_t  IOPEEN         : 1;      uint32_t                 : 2;      uint32_t  ADC1EN         : 1;      uint32_t  ADC2EN         : 1;      uint32_t  TIM1EN         : 1;      uint32_t  SPI1EN         : 1;      uint32_t                 : 1;      uint32_t  USART1EN       : 1;      uint32_t                 :17;    } RCC_APB2ENR_b;    typedef struct {      uint32_t  DMAEN          : 1;      uint32_t                 : 1;      uint32_t  SRAMEN         : 1;      uint32_t                 : 1;      uint32_t  FLITFEN        : 1;      uint32_t                 : 1;      uint32_t  CRCEN          : 1;      uint32_t                 :25;    } RCC_AHBENR_r;      public:    static void EnablePort(uint8_t port_name)    {      volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);      switch (port_name)      {        case 'A': apb2enr->IOPAEN = 1; break;        case 'a': apb2enr->IOPAEN = 1; break;        case 'B': apb2enr->IOPBEN = 1; break;        case 'b': apb2enr->IOPBEN = 1; break;        case 'C': apb2enr->IOPCEN = 1; break;        case 'c': apb2enr->IOPCEN = 1; break;        case 'D': apb2enr->IOPDEN = 1; break;        case 'd': apb2enr->IOPDEN = 1; break;        case 'E': apb2enr->IOPEEN = 1; break;        case 'e': apb2enr->IOPEEN = 1; break;      }    }    static void DisablePort(char port_name)    {      volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);      switch (port_name)      {        case 'A': apb2enr->IOPAEN = 0; break;        case 'a': apb2enr->IOPAEN = 0; break;        case 'B': apb2enr->IOPBEN = 0; break;        case 'b': apb2enr->IOPBEN = 0; break;        case 'C': apb2enr->IOPCEN = 0; break;        case 'c': apb2enr->IOPCEN = 0; break;        case 'D': apb2enr->IOPDEN = 0; break;        case 'd': apb2enr->IOPDEN = 0; break;        case 'E': apb2enr->IOPEEN = 0; break;        case 'e': apb2enr->IOPEEN = 0; break;      }    }    static void EnableAFIO()    {      volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);      apb2enr->AFIOEN = 1;    }    static void DisableAFIO()    {      volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);      apb2enr->AFIOEN = 0;    }        static void EnableI2C(int PortNumber)    {      switch (PortNumber)      {        case 1:        {          volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);          apb1enr->I2C1EN = 1;          break;        }        case 2:        {          volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);          apb1enr->I2C2EN = 1;          break;        }      }    }    static void EnableUART(int PortNumber)    {      switch (PortNumber)      {        case 1:        {          volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);          apb2enr->USART1EN = 1;          break;        }        case 2:        {          volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);          apb1enr->USART2EN = 1;          break;        }        case 3:        {          volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);          apb1enr->USART3EN = 1;          break;        }      }    }        static void DisableUART(int PortNumber)    {      switch (PortNumber)      {        case 1:        {          volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);          apb2enr->USART1EN = 0;          break;        }        case 2:        {          volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);          apb1enr->USART2EN = 0;          break;        }        case 3:        {          volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);          apb1enr->USART3EN = 0;          break;        }      }    }        static void EnableSPI(int PortNumber)    {      switch (PortNumber)      {        case 1:        {          volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);          apb2enr->SPI1EN = 1;          break;        }        case 2:        {          volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);          apb1enr->SPI2EN = 1;          break;        }      }    }    static void DisableSPI(int PortNumber)    {      switch (PortNumber)      {        case 1:        {          volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);          apb2enr->SPI1EN = 0;          break;        }        case 2:        {          volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);          apb1enr->SPI2EN = 0;          break;        }      }    }        static void EnableDMA()    {      volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR);      ahbenr->DMAEN = 1;    }        static void DisableDMA()    {      volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR);      ahbenr->DMAEN = 0;    }  };}


Теперь можно в main.cpp присоединить файл и пользоваться:
#include "STM32F1xx_RCC.h"using namespace STM32F1xx;int main(){  RCC::EnablePort('c');  return 0;}

Теперь можно и с портами поработать. GPIO


Открываем в документации раздел General-purpose and alternate-function I/Os. Находим Port bit configuration table:

Битами CNF[1:0] задается режим работы порта (аналоговый вход, цифровой вход, выход), биты MODE[1:0] отвечат за скорость работы порта в режиме выход.
Взглянем на регистры GPIOx_CRL и GPIOx_CRH (x=A, B, C,...)

видно, что биты идут последовательно:
CNF[1:0], MODE[1:0]
тогда создадим константы с режимами работы портов
enum mode_e{  ANALOGINPUT             = 0,  INPUT                   = 4,  INPUTPULLED             = 8,  OUTPUT_10MHZ            = 1,  OUTPUT_OD_10MHZ         = 5,  ALT_OUTPUT_10MHZ        = 9,  ALT_OUTPUT_OD_10MHZ     = 13,  OUTPUT_50MHZ            = 3,  OUTPUT_OD_50MHZ         = 7,  ALT_OUTPUT_50MHZ        = 11,  ALT_OUTPUT_OD_50MHZ     = 15,  OUTPUT_2MHZ             = 2,  OUTPUT_OD_2MHZ          = 6,  ALT_OUTPUT_2MHZ         = 10,  ALT_OUTPUT_OD_2MHZ      = 14,  OUTPUT                  = 3,  OUTPUT_OD               = 7,  ALT_OUTPUT              = 11,  ALT_OUTPUT_OD           = 15};

тогда метод для конфигурации будет выглядеть так:
// pin_number - номер портаvoid Mode(mode_e mode){  uint32_t* addr;  if(pin_number > 7)    addr = reinterpret_cast<uint32_t*>(GPIOA_CRH);  else    addr = reinterpret_cast<uint32_t*>(GPIOA_CRL);    int bit_offset;  if(pin_number > 7)    bit_offset = (pin_number - 8) * 4;  else    bit_offset = pin_number * 4;  uint32_t mask = ~(15 << bit_offset);  *addr &= mask;  *addr |= ((int)mode) << bit_offset;}

теперь можно сделать более удобные методы для выбора режима:
    void ModeInput()              { Mode(INPUT);         }    void ModeAnalogInput()        { Mode(ANALOGINPUT);   }    void ModeInputPulled()        { Mode(INPUTPULLED);   }    void ModeOutput()             { Mode(OUTPUT);        }    void ModeOutputOpenDrain()    { Mode(OUTPUT_OD);     }    void ModeAlternate()          { Mode(ALT_OUTPUT);    }    void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); }

В документации находим адреса управляющих регистров для портов и перечислим
enum AddrGPIO{  PortA           = 0x40010800,  GPIOA_CRL       = 0x40010800,  GPIOA_CRH       = 0x40010804,  GPIOA_IDR       = 0x40010808,  GPIOA_ODR       = 0x4001080C,  GPIOA_BSRR      = 0x40010810,  GPIOA_BRR       = 0x40010814,  GPIOA_LCKR      = 0x40010818,  PortB           = 0x40010C00,  PortC           = 0x40011000,  PortD           = 0x40011400,  PortE           = 0x40011800,  PortF           = 0x40011C00,  PortG           = 0x40012000};

Долго думал использовать базовый адрес и смещения или абсолютные адреса. В итоге остановился на последнем. Это добавляет некоторые издержки, но в процессе отладки удобней находить в памяти.
Модернизируем метод
if(pin_number > 7)  addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);else  addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);

Возможно, у кого-то будет глаз дергаться, но красивее пока не придумал.
Чтобы перевести ножку в нужное логическое состояние, достаточно записать соответствующий бит в регистре ODRx. Например, так:
void Set(bool st){  uint32_t* addr;  addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);  if(st)    *addr |= 1 << pin_number;  else  {    int mask = ~(1 << pin_number);    *addr &= mask;  } }

Также для управления состоянием можно воспользоваться регистрами GPIOx_BSRR.
По аналогии делаем методы для считывания состояния порта, методы для конфигурации и инициализации (не забываем включить тактирование). В итоге получился такой класс для работы с портами
STM32F1xx_Pin.h
#pragma once#include <stdint.h>#include "STM32F1xx_RCC.h"namespace STM32F1xx{  class Pin  {  public:    enum mode_e    {      ANALOGINPUT             = 0,      INPUT                   = 4,      INPUTPULLED             = 8,      OUTPUT_10MHZ            = 1,      OUTPUT_OD_10MHZ         = 5,      ALT_OUTPUT_10MHZ        = 9,      ALT_OUTPUT_OD_10MHZ     = 13,      OUTPUT_50MHZ            = 3,      OUTPUT_OD_50MHZ         = 7,      ALT_OUTPUT_50MHZ        = 11,      ALT_OUTPUT_OD_50MHZ     = 15,      OUTPUT_2MHZ             = 2,      OUTPUT_OD_2MHZ          = 6,      ALT_OUTPUT_2MHZ         = 10,      ALT_OUTPUT_OD_2MHZ      = 14,      OUTPUT                  = 3,      OUTPUT_OD               = 7,      ALT_OUTPUT              = 11,      ALT_OUTPUT_OD           = 15    };      private:    enum AddrGPIO    {      PortA           = 0x40010800,      GPIOA_CRL       = 0x40010800,      GPIOA_CRH       = 0x40010804,      GPIOA_IDR       = 0x40010808,      GPIOA_ODR       = 0x4001080C,      GPIOA_BSRR      = 0x40010810,      GPIOA_BRR       = 0x40010814,      GPIOA_LCKR      = 0x40010818,      PortB           = 0x40010C00,      PortC           = 0x40011000,      PortD           = 0x40011400,      PortE           = 0x40011800,      PortF           = 0x40011C00,      PortG           = 0x40012000    };      private:    int   pin_number;    int   PortAddr;      public:    Pin()                               { }    Pin(char port_name, int pin_number) { Init(port_name, pin_number); }    ~Pin()    {      Off();      ModeAnalogInput();    }  public:    void Init(char port_name, int pin_number)    {      this->pin_number = pin_number;      RCC::EnablePort(port_name);      switch (port_name)      {        case 'A': PortAddr = PortA; break;        case 'a': PortAddr = PortA; break;        case 'B': PortAddr = PortB; break;        case 'b': PortAddr = PortB; break;        case 'C': PortAddr = PortC; break;        case 'c': PortAddr = PortC; break;        case 'D': PortAddr = PortD; break;        case 'd': PortAddr = PortD; break;        case 'E': PortAddr = PortE; break;        case 'e': PortAddr = PortE; break;      }    }    void ModeInput()              { Mode(INPUT);         }    void ModeAnalogInput()        { Mode(ANALOGINPUT);   }    void ModeInputPulled()        { Mode(INPUTPULLED);   }    void ModeOutput()             { Mode(OUTPUT);        }    void ModeOutputOpenDrain()    { Mode(OUTPUT_OD);     }    void ModeAlternate()          { Mode(ALT_OUTPUT);    }    void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); }    void NoPullUpDown()    {      uint32_t* addr;      if(pin_number > 7)        addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);      else        addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);      int bit_offset;      if(pin_number > 7)        bit_offset = (pin_number - 8) * 4;      else         bit_offset = pin_number * 4;      int mask = ~((1 << 3) << bit_offset);      *addr &= mask;    }        void Mode(mode_e mode)    {      uint32_t* addr;      if(pin_number > 7)        addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);      else        addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);            int bit_offset;      if(pin_number > 7)        bit_offset = (pin_number - 8) * 4;      else        bit_offset = pin_number * 4;      uint32_t mask = ~(15 << bit_offset);      *addr &= mask;      *addr |= ((int)mode) << bit_offset;    }    void Set(bool st)    {      uint32_t* addr;      addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);      if(st)        *addr |= 1 << pin_number;      else      {        int mask = ~(1 << pin_number);        *addr &= mask;      }     }    void On()    {      uint32_t* addr;      addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);      int bit_offset = pin_number;      *addr |= 1 << bit_offset;    }    void Off()    {      uint32_t* addr;      addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);      int bit_offset = pin_number;      int mask = ~(1 << bit_offset);      *addr &= mask;    }    bool Get()    {      uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR - PortA + PortAddr);      int bit_offset = pin_number;      int mask = (1 << bit_offset);      bool ret_val = (*addr & mask);      return ret_val;    }  };};


Ну что, опробуем:
#include "STM32F1xx_Pin.h"using namespace STM32F1xx;Pin led('c', 13);int main(){  led.ModeOutput();  led.On();  led.Off();  return 0;}

Проходим дебагером и убеждаемся, что светодиод сначала загорается (после led.ModeOutput();), потом гаснет (led.On();) и снова загорается (led.Off();). Это связано с тем, что светодиод подклчен к ножке через линию питания. Поэтому, когда на выводе низкий уровень, светодиод загорается.

Не большие итоги


В данной статье я попытался (надеюсь, получилось) показать как можно немного упростить себе жизнь, сделать код более читаемым. Или наоборот как нельзя делать. Каждый решит сам.
Можно было просто написать враперы для CMSIS, но это не интересно.
Спасибо за уделенное время. Если интересно продолжение дайте знать.
Подробнее..

Продолжение очередной статьи STM32 для начинающих. Интерфейсы

13.09.2020 02:19:21 | Автор: admin
Предыдущая публикация:
Очередная статья: STM32 для начинающих

И как этим пользоваться?


В предыдущей статье создали класс для работы с портами ввода-вывода, проверили. И что дальше? Зачем это все запихивать в класс?
Возьмем для примера простенький опрос кнопок:

Для этой схемы в простейшем случае опрос будет выглядеть так
int GetKey(){  volatile uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR);  uint32_t ret_val = *addr;  return ret_val & 0x0F;}

Но, если в схеме поменять порты, подсоединенные к кнопкам, то придется менять и функцию опроса. И так в каждом проекте. Это не всегда удобно. Хочется один раз написать, протестировать и пользоваться.
Перепишем эту функцию под ранее созданный класс:
int GetKey(Pin* p0, Pin* p1, Pin* p2, Pin* p3){  int ret_val = p0->Get() + (p1->Get() << 1) + (p2->Get() << 2) + (p3->Get() << 3);  return ret_val;}

Остается в главной программе инициализировать порты и передать в функцию:
...using namespace STM32F1xx;Pin key0('a', 0);Pin key1('a', 1);Pin key2('a', 2);Pin key3('a', 3);...int main(){  key0.ModeInput();  key1.ModeInput();  key2.ModeInput();  key3.ModeInput();  int key_code = GetKey(&key0, &key1, &key2, &key3);...  return 0;}

А где же интерфейсы?


А теперь представим, законились контроллеры серии f10x, но есть куча f030. По производительности и количеству выводов хватает, только надо хедер поменять для функции GetKey или воспользоваться #ifdef. Сделать глобальный заголовочный файл, в котором прописть тип используемого контроллера (что то типа #define STM32F030) и нагородить кучу дефайнов. Нет, не для этого создавались языки высокого уровня, что бы путаться в макросах!
Пойдем другой дорогой. Создадим класс, в котором перечислим виртуальные методы, необходимые нам по жизни для работы с портами:
iPin.h
#pragma onceclass iPin{public:  virtual void ModeInput()              = 0;  virtual void ModeAnalogInput()        = 0;  virtual void ModeInputPulled()        = 0;  virtual void ModeOutput()             = 0;  virtual void ModeOutputOpenDrain()    = 0;  virtual void Set(bool st) = 0;  virtual bool Get() = 0;  virtual void Reverse() { Set(!Get());}  void On()              { Set(true);  }  void Off()             { Set(false); }};


(те методы, которые приравнены 0, должны быть определены в производном классе!)
и будем его использовать как базовый в классе Pin:
...#include "iPin.h"...class Pin : public iPin...

тогда функция GetKey чуть изменится:
int GetKey(iPin* p0, iPin* p1, iPin* p2, iPin* p3){  int ret_val = p0->Get() + (p1->Get() << 1) + (p2->Get() << 2) + (p3->Get() << 3);  return ret_val;}

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

И что дальше?


Дальше надо оформить класс для работы с системным таймером. Но это уже в следующей публикации.
Подробнее..

Разработка firmware на С словно игра в бисер. Как перестать динамически выделять память и начать жить

07.04.2021 10:06:45 | Автор: admin

C++ is a horrible language. It's made more horrible by the fact that a lotof substandard programmers use it, to the point where it's much mucheasier to generate total and utter crap with it.

Linus Benedict Torvalds

Собеседование шло уже второй час. Мы наконец-то закончили тягучее и вязкое обсуждение моей скромной персоны, и фокус внимания плавно переполз на предлагаемый мне проект. Самый бойкий из трех моих собеседников со знанием дела и без лишних деталей принялся за его описание. Говорил он быстро и уверенно явно повторяет весь этот рассказ уже не первый раз. По его словам, работа велась над неким чрезвычайно малым, но очень важным устройством на базе STM32L4. Потребление энергии должно быть сведено к минимуму... USART... SPI... ничего необычного, уже неоднократно слышал подобное. После нескольких убаюкивающих фраз собеседник внезапно подался чуть вперед и, перехватив мой сонный взгляд, не без гордости произнес:

А firmware мы пишем на C++! мой будущий коллега заулыбался и откинулся в кресле, ожидая моей реакции на свою провокативную эскападу.

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

У вас есть какие-то опасения? поспешил спросить он с искренней озабоченностью в голосе.

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

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

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

И у нас есть код ревью! встрепенувшись, поспешили добавить хором двое других невероятно квалифицированных члена команды.

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

IAR

Так уж получилось, что мы впервые встретились на этом проекте. "Ну, это же специальный компилятор для железок", наивно думал я, "сработаемся". Не скажу, что я жестоко ошибся и проклял тот день, но использование именно этого компилятора доставляет определенный дискомфорт. Дело в том, что в проекте уже начали внедрение относительно нового стандарта С++17. Я уже потирал потные ладошки, представляя, как перепишу вон то и вот это, как станет невероятно красиво, но IAR может охладить пыл не хуже, чем вид нововоронежской Аленушки.

Новый стандарт реализован для нашего любимого коммерческого компилятора лишь частично, несмотря на все заверения о поддержке всех возможностей новейших стандартов. Например, structured binding declaration совсем не работает, сколько не уговаривай упрямца. Еще IAR весьма нежен и хрупок, какая-нибудь относительно сложная конструкция может довести его до истерики: компиляция рухнет из-за некой внутренней ошибки. Это самое неприятное, поскольку нет никаких подсказок, по какой причине все так неприятно обернулось. Такие провалы огорчают даже сильнее финала Игры престолов.

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

SIL

Для некоторых классов устройств существует такое понятие, как стандарты SIL. Safety integrity level уровень полноты безопасности, способность системы обеспечивать функциональную безопасность.

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

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

std::exception

Несмотря на утверждение Герба Саттера, что без исключений C++ уже перестает быть тем языком, который мы знаем и любим, они были беспощадно выпилены. Немудрено, ведь обычный механизм исключений использует динамическое выделение памяти, что недопустимо в нашем проекте.

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

__cxa_allocate_exception

Название у нее уже какое-то нехорошее, и действительно, выделяет память для объекта исключения и делает это весьма неприятным образом прямо в куче. Вполне возможно эту функцию подменить на собственную реализацию и работать со статическим буфером. Если не ошибаюсь, то в руководстве для разработчиков autosar для с++14 так и предлагают делать. Но есть нюансы. Для разных компиляторов реализация может отличаться, нужно точно знать, что делает оригинальная функция, прежде чем грубо вмешиваться в механизм обработки. Проще и безопаснее от исключений отказаться вовсе.Что и было сделано, и соответствующий флаг гордо реет теперь над компилятором! Только вот стандартную библиотеку нужно будет использовать вдвойне осторожнее, поскольку пересобрать ее с нужными опциями под IAR возможности нет.

std::vector

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

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

template <class T, std::size_t Size>class StaticArray { using ssize_t = int;public: using value_type = T; template <class U> struct rebind {   using other = StaticArray<U, Size>; }; StaticArray() = default; ~StaticArray() = default; template <class U, std::size_t S> StaticArray(const StaticArray<U, S>&); auto allocate(std::size_t n) -> value_type*; auto deallocate(value_type* p, std::size_t n) -> void; auto max_size() const -> std::size_t;};

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

Тут очевиднейший пример использования аллокатора
std::vector<int, StaticArray<int, 100>> v;    v.push_back(1000);std::cout<<"check size "<<v.size()<<std::endl;    v.push_back(2000);std::cout<<"check size "<<v.size()<<std::endl;

Результат выполнения такой программы (скомпилировано GCC) будет следующий:

max_size() -> 100

max_size() -> 100

allocate(1)

check size 1

max_size() -> 100

max_size() -> 100

allocate(2)

deallocate(1)

check size 2

deallocate(2)

std::shared_ptr

Умные указатели, безусловно, хорошая вещь, но нужная ли в bare metal? Требование безопасности, запрещающее динамическую аллокацию памяти, делает использование умных указателей в этой области крайне сомнительным мероприятием.

Конечно, контролировать управление памятью путем использования кастомных аллокаторов вполне возможно. В стандартной библиотеке есть замечательная функция std::allocate_shared, которая создаст разделяемый объект именно там, где мы укажем. Указать же можно самолепным аллокатором примерно такого вида:

template <class Element,           std::size_t Size,           class SharedWrapper = Element>class StaticSharedAllocator {  public:  static constexpr std::size_t kSize = Size;  using value_type = SharedWrapper;  using pool_type = StaticPool<Element, kSize>;  pool_type &pool_;  using ElementPlaceHolder = pool_type::value_type;  template <class U>  struct rebind {    using other = StaticSharedAllocator<Element, kSize, U>;  };  StaticSharedAllocator(pool_type &pool) : pool_{pool} {}  ~StaticSharedAllocator() = default;  template <class Other, std::size_t OtherSize>  StaticSharedAllocator(const StaticSharedAllocator<Other, OtherSize> &other)     : pool_{other.pool_} {}  auto allocate(std::size_t n) -> value_type * {    static_assert(sizeof(value_type) <= sizeof(ElementPlaceHolder));    static_assert(alignof(value_type) <= alignof(ElementPlaceHolder));    static_assert((alignof(ElementPlaceHolder) % alignof(value_type)) == 0u);      return reinterpret_cast<value_type *>(pool_.allocate(n));  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(reinterpret_cast<value_type *>(p), n);  }};

Очевидно, Element тип целевого объекта, который и должен храниться как разделяемый объект. Size максимальное число объектов данного типа, которое можно создать через аллокатор. SharedWrapper это тип объектов, которые будут храниться в контейнере на самом деле!

Конечно, вы знаете, что для работы shared_ptr необходима некоторая дополнительная информация, которую нужно где-то хранить, лучше прямо с целевым объектом вместе. Поэтому для этого аллокатора очень важна структура rebuild. Она используется в недрах стандартной библиотеки, где-то в районе alloc_traits.h, чтобы привести аллокатор к виду, который необходим для работы разделяемого указателя:

using type = typename _Tp::template rebind<_Up>::other;

где _Tp это StaticSharedAllocator<Element, Size>,

_Up это std::_Sp_counted_ptr_inplace<Object, StaticSharedAllocator<Element, Size>, __gnu_cxx::_S_atomic>

К сожалению, это верно только для GCC, в IAR тип будет немного другой, но общий принцип неизменен: нам нужно сохранить немного больше информации, чем содержится в Element. Для простоты тип целевого объекта и расширенный тип должны быть сохранены в шаблонных параметрах. Как вы уже догадались, SharedWrapper и будет расширенным типом, с которым непосредственно работает shared_ptr.

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

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

Еще немного кода для иллюстрации

Сам пул объектов основан на StaticArray аллокаторе. А чего добру пропадать?

template <class Type, size_t Size>struct StaticPool {  static constexpr size_t kSize = Size;  static constexpr size_t kSizeOverhead = 48;  using value_type = std::aligned_storage_t<sizeof(Type)+kSizeOverhead,                                             alignof(std::max_align_t)>;  StaticArray<value_type, Size> pool_;    auto allocate(std::size_t n) -> value_type * {    return pool_.allocate(n);  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(p, n);  }};

А теперь небольшой пример, как это все работает вместе:

struct Object {  int index;};constexpr size_t kMaxObjectNumber = 10u;StaticPool<Object, kMaxObjectNumber> object_pool {};StaticSharedAllocator<Object, kMaxObjectNumber> object_alloc_ {object_pool};std::shared_ptr<Object> MakeObject() {  return std::allocate_shared<Object>(object_alloc_);}

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

std::function

Универсальная полиморфная обертка над функциями или функциональными объектами. Очень удобная штука. Точно была бы полезна в embedded проекте, хотя бы для каких-нибудь функций обратного вызова (callbacks).

Чем мы платим за универсальность?

Во-первых, std::function может использовать динамическую аллокацию памяти.

Небольшой и несколько искусственный пример:

int x[] = {1, 2, 3, 4, 5};    auto sum = [=] () -> int {      int sum = x[0];      for (size_t i = 1u; i < sizeof(x) / sizeof(int); i++) {        sum += x[i];      }      return sum;    };        std::function<int()> callback = sum; 

Когда элементов массива 5, то размер функции 20 байт. В этом случае, когда мы присваиваем переменной callback экземпляр нашей лямбда-функции, будет использована динамическая аллокация.

Дело в том, что в классе нашей универсальной обертки содержится небольшой участок памяти (place holder), где может быть определена содержащаяся функция.

Любая функция в С++ может быть определена с помощью двух указателей максимум. Для свободных функций или функторов достаточно одного указателя, если нужно вызвать метод класса, то нужен указатель на объект и смещение внутри класса. Собственно, у нас есть небольшое укромное местечко для пары указателей. Конечно, небольшие функциональные объекты можно хранить прямо на месте этих указателей! Если размер лямбды, например, не позволяет целиком запихать ее туда, то на помощь снова придет динамическая аллокация.

Для GCC

Опции -specs=nano.specs уже не будет хватать для std::function.

Сразу появится сообщения подобного вида:

abort.c:(.text.abort+0xa): undefined reference to _exit

signalr.c:(.text.killr+0xe): undefined reference to _kill

signalr.c:(.text.getpidr+0x0): undefined reference to _getpid

Правильно, ведь пустая функция должна бросать исключение.

Нужна другая опция -specs=nosys.specs, где включены все необходимые заглушки для всяких системных функций.

Соберем небольшую прошивку, чтоб проверить как повлияет включение std::function на потребление памяти различных видов. Прошивка стандартный пример от ST для подмигивающего светодиода. Изменения в размере секций файла-прошивки в таблице:

text

data

bss

67880

2496

144

Невооруженным взглядом видно, что секция .text выросла просто фантастически (на 67Кб!). Как одна функция могла сделать такое?

Внутри std::function явно вызываются исключения, которые внутри себя используют demangling для имен функций, и это дорогое удовольствие. Нужно ли это в небольших устройствах? Скорее всего, любоваться красивыми именами не придется.

Если заглянуть в получившийся elf-файл, то можно увидеть много новых символов. Отсортируем их по размеру и посмотрим на самые жирные.

00000440cplus_demangle_operators0000049e__gxx_personality_v0000004c4 d_encoding000004fed_exprlist00000574_malloc_r0000060cd_print_mod000007f0d_type00000eec_dtoa_r00001b36_svfprintf_r0000306cd_print_comp

Много функций с префиксом d_* функции из файла cp-demangle.c библиотеки libiberty, которая, как я понимаю, встроена в gcc, и не так просто выставить ее за дверь.

Также имеются функции для обработки исключений (bad_function_call, std::unexpected, std::terminate)

_sbrk, malloc, free функции для работы с динамическим выделением памяти.

Результат ожидаемый флаги -fno-exceptions и -fno-rtti не спасают.

Внедрим второй подобный функциональный объект в другой единице трансляции:

text

data

bss

67992

2504

144

Вторая std::function обошлась не так уж и дорого.

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

Для случая без std::function список короткий
libc_nano.alibg_nano.alibg_nano.a(lib_a-exit.o)libg_nano.a(lib_a-exit.o) (_global_impure_ptr)libg_nano.a(lib_a-impure.o)libg_nano.a(lib_a-init.o)libg_nano.a(lib_a-memcpy-stub.o)libg_nano.a(lib_a-memset.o)libgcc.alibm.alibstdc++_nano.a
Для случая с std::function список гораздо длиннее
libc.alibg.alibg.a(lib_a-__atexit.o)libg.a(lib_a-__call_atexit.o)libg.a(lib_a-__call_atexit.o) (__libc_fini_array)libg.a(lib_a-__call_atexit.o) (atexit)libg.a(lib_a-abort.o)libg.a(lib_a-abort.o) (_exit)libg.a(lib_a-abort.o) (raise)libg.a(lib_a-atexit.o)libg.a(lib_a-callocr.o)libg.a(lib_a-closer.o)libg.a(lib_a-closer.o) (_close)libg.a(lib_a-ctype_.o)libg.a(lib_a-cxa_atexit.o)libg.a(lib_a-cxa_atexit.o) (__register_exitproc)libg.a(lib_a-dtoa.o)libg.a(lib_a-dtoa.o) (_Balloc)libg.a(lib_a-dtoa.o) (__aeabi_ddiv)libg.a(lib_a-exit.o)libg.a(lib_a-exit.o) (__call_exitprocs)libg.a(lib_a-exit.o) (_global_impure_ptr)libg.a(lib_a-fclose.o)libg.a(lib_a-fflush.o)libg.a(lib_a-findfp.o)libg.a(lib_a-findfp.o) (__sread)libg.a(lib_a-findfp.o) (_fclose_r)libg.a(lib_a-findfp.o) (_fwalk)libg.a(lib_a-fini.o)libg.a(lib_a-fputc.o)libg.a(lib_a-fputc.o) (__retarget_lock_acquire_recursive)libg.a(lib_a-fputc.o) (__sinit)libg.a(lib_a-fputc.o) (_putc_r)libg.a(lib_a-fputs.o)libg.a(lib_a-fputs.o) (__sfvwrite_r)libg.a(lib_a-freer.o)libg.a(lib_a-fstatr.o)libg.a(lib_a-fstatr.o) (_fstat)libg.a(lib_a-fvwrite.o)libg.a(lib_a-fvwrite.o) (__swsetup_r)libg.a(lib_a-fvwrite.o) (_fflush_r)libg.a(lib_a-fvwrite.o) (_free_r)libg.a(lib_a-fvwrite.o) (_malloc_r)libg.a(lib_a-fvwrite.o) (_realloc_r)libg.a(lib_a-fvwrite.o) (memchr)libg.a(lib_a-fvwrite.o) (memmove)libg.a(lib_a-fwalk.o)libg.a(lib_a-fwrite.o)libg.a(lib_a-impure.o)libg.a(lib_a-init.o)libg.a(lib_a-isattyr.o)libg.a(lib_a-isattyr.o) (_isatty)libg.a(lib_a-locale.o)libg.a(lib_a-locale.o) (__ascii_mbtowc)libg.a(lib_a-locale.o) (__ascii_wctomb)libg.a(lib_a-locale.o) (_ctype_)libg.a(lib_a-localeconv.o)libg.a(lib_a-localeconv.o) (__global_locale)libg.a(lib_a-lock.o)libg.a(lib_a-lseekr.o)libg.a(lib_a-lseekr.o) (_lseek)libg.a(lib_a-makebuf.o)libg.a(lib_a-makebuf.o) (_fstat_r)libg.a(lib_a-makebuf.o) (_isatty_r)libg.a(lib_a-malloc.o)libg.a(lib_a-mallocr.o)libg.a(lib_a-mallocr.o) (__malloc_lock)libg.a(lib_a-mallocr.o) (_sbrk_r)libg.a(lib_a-mbtowc_r.o)libg.a(lib_a-memchr.o)libg.a(lib_a-memcmp.o)libg.a(lib_a-memcpy.o)libg.a(lib_a-memmove.o)libg.a(lib_a-memset.o)libg.a(lib_a-mlock.o)libg.a(lib_a-mprec.o)libg.a(lib_a-mprec.o) (_calloc_r)libg.a(lib_a-putc.o)libg.a(lib_a-putc.o) (__swbuf_r)libg.a(lib_a-readr.o)libg.a(lib_a-readr.o) (_read)libg.a(lib_a-realloc.o)libg.a(lib_a-reallocr.o)libg.a(lib_a-reent.o)libg.a(lib_a-s_frexp.o)libg.a(lib_a-sbrkr.o)libg.a(lib_a-sbrkr.o) (_sbrk)libg.a(lib_a-sbrkr.o) (errno)libg.a(lib_a-signal.o)libg.a(lib_a-signal.o) (_kill_r)libg.a(lib_a-signalr.o)libg.a(lib_a-signalr.o) (_getpid)libg.a(lib_a-signalr.o) (_kill)libg.a(lib_a-sprintf.o)libg.a(lib_a-sprintf.o) (_svfprintf_r)libg.a(lib_a-stdio.o)libg.a(lib_a-stdio.o) (_close_r)libg.a(lib_a-stdio.o) (_lseek_r)libg.a(lib_a-stdio.o) (_read_r)libg.a(lib_a-strcmp.o)libg.a(lib_a-strlen.o)libg.a(lib_a-strncmp.o)libg.a(lib_a-strncpy.o)libg.a(lib_a-svfiprintf.o)libg.a(lib_a-svfprintf.o)libg.a(lib_a-svfprintf.o) (__aeabi_d2iz)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpeq)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpun)libg.a(lib_a-svfprintf.o) (__aeabi_dmul)libg.a(lib_a-svfprintf.o) (__aeabi_dsub)libg.a(lib_a-svfprintf.o) (__aeabi_uldivmod)libg.a(lib_a-svfprintf.o) (__ssprint_r)libg.a(lib_a-svfprintf.o) (_dtoa_r)libg.a(lib_a-svfprintf.o) (_localeconv_r)libg.a(lib_a-svfprintf.o) (frexp)libg.a(lib_a-svfprintf.o) (strncpy)libg.a(lib_a-syswrite.o)libg.a(lib_a-syswrite.o) (_write_r)libg.a(lib_a-wbuf.o)libg.a(lib_a-wctomb_r.o)libg.a(lib_a-writer.o)libg.a(lib_a-writer.o) (_write)libg.a(lib_a-wsetup.o)libg.a(lib_a-wsetup.o) (__smakebuf_r)libgcc.alibgcc.a(_aeabi_uldivmod.o)libgcc.a(_aeabi_uldivmod.o) (__aeabi_ldiv0)libgcc.a(_aeabi_uldivmod.o) (__udivmoddi4)libgcc.a(_arm_addsubdf3.o)libgcc.a(_arm_cmpdf2.o)libgcc.a(_arm_fixdfsi.o)libgcc.a(_arm_muldf3.o)libgcc.a(_arm_muldivdf3.o)libgcc.a(_arm_unorddf2.o)libgcc.a(_dvmd_tls.o)libgcc.a(_udivmoddi4.o)libgcc.a(libunwind.o)libgcc.a(pr-support.o)libgcc.a(unwind-arm.o)libgcc.a(unwind-arm.o) (__gnu_unwind_execute)libgcc.a(unwind-arm.o) (restore_core_regs)libm.alibnosys.alibnosys.a(_exit.o)libnosys.a(close.o)libnosys.a(fstat.o)libnosys.a(getpid.o)libnosys.a(isatty.o)libnosys.a(kill.o)libnosys.a(lseek.o)libnosys.a(read.o)libnosys.a(sbrk.o)libnosys.a(write.o)libstdc++.alibstdc++.a(atexit_arm.o)libstdc++.a(atexit_arm.o) (__cxa_atexit)libstdc++.a(class_type_info.o)libstdc++.a(cp-demangle.o)libstdc++.a(cp-demangle.o) (memcmp)libstdc++.a(cp-demangle.o) (realloc)libstdc++.a(cp-demangle.o) (sprintf)libstdc++.a(cp-demangle.o) (strlen)libstdc++.a(cp-demangle.o) (strncmp)libstdc++.a(del_op.o)libstdc++.a(del_ops.o)libstdc++.a(eh_alloc.o)libstdc++.a(eh_alloc.o) (std::terminate())libstdc++.a(eh_alloc.o) (malloc)libstdc++.a(eh_arm.o)libstdc++.a(eh_call.o)libstdc++.a(eh_call.o) (__cxa_get_globals_fast)libstdc++.a(eh_catch.o)libstdc++.a(eh_exception.o)libstdc++.a(eh_exception.o) (operator delete(void*, unsigned int))libstdc++.a(eh_exception.o) (__cxa_pure_virtual)libstdc++.a(eh_globals.o)libstdc++.a(eh_personality.o)libstdc++.a(eh_term_handler.o)libstdc++.a(eh_terminate.o)libstdc++.a(eh_terminate.o) (__cxxabiv1::__terminate_handler)libstdc++.a(eh_terminate.o) (__cxxabiv1::__unexpected_handler)libstdc++.a(eh_terminate.o) (__gnu_cxx::__verbose_terminate_handler())libstdc++.a(eh_terminate.o) (__cxa_begin_catch)libstdc++.a(eh_terminate.o) (__cxa_call_unexpected)libstdc++.a(eh_terminate.o) (__cxa_end_cleanup)libstdc++.a(eh_terminate.o) (__gxx_personality_v0)libstdc++.a(eh_terminate.o) (abort)libstdc++.a(eh_throw.o)libstdc++.a(eh_type.o)libstdc++.a(eh_unex_handler.o)libstdc++.a(functional.o)libstdc++.a(functional.o) (std::exception::~exception())libstdc++.a(functional.o) (vtable for __cxxabiv1::__si_class_type_info)libstdc++.a(functional.o) (operator delete(void*))libstdc++.a(functional.o) (__cxa_allocate_exception)libstdc++.a(functional.o) (__cxa_throw)libstdc++.a(pure.o)libstdc++.a(pure.o) (write)libstdc++.a(si_class_type_info.o)libstdc++.a(si_class_type_info.o) (__cxxabiv1::__class_type_info::__do_upcast(__cxxabiv1::__class_type_info const*, void**) const)libstdc++.a(si_class_type_info.o) (std::type_info::__is_pointer_p() const)libstdc++.a(tinfo.o)libstdc++.a(tinfo.o) (strcmp)libstdc++.a(vterminate.o)libstdc++.a(vterminate.o) (__cxa_current_exception_type)libstdc++.a(vterminate.o) (__cxa_demangle)libstdc++.a(vterminate.o) (fputc)libstdc++.a(vterminate.o) (fputs)libstdc++.a(vterminate.o) (fwrite)

А что IAR?

Все устроено немного иначе. Он не требует явного указания спецификации nano или nosys, ему не нужны никакие заглушки. Этот компилятор все знает и сделает все в лучшем виде, не нужно ему мешать.

text

ro data

rw data

2958

38

548

О, добавилось всего-то каких-то жалких 3Кб кода! Это успех. Фанат GCC во мне заволновался, почему так мало? Смотрим, что же добавил нам IAR.

Добавились символы из двух новых объектных файлов:

dlmalloc.o 1'404 496

heaptramp0.o 4

Конечно же, появились функции и данные для работы с динамической памятью, как в случае GCC.

Естественно, никаких выделений в куче нет, но IAR приготовился: видно, что он создал структуру gm (global malloc: a malloc_state holds all of the bookkeeping for a space) и некоторые функции для работы с этой структурой.

Объектный файл того юнита, в котором была добавлена функция, тоже ощутимо располнел:

до

main.cpp.obj 3'218 412 36'924

после

main.cpp.obj 4'746 451 36'964

Файл прибавил более 1Кб. Появилась std::function, ее сопряжение с лямбдой, аллокаторы.

Добавление второго такого функционального объекта в другую единицу трансляции дает нам очередной прирост:

text

ro data

rw data

3 998

82

600

Прибавили более 1Кб. Т.е. каждая новая функция добавляет нам по килобайту кода в каждой единице трансляции. Это не слишком помогает экономить: в проекте не один и не два колбэка, больше десятка наберется. Хорошо, что большинство таких функций имеют сигнатуру void(*)(void) или void(*)(uint8_t *, int), мы можем быстро накидать свою реализацию std::function без особых проблем. Что я и сделал.

Убедившись, что моя примитивная реализация function работает и не требует много памяти, перенес ее в проект, заменив все std::function, до которых смог дотянуться. С чувством выполненного долга я отправился на набережную любоваться закатом.

Дома меня поджидало письмо от коллеги, преисполненное благодарности. Он писал, что благодаря отказу от богомерзких std::function проект сильно схуднул, мы все молодцы! Сочившееся из меня самодовольство брызнуло во все стороны. Прилагался также классический рекламно-наглядный график до-после, вопивший об уменьшении размера отладочной версии прошивки аж на 30 процентов. В абсолютных величинах цифра была еще страшнее, это, на минуточку, целых 150 килобайт! Что-о-о-о? Улыбка довольного кота медленно отделилась от лица и стремительным домкратом полетела вниз, пробивая перекрытия. В коде просто нет столько колбэков, чтоб хоть как-то можно было оправдать этот странный феномен. В чем дело?

Смотря на сонное спокойствие темной улицы, раскинувшейся внизу, я твердо решил, что не сомкну глаз, пока не отыщу ответ. Проснувшись утром, в первую очередь сравнил два разных elf-файла: до и после замены std::function. Тут все стало очевидно!

В одном забытом богом и кем-то из разработчиков заголовочном файле были такие строчки:

using Handler = std::function<void()>;static auto global_handlers = std::pair<Handler, Handler> {};

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

Понятно, чего хотел добиться неизвестный мне автор, и это вполне могло получиться. Начиная с 17-го стандарта, в заголовочном файле можно разместить некие глобальные объекты, которые будут видны и в других единицах трансляции. Достаточно вместо static написать inline. Это работает даже для IAR. Впрочем, я не стал изменять себе и просто все убрал.

Вот тут я все же не удержатся от объяснения очевидных вещей

Если у вас несколько единиц трансляции и создание глобального объекта вынесено в заголовочный файл, то при сборке проекта вы неизбежно получите ошибку multiple definition. Ежели добавить static, как сделал неизвестный мне разработчик, то все пройдет гладко, но в итоге будет занято несколько участков памяти и от глобальности ничего не останется.

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

// a.h

#pragma once

int a();

// a.cpp

#include "a.h"

#include "c.hpp"

int a() { return cglob * 2; }

// b.h

#pragma once

int b();

// b.cpp

#include "b.h"

#include "c.hpp"

int b() { return cglob * 4; }

// main.cpp

#include "a.h"

#include "b.h"

int main() { return a() + b(); }

// c.hpp

#pragma once

int c_glob = 0;

Пробуем собрать наш небольшой и бесполезный проект.

$ g++ a.cpp b.cpp main.cpp -o test

/usr/lib/gcc/x8664-pc-cygwin/10/../../../../x8664-pc-cygwin/bin/ld: /tmp/cccXOcPm.o:b.cpp:(.bss+0x0): повторное определение cglob; /tmp/ccjo1M9W.o:a.cpp:(.bss+0x0): здесь первое определение

collect2: ошибка: выполнение ld завершилось с кодом возврата 1

Неожиданно получаем ошибку. Так, теперь меняем содержимое файла c.hpp:

static int c_glob = 0;

Вот теперь все собирается! Полюбуемся на символы:

$ objdump.exe -t test.exe | grep glob | c++filt.exe

[ 48](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000000 c_glob

[ 65](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000010 c_glob

Вот и второй лишний символ, что и требовалось доказать.

А ежели изменить c.hpp таким образом:

inline int c_glob = 0;

Объект c_glob будет единственным, все единицы трансляции будут ссылаться на один и тот же объект.

Вывод будет весьма банален: нужно понимать, что делаешь... и соответствовать стандартам SIL!

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

Всем спасибо, всем удачи!

Подробнее..

Сравнение компиляторов ARMCC, IAR и GCC

13.11.2020 00:08:03 | Автор: admin
image

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

Представляю вашему вниманию небольшое сравнение.

Для теста я сделал проект в Cube MX, который включает в себя USB_DEVICE и Mass Storage Class. Это довольно большие библиотеки для теста.

Получившийся main.c выглядит примерно так:

int main() {  HAL_Init();  SystemClock_Config();  MX_GPIO_Init();  MX_USB_DEVICE_Init();}

Дефайн USBD_DEBUG_LEVEL установлен в 0, чтобы отладочные сообщения USB стека не требовали наличия printf

Подопытные компиляторы:

  • IAR EWARM 8.32.1
  • arm-none-eab-gcc 7-2018-q2-update (среда STM32 Cube IDE 1.4.2)
  • ARMCC v5.06 update 7 (среда Keil uVision 5.32)
  • ARMCC v6.14.1 (среда Keil uVision 5.32)

Настройки IAR:

  • Оптимизация по размеру
  • Run-time библиотека NORMAL
  • Без low level IO (отключен printf)
  • Включены оптимизации линкера: Inline small routines, merge duplicate sections

Настройки GCC:

  • Reduced runtime library --specs=nano.specs
  • Optimize for size -Os
  • Place functions in their own sections --ffunction-sections
  • Place data in their own sections --ffdata-sections
  • Discard unused sections -Wl, --gc-sections

Настройки armcc5:

  • Use micro lib
  • Use cross module optimization
  • Optimization -O3
  • One ELF section per function --split_sections

Настройки armcc6:

  • Use micro lib
  • Optimization image size -Oz
  • One ELF section per function --split_sections


GCC armcc5 IAR armcc6
Размер прошивки 14036 13548 12997 12984


Как видно, armcc6 на самую малость лучше IAR. За ним идет armcc5 с отставанием на 4%, а gcc отстает от лидера на 8%.

Надо отметить, что опция KEIL Use cross module optimization Значительно увеличила время компиляции, но ни чуть не уменьшила размер кода.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru