Хабрахабр

Можно ли использовать С++ вместо Си для небольших проектов в микроконтроллерах

Существует мнение, что использование С++ при разработке программного обеспечения для микроконтроллеров это как стрельба из пушки по воробьям. Мол код получается большого размера и неповоротливый, а мы привыкли бороться за каждый бит в ОЗУ или ПЗУ. И действительно, ведь язык Си был задуман как альтернатива ассемблеру, код должен был быть такой же компактный и быстрый, а читаемость и удобство разработки позволять легко писать довольно большие программы. Но ведь когда-то и разработчики на ассемблере говорили тоже самое про С, с тех пор утекло много воды и программистов, использующих только ассемблер, можно по пальцам пересчитать. Конечно, ассемблер еще играет важную роль в разработке кода для быстрых параллельных вычислений, написании ОСРВ, но это скорее исключение из правил. Так же как когда-то Си пробивал себе дорогу в качестве стандарта для встроенного ПО, так и язык С++ уже вполне может заменить Си в этой области. Современный С++ стандарта С++14 имеет достаточно средств для того, чтобы создавать компактный код, и не уступает по эффективности коду, созданному на Си, а порой, благодаря нововведениям, может быть и рациональнее. Ниже приведен код поиска наименьшего числа в массиве из 5 целых чисел на двух языках Си и С++ на компиляторе IAR for ARM 8.20.
Код на Си, занимающий 156 байт

 int main(void) { int testArray[5U] = {-1, 20, -3, 0, 4}; int lowest = 0; for (int i = 0; i < 5; i++) { lowest = ((lowest < testArray[i]) ? lowest : testArray[i]); };
return 0;

И его ассемблерное представление

image

И код на С++, занимающий 152 байт

int main() { int testArray[5U] = {-1, 20, -3, 0, 4}; int lowest = 0; for (auto it: testArray) { lowest = ((lowest < it) ? lowest : it); };
return 0;

И его ассемблерное представление

image

Как можно увидеть сгенерированный компилятором код на С++ на 4 байт меньше, а скорость работы на 12 тактов быстрее. Все это достигается за счет новых возможностей С++14. Конечно, можно заметить, что это очень синтетический тест, который не имеет ничего общего с реальной реализацией, но все же можно сказать, что не все так однозначно, а данный пример скорее всего не единственный.
Учитывая особенности программирования для микроконтроллеров. Требования к небольшому объему памяти программ 32,64..512 кБ, еще меньшему объему ОЗУ и низкой частоты микропроцессоров( особенно при использовании для низкопотребляющих датчиков), накладывают свои ограничения. И с уверенностью можно сказать, что не все фишки С++ полезны. Такие полезные в большом мире С++ вещи как исключения, можно с увернностью выкинуть из проектов для небольших микроконтроллеров, поскольку они требуют значительного увеличения размера стека и кода, для хранения информации об обработчике исключения и дальнейшем его поиске. Поэтому я попытаюсь рассказать как можно использовать С++ и его новые особенности для небольших проектов, и постараюсь показать, что с без зазрения совести С++ можно использовать вместо Си.
Первым делом надо определиться с задачей. Она должна быть достаточно простой, но и достаточно показательной, чтобы увидеть как можно, например, полностью отказаться от макросов, по-возможности уйти от указателей, уменьшить риски глупых ошибок и так далее…
Выбор, как обычно пал на светодиды.
Для того, чтобы читатель понимал, что мы хотим сделать, я приведу конечный вариант задачи, которую необходимо реализовать на микроконтроллере:

  • Используемая плата XNUCLEO-F411RE www.waveshare.com/wiki/XNUCLEO-F411RE
  • ПО должно работать на микропроцессоре STMF411R, работающего от внешней частоты 16 Мгц.
  • ПО должно поддерживать управление 4 светодиодами на плате, подключенных к портам (Светодиод 1 – GPIOA.5, Светодиод 2 – GPIOC.9, Светодиод 3 – GPIOC.8, Светодиод 4 – GPIOC.5).
  • ПО должно поддерживать 3 режима управления светодиодами (Елочка – все светодиоды загораются поочерёдно, потом в таком же порядке поочередно потухают. Шахматы – вначале загораются четные светодиоды и гаснут нечетные, затем наоборот. Режим Все – все светодиоды загораются и затем гаснут ). Время смены состояния светодиодов – 1 секунда.
  • ПО должно поддерживать смену режима управления светодиодами с помощью кнопки, подключенной к порту GPIOC.13 в кольцевом порядке в последовательности Елочка-Шахматы-Все.

Так выглядит конечное требование от заказчика. Но как это бывает на практике, вначале заказчик пришел с идеей попроще, он решил, что для полного счастья ему не хватает яркой индикации, а именно моргания зеленым светодиодом раз в 1 секунду. Эту задачу и бросился реализовать программист с именем Снежинка.
image
Итак, на нашей плате есть 4 светодиода: LED1, LED2, LED3 и LED4. Они подключены к портам GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9 соответственно. Пока давайте будем работать с LED1, который находится на GPIOA.5.
Для начала программист Снежинка написал вот такой вот простой код на Си, который будет переключать светодиод. Выглядит это так:

int main() { GPIOC->ODR ^= GPIO_ODR_OD5; //переключаем состояние светодиода LED1 на противоположное Delay(1000U); GPIOC->ODR ^= GPIO_ODR_OD5; //еще раз, чтобы моргнуть светодиодом return 0;
}

Код работает хорошо и правильно, Снежинка, остался доволен своей работой и пошел отдыхать. Но для непосвящённого в тонкости разводки платы и булевые операции пользователя, этот код не совсем понятен, поэтому Снежинке пришлось дописывать комментарии, которые поясняют, что на порте GPIOA.5 находится светодиод и собственно мы хотим его переключить.
Давай подумаем, как должен выглядеть такой код на человеческом языке. Может быть так:
Toggle Led1 then
Delay 1000ms then
Toggle Led1

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

int main() { Led Led1(*GPIOA, 5U); Led1.Toggle(); Delay(1000U); Led1.Toggle(); return 0;
}

Полный код

startup.cpp


#pragma language = extended
#pragma segment = "CSTACK"
extern "C" void __iar_program_start( void ); class DummyModule {
public: static void handler();
}; typedef void( *intfunc )( void );
//cstat !MISRAC++2008-9-5-1
typedef union { intfunc __fun; void * __ptr; } intvec_elem; #pragma location = ".intvec"
//cstat !MISRAC++2008-0-1-4_b !MISRAC++2008-9-5-1
extern "C" const intvec_elem __vector_table[] =
{ { .__ptr = __sfe( "CSTACK" ) }, __iar_program_start, DummyModule::handler, DummyModule::handler, DummyModule::handler, DummyModule::handler, DummyModule::handler, 0, 0, 0, 0, DummyModule::handler, DummyModule::handler, 0, DummyModule::handler, DummyModule::handler, //External Interrupts DummyModule::handler, //Window Watchdog DummyModule::handler, //PVD through EXTI Line detect/EXTI16 DummyModule::handler, //Tamper and Time Stamp/EXTI21 DummyModule::handler, //RTC Wakeup/EXTI22 DummyModule::handler, //FLASH DummyModule::handler, //RCC DummyModule::handler, //EXTI Line 0 DummyModule::handler, //EXTI Line 1 DummyModule::handler, //EXTI Line 2 DummyModule::handler, //EXTI Line 3 DummyModule::handler, //EXTI Line 4 DummyModule::handler, //DMA1 Stream 0 DummyModule::handler, //DMA1 Stream 1 DummyModule::handler, //DMA1 Stream 2 DummyModule::handler, //DMA1 Stream 3 DummyModule::handler, //DMA1 Stream 4 DummyModule::handler, //DMA1 Stream 5 DummyModule::handler, //DMA1 Stream 6 DummyModule::handler, //ADC1 0, //USB High Priority 0, //USB Low Priority 0, //DAC 0, //COMP through EXTI Line DummyModule::handler, //EXTI Line 9..5 DummyModule::handler, //TIM9/TIM1 Break interrupt DummyModule::handler, //TIM10/TIM1 Update interrupt DummyModule::handler, //TIM11/TIM1 Trigger/Commutation interrupts DummyModule::handler, //TIM1 Capture Compare interrupt DummyModule::handler, //TIM2 DummyModule::handler, //TIM3 DummyModule::handler, //TIM4 DummyModule::handler, //I2C1 Event DummyModule::handler, //I2C1 Error DummyModule::handler, //I2C2 Event DummyModule::handler, //I2C2 Error DummyModule::handler, //SPI1 DummyModule::handler, //SPI2 DummyModule::handler, //USART1 DummyModule::handler, //USART2 0, DummyModule::handler, //EXTI Line 15..10 DummyModule::handler, //EXTI Line 17 interrupt / RTC Alarms (A and B) through EXTI line interrupt DummyModule::handler, //EXTI Line 18 interrupt / USB On-The-Go FS Wakeup through EXTI line interrupt 0, //TIM6 0, //TIM7 f0 0, 0, DummyModule::handler, //DMA1 Stream 7 global interrupt fc 0, DummyModule::handler, //SDIO global interrupt DummyModule::handler, //TIM5 global interrupt DummyModule::handler, //SPI3 global interrupt 0, // 110 0, 0, 0, DummyModule::handler, //DMA2 Stream0 global interrupt 120 DummyModule::handler, //DMA2 Stream1 global interrupt DummyModule::handler, //DMA2 Stream2 global interrupt DummyModule::handler, //DMA2 Stream3 global interrupt DummyModule::handler, //DMA2 Stream4 global interrupt 130 0, 0, 0, 0, 0, 0, DummyModule::handler, //USB On The Go FS global interrupt, 14C DummyModule::handler, //DMA2 Stream5 global interrupt DummyModule::handler, //DMA2 Stream6 global interrupt DummyModule::handler, //DMA2 Stream7 global interrupt DummyModule::handler, //USART6 15C DummyModule::handler, //I2C3 Event DummyModule::handler, //I2C3 Error 164 0, 0, 0, 0, 0, 0, 0, DummyModule::handler, //FPU 184 0, 0, DummyModule::handler, //SPI 4 global interrupt DummyModule::handler //SPI 5 global interrupt
}; __weak void DummyModule::handler() { for(;;) {} }; extern "C" void __cmain( void );
extern "C" __weak void __iar_init_core( void );
extern "C" __weak void __iar_init_vfp( void ); #pragma required=__vector_table
void __iar_program_start( void )
{ __iar_init_core(); __iar_init_vfp(); __cmain();
}

utils.hpp

#ifndef UTILS_H
#define UTILS_H
#include <cassert> namespace utils { template<typename T, typename T1> inline void setBit(T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); value |= static_cast<T>(static_cast<T>(1) << static_cast<T>(bit)); }; template<typename T, typename T1> inline void clearBit(T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); value &= ~static_cast<T>(static_cast<T>(1) << static_cast<T>(bit)); }; template<typename T, typename T1> inline void toggleBit(T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); value ^= static_cast<T>(static_cast<T>(1) << static_cast<T>(bit)); }; template<typename T, typename T1> inline bool checkBit(const T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); return !((value & (static_cast<T>(1) << static_cast<T>(bit))) == static_cast<T>(0U)); };
};
#endif

led.hpp

#ifndef LED_H
#define LED_H
#include "utils.hpp" class Led
{
public: Led(GPIO_TypeDef &portName, unsigned int pinNum) : port(portName), pin(pinNum) {}; inline void Toggle() { utils::toggleBit(port.ODR, pin); } inline void SwitchOn() { utils::setBit(port.ODR, pin); } inline void SwitchOff() { utils::clearBit(port.ODR, pin); }
private: GPIO_TypeDef &port; unsigned int pin;
};
#endif

main.cpp

#include <stm32f411xe.h> #include "led.hpp" extern "C" { int __low_level_init(void) { //Включение внешнего генератора на 16 МГц RCC->CR |= RCC_CR_HSION; while ((RCC->CR & RCC_CR_HSIRDY) != RCC_CR_HSIRDY) { } //Переключаем системную частоту на внешний генератор RCC->CFGR |= RCC_CFGR_SW_HSI; while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI) { } //Подаем тактирование на порты С и А RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOAEN); //LED1 на PortA.5, устанавливаем PortA.5 как выход GPIOA->MODER |= GPIO_MODER_MODE5_0; return 1; } //Задержка, для простоты реализована в виде цикла inline void Delay(unsigned int mSec) { for (unsigned int i = 0U; i < mSec * 3000U; i++) { __NOP(); }; }
}
int main() { Led Led1(*GPIOA, 5U); Led1.Toggle(); Delay(1000U); Led1.Toggle(); return 0;
}

