Как стать автором
Обновить

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

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

Код на Си, занимающий 156 байт

  int main(void) {
  int testArray[5U] = {-1,20,-3,0,4};
  int lowest = INT_MAX;
  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 = std::numeric_limits<int>.max();
  for (auto it: testArray) {
    lowest = ((lowest < it) ? lowest : it);
  };
return 0;

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

image

Как можно увидеть сгенерированный компилятором код на С++ на 4 байт меньше, а скорость работы на 12 тактов быстрее. Все это достигается за счет новых возможностей С++14. Конечно, можно заметить, что для обоих компиляторов была отключена оптимизация, что это очень синтетический тест который не имеет ничего общего с реальной реализацией, но все же можно сказать, что не все так однозначно.

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

Первым делом надо определиться с задачей. Она должна быть достаточно простой, но и достаточно показательной, чтобы увидеть как можно, например, полностью отказаться от макросов, по-возможности уйти от указателей, уменьшить риски глупых ошибок и так далее…
Выбор, как обычно пал на светодиды.
Для того, чтобы читатель понимал что мы хотим сделать, я приведу конечный вариант задачи, которую необходимо реализовать на микроконтроллере:

  • Используемая плата 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() const { utils::toggleBit(port.ODR, pin); }
	inline void SwitchOn() const { utils::setBit(port.ODR, pin); }
	inline void SwitchOff() const { 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 и установить его в единицу. Пусть этим занимается разработчик модуля АЦП.
Первоначально может показаться, что необходимо писать много дополнительного кода, но уверяю вас, это с лихвой окупится, когда приложение станет немного сложнее, чем просто моргнуть светодиодом.

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

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

#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, Снежинка скопировал первую запись, и забыл поменять порт. Это типичная ошибка, которую допускают программисты, работающие под давлением менеджмента, устанавливающих срок “вчера”. Проблема заключается в том, что для поставленной задачи надо было быстро написать код, и у Снежинки не было времени подумать над дизайном ПО, он просто сел и написал то что надо было, при этом допустив небольшую помарку, которую он конечно же найдет при первой же прошивке в устройство. Однако, нужно понимать, что на это он потратит какое-то время. Кроме того, Снежинка добавил два ужасных макроса, которые по его мнению облегчают ему работу. В предыдущем примере на С++ мы добавили довольно много кода, в том числе для того, чтобы заменить эти макросы на замечательные встроенные функции. Зачем?

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

#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 проверяет условие во время исполнения и если условие соблюдено, то код продолжает исполняться дальше, а вот если условия не соблюдено, то программа завершится с ошибкой. Описание прототипа функции assert лежит в файле cassert, его и нужно подключить. Такая проверка будет полезна во время разработки, если вдруг кто-то решит передать неверный входной параметр, вы заметите это во время работы, когда он сработает. Понятно, что в продуктовом коде нет смысла использовать проверку входных параметров, так как это занимает место, замедляет работу, да к тому же во время разработки вы уже отлавили все потенциальные возможности передачи неверных параметров, поэтому assert можно отключить, определив NDEBUG символ в исходном файле или определив его для всего проекта.

Обратите внимание на ключевое слово 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"
#include <array>
constexpr unsigned int LedsCount = 4U;
class LedsController {
public:
  LedsController() {};
  inline void SwitchOnAll() {
    for (auto &led : Leds) {
      led.SwitchOn();
    }
 };
 std::array<Led, LedsCount> leds{Led{*GPIOC, 5U},Led{*GPIOC, 8U},Led{*GPIOC, 9U},Led{*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_End) ? (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() const { utils::toggleBit(port.ODR, pin); }
    inline void SwitchOn() const { utils::setBit(port.ODR, pin); }
    inline void SwitchOff() const { utils::clearBit(port.ODR, pin); }
  private:
    GPIO_TypeDef &port;
    unsigned int pin;
};
#endif


LedsController.hpp
#ifndef LEDSCONTROLLER_H
#define LEDSCONTROLLER_H
#include "led.hpp"
#include <array>

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::End) ? 
      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;
    std::array<Led, LedsCount> leds{Led{*GPIOC, 5U},Led{*GPIOC, 8U},Led{*GPIOC, 9U},Led{*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();
	};
}

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 байта, а ассемблерный код практически идентичен коду на Си при очевидном преимуществе в простоте и лаконичности кода на С++. И кто теперь скажет что на С++ не выгодно писать встроенное ПО?

P.S.:
Пример кода можно взять тут.

Спасибо за найденный недочет vanxant, Mabu, exchg, Antervis. kosmos89 за хороший совет, а NightShad0w за пример поиска наименьшего с помощью std библиотеки Код поиска наменьшего с std

По совету Jef239 для уменьшения размера ОЗУ под массив leds его можно задать как static const, а все методы класса Led сделать константными, тогда массив на этом процессоре будет расположен в памяти программ и стек уменьшится на размер этого массива. Выбор за разработчиком, что важнее…
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+55
Комментарии 134
Комментарии Комментарии 134

Публикации

Истории

Работа

QT разработчик
13 вакансий
Программист С
43 вакансии
Программист C++
121 вакансия

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн