Portfolio Обо мне Блог

Не давно я писал статью про то как настроить рабочее окружение для написания кода на СИ для работы в редакторе VScode. Но что делать если мы хотим писать код на С++, а не на СИ? На самом деле ответ на этот вопрос не тривиален. CubeMX генерирует проект, и параметры сборки таким образом что там используются только си файлы. Это значит что нам необходимо сделать 2 вещи - это соединить сишный код сгенерированный кубом с нашим плюсовым кодом и дописать make файл чтобы он мог собирать дополнительные с++ файлы.

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

Соединяем CPP и C

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

Для начала я предлагаю создать папку App в коре проекта, чтобы поместить туда все cpp файлы что позволит нам явно разделить сишную и плюсовую часть проекта. Далее внутри этой папки создать Inc и Src, и поместить туда хедеры и cpp файлы соответственно. Это нужно для сохранения стилистики кода сгенерированного в CubeMX.

mkdir App App/Inc App/Src

Далее я предлагаю создать файл app.cpp и app.h в которых будет описана функция app(), которая будет вызываться в main перед while(1) {} и по сути станет для нас плюсовой версией функции main.

app.h:

#ifndef INC_APP_H_
#define INC_APP_H_

#ifdef __cplusplus
extern "C" {
#endif
void app_c(void);
#ifdef __cplusplus
}
#endif

void app(void);

#endif /* INC_APP_H_ */

app.cpp:

#include "app.h"
#include "main.h"
#include "blink.h"

BLINK blink;

extern "C" void app_c(void) {
    app();
}

void app(void) {
    while (true) {
        blink.toggle();
    }
}

Далее создадим 2 файла blink.h и blink.c, в них будет описан класс BLINK метод которого и будет мигать диодом:

blink.h

#ifndef INC_BLINK_H_
#define INC_BLINK_H_

class BLINK {
public:
    void toggle();
};

#endif /* INC_BLINK_H_ */

blink.cpp

#include "blink.h"
#include "gpio.h"

void BLINK::toggle(){
    HAL_GPIO_TogglePin(LD_D_GPIO_Port, LD_D_Pin);
}

Далее я предлагаю чуть подробнее рассмотреть что мы написали:

  1. Конструкция в начале каждого хедера нужна для того чтобы избежать повторных добавления, вместо нее можно использовать #pragma once.
#ifndef INC_BLINK_H_
#define INC_BLINK_H_

#endif /* INC_BLINK_H_ */
  1. Данный код нужен для того чтобы g++ собрал app_c() как сишную функцию, при этом при добавлении этого хедера в *.c файл, это функция будет рассматриваться как обычная:
#ifdef __cplusplus
extern "C" {
#endif
void app_c(void);
#ifdef __cplusplus
}
#endif

Без extern "C" {} функция собралась бы как плюсовая и её нельзя было бы вызвать из си кода. Поэтому попытка вызова app() (расположенной ниже) в си коде вызовет ошибку.

  1. В *.cpp файле функцию app_c() тоже необходимо обернуть в extern "C" но тут не нужны никакие #ifdef потому что этот файл будет собираться только g++.
extern "C" void app_c(void) {
    app();
}
  1. Дальше все как обычно, расписываем функцию app() и создаем экземпляр класса BLINK.
  2. Для того чтобы добавить СИ код в С++ все достаточно просто, добавляем хедер, и вызываем функцию в методе и все:
#include "gpio.h"

void BLINK::toggle(){
    HAL_GPIO_TogglePin(LD_D_GPIO_Port, LD_D_Pin);
}

И в конце нужно добавить вызов нашей app_c() из main.c файла. Для этого добавляем хедер:

/* USER CODE BEGIN Includes */
#include "app.h"
/* USER CODE END Includes */

А также добавляем сам вызов после инициализации CubeIDE:

  /* USER CODE BEGIN WHILE */
  app_c();
  while (1)
  {
    /* USER CODE END WHILE */

MAKE

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

Для начала посмотрим на один из верхних блоков, править его сейчас не нужно, но если мы будем готовить версию к релизу нужно будет выставить 0 и -2g соответственно.

######################################
# building variables
######################################
# debug build?
DEBUG = 1
# optimization
OPT = -Og

Далее блок сo ссылками на исходники программы:

######################################
# source
######################################
# C sources
C_SOURCES =  \
Core/Src/main.c \

Ниже нам нужно будет добавить свои CPP sources:

# CPP sources
CPP_SOURCES =  \
App/Src/app.cpp \
App/Src/blink.cpp

После идет блок с сокращенными названиями, в него нужно добавить блок с CXX:

ifdef GCC_PATH
CC = $(GCC_PATH)/$(PREFIX)gcc
CXX = $(GCC_PATH)/$(PREFIX)g++
AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp
CP = $(GCC_PATH)/$(PREFIX)objcopy
SZ = $(GCC_PATH)/$(PREFIX)size
else
CC = $(PREFIX)gcc
CXX = $(PREFIX)g++
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
SZ = $(PREFIX)size
endif
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S

Потом идут CFLAGS, параметры контроллера в них трогать не нужно, а вот в C_INCLUDES будет необходимо добавить папку с нашими хедерами:

# C includes
C_INCLUDES =  \
-IApp/Inc \
-ICore/Inc \
...

После чего, для каждого CFLAGS нужно будет сделать почти такой же CPPFLAGS, только вместо -std=c11, должно быть -std=c++17:

# compile gcc flags
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections

CFLAGS = $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections -std=c11
CPPFLAGS = $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections -std=c++17

ifeq ($(DEBUG), 1)
CFLAGS += -g -gdwarf-2
CPPFLAGS += -g -gdwarf-2
endif

# Generate dependency information
CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"
CPPFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"

Далее идет блок с параметрами линковщика, тут нужно добавить 2 флага -specs=nosys.specs -lstdc++ в LDFLAGS, для избежания проблем со стандартной библиотекой. В итоге должно получится так:

LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections -specs=nosys.specs -lstdc++

После чего идет описание сборки кода, где нам необходимо добавить объектники которые соберутся с++:

#######################################
# build the application
#######################################
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
# list of c++ objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(CPP_SOURCES:.cpp=.o)))
vpath %.cpp $(sort $(dir $(CPP_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))

И наконец нужно будет собрать все эти обьектники, где тоже необходимо добавить описание сборки файлов с++:

$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) 
    $(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@

$(BUILD_DIR)/%.o: %.cpp Makefile | $(BUILD_DIR) 
    $(CXX) -c $(CPPFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.cpp=.lst)) $< -o $@

$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
    $(AS) -c $(CFLAGS) $< -o $@

После чего идет блок линковщика и в нем уже ничего править не нужно.

Пример настроенного проекта доступен тут.