Программисты минималисты могут сказать, что да код понятнее, но ведь он избыточен, создается объект, идет вызов конструктора, методов, сколько же ОЗУ и дополнительного кода генерируется. Но если вы взглянете листинг на ассемблере, то приятно удивитесь, размер кода на С++ при включенной опции inline functions для обоих компиляторов, будет такой же как и для Си программы, а из-за особенностей вызова функции main, общий код на С++ даже на одну инструкцию меньше.
Ассемблерный код из Си исходников

image

Ассемблерный код из С++ исходников

image

Это еще раз подтверждает тот факт, что современные компиляторы делают свою работу по превращению вашего замечательного и понятного кода на языке С++ в оптимальный ассемблерный код. И совсем не каждый программист на ассемблере может достичь такого уровня оптимизации.
Конечно при отключенной оптимизации код на С++ не будет таким компактным по размеру стека и быстродействию. Для сравнения приведу неоптимизированный вариант с вызовом конструктора и методов.
image

Для меня нет дилеммы между держанием в голове множество ненужных деталей чтобы написать микропрограмму датчика (какие элементы на какие порты подключены, в каком текущем состоянии находится сейчас порт или тот или иной модуль и так далее) и простотой и понятностью кода. Ведь в конце концов, нам нужно описать логику работы устройства, интерфейс взаимодействия с пользователем, реализовать расчеты, а не запомнить, что для того чтобы считать данные с АЦП, нужно вначале его выбрать с помощью сигнала CS, находящегося на порту GPIOA.3 и установить его в единицу. Пусть этим занимается разработчик модуля АЦП.
Первоначально может показаться, что необходимо писать много дополнительного кода, но уверяю вас, это с лихвой окупится, когда приложение станет немного сложнее, чем просто моргнуть светодиодом.
Вернемся к нашему заданию. Не успел Снежинка показать результат своей работы заказчику, как заказчик, ощутив прелесть моргания светодиода в ночи, решил, что хорошо бы иметь моргающие в режиме “Елочка” 4 светодиода, тем более что на носу Китайский Новый год и будет много потенциальных покупателей.
Наш программист Снежинка, одновременно выполняющий несколько проектов, решил сэкономить время и сделать все в лоб самым, как он считает надежным и понятным способом:

#define TOGGLE_BIT(A,B) ((A) ^= (1U << ((B) & 31UL)))
#define SET_BIT(A,B) ((A) |= (1U << ((B) & 31UL))) int main(void) { //Зажигаем все светодиоды SET_BIT(GPIOC->ODR, 5U); SET_BIT(GPIOC->ODR, 8U); SET_BIT(GPIOC->ODR, 9U); SET_BIT(GPIOA->ODR, 5U); //Переключаем по очереди все светодиоды for (;;) { Delay(1000U); TOGGLE_BIT(GPIOC->ODR, 5U); Delay(1000U); TOGGLE_BIT(GPIOC->ODR, 8U); Delay(1000U); TOGGLE_BIT(GPIOC->ODR, 9U); Delay(1000U); TOGGLE_BIT(GPIOС->ODR, 5U); //ошибка: должно быть TOGGLE_BIT(GPIOA->ODR, 5U } return 0;
}

Код работает, но обратите внимание, на последнюю запись TOGGLE_BIT(GPIOС->ODR, 5U). Светодиоды 1 и 4 находятся на ножке номер 5, но на разных портах. Используя Ctrl С-Ctrl V, Снежинка скопировал первую запись, и забыл поменять порт. Это типичная ошибка, которую допускают программисты, работающие под давлением менеджмента, устанавливающих срок “вчера”. Проблема заключается в том, что для поставленной задачи надо было быстро написать код, и у Снежинки не было времени подумать над дизайном ПО, он просто сел и написал то, что надо было, при этом допустив небольшую помарку, которую он конечно же найдет при первой же прошивке в устройство. Однако, нужно понимать, что на это он потратит какое-то время. Кроме того, Снижинка добавил два ужасных макроса, которые по его мнению облегчают ему работу. В предыдущем примере на С++ мы добавили довольно много кода, например для того, чтобы заменить эти макросы на замечательные встроенные функции. Зачем?
Давайте рассмотрим очень популярный макрос установки бита. С помощью него можно устанавливать бит в любом целочисленном типе.

include <cassert> #define SET_BIT(A,B) (A |= (1 << B))
int main() { unsigned char value = 0U; SET_BIT(value, 10); return 0;
}

Все выглядит очень красиво, за исключением одного – в данном коде ошибка и нужный бит не установится. С помощью макроса SET_BIT устанавливается 10 бит в переменной value, которая имеет размер 8 бит. Интересно сколько программист будет искать такую ошибку, если объявление переменной будет не так близко к вызову макроса? Единственное преимущество данного подхода – это несомненный факт того, что код будет занимать наименьший размер.
Чтобы избежать потенциальной ошибки, давайте заменим этот макрос на шаблонную функцию

template<typename T, typename T1>
inline void setBit(T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); value |= static_cast<T>(static_cast<T>(1) << static_cast<T>(bit));
};

Здесь встроенная функция setBit принимает ссылку на параметр, в котором нужно установить бит и номер бита. Функция может принимать произвольный тип параметра и номер бита. В данном случае для того, чтобы убедиться, что номер бита не превышает размер типа параметра, другими словами, что бит точно можно установить в параметре такого типа, мы делаем проверку с помощью функции assert. Функция assert проверяет условие во время исполнения и если условие соблюдено, то код продолжает исполняться дальше, а вот если условия не соблюдено, то программа завершиться с ошибкой. Описание прототипа функции лежит в файле cassert, его и нужно подключить. Такая проверка будет полезна во время разработки, если вдруг кто-то решит передать неверный входной параметр, вы заметите это во время работы, когда он сработает. Понятно, что в продуктовом коде, нет смысла использовать проверку входных параметров, так как это занимает место, замедляет работу, да к тому же во время разработки вы исправили все места, где могли передаваться неверные параметры, поэтому assert можно отключить, определив NВEBUG символ в исходном файле или определив его для всего проекта
Обратите внимание на ключевое слово inline. Это ключевое слово указывает компилятору, что хотелось, чтобы данная функция рассматривалась как встраиваемая. Т.е. мы предполагаем, что компилятор просто заменит вызов функции на её код, однако на практике такого можно добиться только с установками оптимизации у компилятора. В IAR Workbench, это установка флажка напротив опции “Function Inlining” в закладке С/С++ Compiler->Optimization. В таком случае, наша функция также быстра и занимает столько же места как и макрос
Вернемся снова к коду Снежинки, как же тут обстоят дела с расширяемостью?

Код Снежинки

#define TOGGLE_BIT(A,B) ((A) ^= (1U << ((B) & 31UL)))
#define SET_BIT(A,B) ((A) |= (1U << ((B) & 31UL))) int main(void) { //Зажигаем все светодиоды SET_BIT(GPIOC->ODR, 5U); SET_BIT(GPIOC->ODR, 8U); SET_BIT(GPIOC->ODR, 9U); SET_BIT(GPIOA->ODR, 5U); //Переключаем по очереди все светодиоды for (;;) { Delay(1000U); TOGGLE_BIT(GPIOC->ODR, 5U); Delay(1000U); TOGGLE_BIT(GPIOC->ODR, 8U); Delay(1000U); TOGGLE_BIT(GPIOC->ODR, 9U); Delay(1000U); TOGGLE_BIT(GPIOС->ODR, 5U); //ошибка: должно быть TOGGLE_BIT(GPIOA->ODR, 5U } return 0;
}

Ведь судя по всему заказчик не остановится на этом и что произойдет, если светодиодов будет не 4, а 40? Размер кода увеличится линейно в 10 раз. Вероятность ошибки возрастет во столько же раз, а поддержка кода в дальнейшем превратится в рутину.
Более мудрый программист на С мог бы написать код так:

int main(void) { tLed pLeds[] = {{ GPIOC, 5U },{ GPIOC, 8U },{ GPIOC, 9U },{ GPIOA, 5U }}; SwitchOnAllLed(pLeds, LEDS_COUNT); for (;;) { for (int i = 0; i < LEDS_COUNT; i++) { Delay(1000U); ToggleLed(&pLeds[i]); } } return 0;
}

Функция main теперь содержит меньше кода и самое главное стала легко расширяемая. При увеличении количества светодиодов, теперь достаточно просто добавить порт, к которому подключен светодиод в массив светодиодов pLeds и макрос LEDS_COUNT поменять на количество светодиодов. При этом размер кода вообще не увеличится. Конечно глубина стека при этом вырастет значительно, так как массив светодиодов создается на стеке, и он равен уже 56 байт.
Между первым решением и вторым всегда есть выбор, что важнее для конкретной вашей реализации: Меньший размер кода, расширяемость, удобочитаемость и лаконичность или меньший размер ОЗУ и скорость. По моему опыту в 90% случаев можно выбрать первое.
Но давайте рассмотрим этот код повнимательнее. Это типичный код на Си с использованием указателей и макросов типа SET_BIT() и TOGGLE_BIT(). И в связи с этим, здесь существуют риски потенциальных проблем, например, функция SwitchOnAllLed(tLed *pLed, int size) принимает указатель и размер. Во-первых, нужно понимать, что ничего не запрещает передать в эту функцию нулевой указатель, поэтому нужна проверка, что указатель не равен NULL. Во-вторых, в случае, если вдруг программист передаст размер больше чем объявленный размер массива, поведение такой функции будет совершенно непредвиденным. Поэтому конечно, лучше в этой функции проверять размер. Добавление таких проверок приведет к увеличению кода, конечно можно использовать assert, но лучше попробовать написать тоже самое на С++

int main() { LedsController LedsContr; LedsContr.SwitchOnAll(); for (;;) { for (auto led : LedsContr.Leds) { Delay(1000U); led.Toggle(); } }
return 0;
}

Да этот год занимает уже значительно больше места. Но мы увидим в дальнейшем, как такой дизайн поможет нам сэкономить время, а размер кода будет практически таким же, как и на С, при усложнении программы.
Здесь используется класс LedsController, приведу его код:

#ifndef LEDSCONTROLLER_H
#define LEDSCONTROLLER_H
#include "led.hpp" constexpr unsigned int LedsCount = 4U;
class LedsController {
public: LedsController() {}; inline void SwitchOnAll() { for (auto led : Leds) { led.SwitchOn(); } }; Led Leds[LedsCount] = {{*GPIOC, 5U },{ *GPIOC, 8U },{ *GPIOC, 9U },{ *GPIOA, 5U }};
};
#endif

Методу SwitchOnAll() теперь не надо передавать указатель на массив, он использует уже существующий массив, сохраненный внутри объекта класса.
Почему же этот код считается надежнее? Во-первых, мы нигде не используем указатели, мы храним массив объектов на все существующие светодиоды в нашем классе и обращаемся непосредственно к объекту, а не к указателю. Во-вторых, мы используем специальный синтаксис для цикла for, который обходит наш массив без необходимости указывания его размера, за нас это делает компилятор. Этот цикл работает с любыми объектами являющиеся итераторами. Массив в С++ по умолчанию является таким объектом.
Единственное место, где можно ошибиться, это задание размера массива с помощью константы LedsCount. Однако, даже из этого небольшого примера, можно увидеть, что С++ предоставляет намного больше средств для написания надежного кода.
Еще один момент, требующий внимания – это то, что мы можем по ошибке создать несколько объектов класса LedsController, что приведет к увеличению размера используемого ОЗУ (стека) и к интересному поведению программы. Защититься от этого может помочь шаблон Одиночка, но делать это стоит только тогда, когда у вас довольно крупный проект, большая команда разработчиков и существует риск, что кто-то забудет о том, что объект вашего контроллера уже создан и нечаянно создаст еще один такой же. В нашем же случае, это явный переизбыток, функция небольшая, и мы четко помним, что объект класса LedsController у нас один.
Но вернемся к разработке, как обычно бывает, в тот момент, когда программист реализовал задачу (елочку в нашем случае), заказчик тут же просит реализовать еще два режима: моргание в шахматном порядке и моргание всеми светодиодами, а режимы должны меняться по нажатию кнопки. В случае со Снежинкой, произойдет практически полный провал, и если привести код программы в стиле Снежинка, то он будет настолько громоздким, что не влезет на страницу данной статьи, поэтому приводить здесь я его не буду.
Лучше посмотрим, что сможет сделать программист на С. Понимая, что от заказчика могут поступить еще новые предложения, он скорее всего сделает нечто вроде этого:

int main(void) { tPort Leds[] = { { GPIOC, 5U },{ GPIOC, 8U },{ GPIOC, 9U },{ GPIOA, 5U } }; tPort Button = { GPIOC, BUTTON_PIN }; //Кнопка на порте GPIOC.13 tLedMode Mode = LM_Tree; int currentLed = 0; SwitchOnAllLed(Leds, LEDS_COUNT); for (;;) { //Проверяем нажата ли кнопка. Она подтянута к 1, поэтому проверка на 0 if (!CHECK_BIT(Button.pPort->IDR, BUTTON_PIN)) { //Устанавливаем следующий режим Mode = (Mode < LM_End) ? (tLedMode)(Mode + 1U) : LM_Tree; //Устанавливаем начальное состояние для нового режима currentLed = 0; switch (Mode) { case LM_Tree: case LM_All: SwitchOnAllLed(Leds, LEDS_COUNT); break; case LM_Chess: SwitchChessLed(Leds, LEDS_COUNT); break; default: break; } } //Переключаем светодиоды в зависимости от режима switch (Mode) { case LM_Tree: ToggleLed(&Leds[currentLed]); break; case LM_All: case LM_Chess: ToggleAll(Leds, LEDS_COUNT); break; default: break; } currentLed = (currentLed < (LEDS_COUNT – 1)) ? (currentLed + 1) : 0; Delay(300U); } return 0;
}

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

inline void SetLedsBeginState(tLedMode mode, tPort *leds) { switch (mode) { case LM_Tree: case LM_All: SwitchOnAllLed(leds, LEDS_COUNT); break; case LM_Chess: SwitchChessLed(leds, LEDS_COUNT); break; default: break; }
} inline void UpdateLeds(tLedMode mode, tPort *leds, int curLed) { switch (mode) { case LM_Tree: ToggleLed(&leds[curLed]); break; case LM_All: case LM_Chess: ToggleAll(leds, LEDS_COUNT); break; default: break; }
}

В таком случае основная программ выглядит намного лучше:

int main(void) { tPort Leds[] = { {GPIOC, 5U},{GPIOC, 8U},{GPIOC, 9U},{GPIOA, 5U} }; tPort Button = {GPIOC, BUTTON_PIN}; tLedMode Mode = LM_Tree; int currentLed = 0; SwitchOnAllLed(Leds, LEDS_COUNT); for (;;) { //Проверяем нажата ли кнопка. Она подтянута к 1, поэтому проверка на 0 if (!CHECK_BIT(Button.pPort->IDR, BUTTON_PIN)) { //Устанавливаем следующий режим Mode = (Mode < LM_All) ? (tLedMode)(Mode + 1U) : LM_Tree; currentLed = 0; //Устанавливаем начальное состояние для нового режима SetLedsBeginState(Mode, Leds); } //Переключаем светодиоды в зависимости от режима UpdateLeds(Mode, Leds, currentLed); currentLed = (currentLed < (LEDS_COUNT -1)) ? (currentLed + 1) : 0; Delay(300U); } return 0;
}

Но все же хотелось бы что-то вроде человеческого
If Button is Pressed then
set Next Light Mode
Update Leds
Delay 1000ms

Можно попытаться сделать такое на в Си, но тогда придется держать несколько переменных вне функций, например, currentLed, Mode. Эти переменные должны быть глобальными, чтобы функции знали про них. А глобальные переменные, как мы знаем это опять потенциальный риск ошибки. Можно нечаянно поменять значение глобальной переменной в каком-то из модулей, а можно и не специально, так как вы не можете держать в голове все возможные места, где и как она меняется, а через год уже и не вспомните зачем она вообще нужна
Можно использовать для хранения этих данных структуры и пытаться использовать ООП на Си, но следует понимать, что в данном случае будет много накладных расходов, придется как минимум хранить указатель на функцию, а код будет выглядеть практически как на С++
Поэтому перейдем сразу к коду на С++

int main() { LedsController leds; Button button{ *GPIOC, 13U }; for (;;) { if (button.IsPressed()) { leds.NextMode(); } else { leds.Update(); } Delay(1sec); } return 0;
}

Полный код

utils.hpp

#ifndef UTILS_H
#define UTILS_H
#include <cassert> namespace utils { template<typename T, typename T1> inline void setBit(T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); value |= static_cast<T>(static_cast<T>(1) << static_cast<T>(bit)); }; template<typename T, typename T1> inline void clearBit(T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); value &=~ static_cast<T>(static_cast<T>(1) << static_cast<T>(bit)); }; template<typename T,typename T1> inline void toggleBit(T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); value ^= static_cast<T>(static_cast<T>(1) << static_cast<T>(bit)); }; template<typename T, typename T1> inline bool checkBit(const T &value, T1 bit) { assert((sizeof(T) * 8U) > bit); return !((value & (static_cast<T>(1) << static_cast<T>(bit))) == static_cast<T>(0U)); };
}; constexpr unsigned long long operator "" sec(unsigned long long sec) { return sec * 1000U;
}
#endif

led.hpp

#ifndef LED_H
#define LED_H
#include "utils.hpp" class Led
{ public: Led(GPIO_TypeDef &portName, unsigned int pinNum): port(portName), pin(pinNum) {}; inline void Toggle() { utils::toggleBit(port.ODR, pin); } inline void SwitchOn() { utils::setBit(port.ODR, pin); } inline void SwitchOff() { utils::clearBit(port.ODR, pin); } private: GPIO_TypeDef &port; unsigned int pin;
};
#endif

LedsController.hpp

#ifndef LEDSCONTROLLER_H
#define LEDSCONTROLLER_H
#include "led.hpp" enum class LedMode : unsigned char { Tree = 0, Chess = 1, All = 2, End = 2
}; constexpr int LedsCount = 4;
class LedsController { public: LedsController() { SwitchOnAll(); }; void SwitchOnAll() { for (auto led: leds) { led.SwitchOn(); } }; void ToggleAll() { for (auto led: leds) { led.Toggle(); } }; void NextMode() { mode = (mode < LedMode::All) ? static_cast<LedMode>(static_cast<unsigned char>(mode) + 1U) : LedMode::Tree; currentLed = 0; if (mode == LedMode::Chess){ for(int i = 0; i < LedsCount; i++) { if ((i % 2) == 0) { leds[i].SwitchOn(); } else { leds[i].SwitchOff(); } } } else { SwitchOnAll(); } }; void Update() { switch(mode) { case LedMode::Tree: leds[currentLed].Toggle(); break; case LedMode::All: case LedMode::Chess: ToggleAll(); break; default: break; } currentLed = (currentLed < (LedsCount - 1)) ? (currentLed + 1) : 0; } private: LedMode mode = LedMode::Tree; int currentLed = 0; Led leds[LedsCount] = {{*GPIOC, 5U},{*GPIOC, 8U},{*GPIOC, 9U},{*GPIOA, 5U}};
};
#endif

startup.cpp

#pragma language = extended
#pragma segment = "CSTACK"
extern "C" void __iar_program_start( void ); class DummyModule {
public: static void handler();
}; typedef void( *intfunc )( void );
//cstat !MISRAC++2008-9-5-1
typedef union { intfunc __fun; void * __ptr; } intvec_elem; #pragma location = ".intvec"
//cstat !MISRAC++2008-0-1-4_b !MISRAC++2008-9-5-1
extern "C" const intvec_elem __vector_table[] =
{ { .__ptr = __sfe( "CSTACK" ) }, __iar_program_start, DummyModule::handler, DummyModule::handler, DummyModule::handler, DummyModule::handler, DummyModule::handler, 0, 0, 0, 0, DummyModule::handler, DummyModule::handler, 0, DummyModule::handler, DummyModule::handler, //External Interrupts DummyModule::handler, //Window Watchdog DummyModule::handler, //PVD through EXTI Line detect/EXTI16 DummyModule::handler, //Tamper and Time Stamp/EXTI21 DummyModule::handler, //RTC Wakeup/EXTI22 DummyModule::handler, //FLASH DummyModule::handler, //RCC DummyModule::handler, //EXTI Line 0 DummyModule::handler, //EXTI Line 1 DummyModule::handler, //EXTI Line 2 DummyModule::handler, //EXTI Line 3 DummyModule::handler, //EXTI Line 4 DummyModule::handler, //DMA1 Stream 0 DummyModule::handler, //DMA1 Stream 1 DummyModule::handler, //DMA1 Stream 2 DummyModule::handler, //DMA1 Stream 3 DummyModule::handler, //DMA1 Stream 4 DummyModule::handler, //DMA1 Stream 5 DummyModule::handler, //DMA1 Stream 6 DummyModule::handler, //ADC1 0, //USB High Priority 0, //USB Low Priority 0, //DAC 0, //COMP through EXTI Line DummyModule::handler, //EXTI Line 9..5 DummyModule::handler, //TIM9/TIM1 Break interrupt DummyModule::handler, //TIM10/TIM1 Update interrupt DummyModule::handler, //TIM11/TIM1 Trigger/Commutation interrupts DummyModule::handler, //TIM1 Capture Compare interrupt DummyModule::handler, //TIM2 DummyModule::handler, //TIM3 DummyModule::handler, //TIM4 DummyModule::handler, //I2C1 Event DummyModule::handler, //I2C1 Error DummyModule::handler, //I2C2 Event DummyModule::handler, //I2C2 Error DummyModule::handler, //SPI1 DummyModule::handler, //SPI2 DummyModule::handler, //USART1 DummyModule::handler, //USART2 0, DummyModule::handler, //EXTI Line 15..10 DummyModule::handler, //EXTI Line 17 interrupt / RTC Alarms (A and B) through EXTI line interrupt DummyModule::handler, //EXTI Line 18 interrupt / USB On-The-Go FS Wakeup through EXTI line interrupt 0, //TIM6 0, //TIM7 f0 0, 0, DummyModule::handler, //DMA1 Stream 7 global interrupt fc 0, DummyModule::handler, //SDIO global interrupt DummyModule::handler, //TIM5 global interrupt DummyModule::handler, //SPI3 global interrupt 0, // 110 0, 0, 0, DummyModule::handler, //DMA2 Stream0 global interrupt 120 DummyModule::handler, //DMA2 Stream1 global interrupt DummyModule::handler, //DMA2 Stream2 global interrupt DummyModule::handler, //DMA2 Stream3 global interrupt DummyModule::handler, //DMA2 Stream4 global interrupt 130 0, 0, 0, 0, 0, 0, DummyModule::handler, //USB On The Go FS global interrupt, 14C DummyModule::handler, //DMA2 Stream5 global interrupt DummyModule::handler, //DMA2 Stream6 global interrupt DummyModule::handler, //DMA2 Stream7 global interrupt DummyModule::handler, //USART6 15C DummyModule::handler, //I2C3 Event DummyModule::handler, //I2C3 Error 164 0, 0, 0, 0, 0, 0, 0, DummyModule::handler, //FPU 184 0, 0, DummyModule::handler, //SPI 4 global interrupt DummyModule::handler //SPI 5 global interrupt
}; __weak void DummyModule::handler() { for(;;) {} }; extern "C" void __cmain( void );
extern "C" __weak void __iar_init_core( void );
extern "C" __weak void __iar_init_vfp( void ); #pragma required=__vector_table
void __iar_program_start( void )
{ __iar_init_core(); __iar_init_vfp(); __cmain();
}

main.cpp

#include <stm32f411xe.h> #include "ledscontroller.hpp"
#include "button.hpp" extern "C" { int __low_level_init(void) { //Включение внешнего генератора на 16 МГц RCC->CR |= RCC_CR_HSION; while ((RCC->CR & RCC_CR_HSIRDY) != RCC_CR_HSIRDY) { } //Переключаем системную частоту на внешний генератор RCC->CFGR |= RCC_CFGR_SW_HSI; while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI) { } //Подаем тактирование на порты С и А RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOCEN | RCC_AHB1ENR_GPIOAEN); //LED1 на PortA.5, ставим PortA.5 на выход GPIOA->MODER |= GPIO_MODER_MODE5_0; //LED2 на PortС.9,LED3 на PortC.8,LED4 на PortC.5 ставим PortC.5,8,9 на выход GPIOC->MODER |= (GPIO_MODER_MODE5_0 | GPIO_MODER_MODE8_0 | GPIO_MODER_MODE9_0); return 1; } //Задержка, для простоты реализована в виде цикла inline void Delay(unsigned int mSec) { for (unsigned int i = 0U; i < mSec * 3000U; i++) { __NOP(); }; }
} inline void Delay(unsigned int mSec) { for (unsigned int i = 0U; i < mSec * 3000U; i++) { __NOP(); };
} int main() { LedsController leds; LedsController leds1; Button buttonUser{*GPIOC, 13U}; for(;;) { if (buttonUser.IsPressed()) { leds.NextMode(); } else { leds.Update(); leds1.Update(); } Delay(1sec); } return 0;
}

Похоже на то, что нам удалось написать на С++ практически человеческим языком? Неправда ли очень понятный и простой код. Этот код не требует никаких комментариев, все понятно и так. Мы даже использовали пользовательские литерал «sec», чтобы было понятно, что это секунда, которая затем преобразуются в отсчеты для передачи в функцию Delay, с помощью следующей конструкции:

constexpr unsigned long long operator "" sec(unsigned long long sec) { return sec * 1000U; }
...
Delay(1sec);

Определение пользовательского литерала задается с помощью оператора "" и названия литерала. Ключевое constexpr указывает копилятору, что если это возможно значение должно быть посчитано на этапе компиляции и просто подставлено в код. В данном случае все значения известны на входе, мы передаем 1 и на выходе получаем 1000. Поэтому компилятор просто заменит вызов Delay(1sec) на Delay(1000) — очень удобно и читабельно. С помощью этого же ключевого слова можно заменить все макросы типа

#define MAGIC_NUM 0x5f3759df

, на более понятное

constexpr unsigned int MagicNumber = 0x5f3759df;

Еще раз повторюсь, мы получили очень расширяемый и понятный код, такой что при добавлении новых режимов моргания светодиодами или изменения количества светодиодов здесь вообще ничего не надо будет менять! Необходимо будет сделать небольшие изменения только в классе LedsController, отвечающий за поведение светодиодов. На лицо преимущество использования такого подхода.
Так сколько ресурсов теперь стало занимать такое решение? Взглянув на код, практически любой программист скажет, то код на С++ должен быть значительно больше, да что там, я сам в этом убеждён. Ведь тут и несколько объектов на стеке, вызовы конструкторов и дополнительных методов. Но, хватит предположений — перейдем к цифрам и сравним размеры кода на Си и С++ при отключенной оптимизации Код на Си занимает 496 байт и 80 байт максимальная вложенность стека. Код на С++ занимает 606 байт и 112 байта вложенности стека.
Казалось бы, на лицо 20% преимущество по размеру кода и стека в пользу Си. Но дело в том, что по умолчанию IAR компилятор никак не реагирует на ключевое слово inline у функций, и поэтому каждый раз вставляет вызовы функций, это в свою очередь приводит к увеличению кода и стека из-за сохранения и восстановления контекстов функций, а также к уменьшению скорости выполнения. Сделано это для того, чтобы можно было нормально провести отладку методов и функций, иначе бы некоторых функций и переменных вообще бы не существовало в результирующем коде.
Если мы включим оптимизацию и поддержку inline функций, то картина будет уже другой. Код на Си занимает 396 байта и 72 байта на стеке
Код же на С++ занимает 400 байта и 72 байта на стеке. Разница в 4 байт, а ассемблерный код практически идентичен коду на Си, при очевидном преимуществе в простоте, и лаконичности кода на С++. И кто теперь скажет что на С++ не выгодно писать встроенное ПО?
PS:
Пример кода можно взять тут:
yadi.sk/d/31QywFFX3RxRCa

Показать больше

Похожие публикации

Кнопка «Наверх»