Год назад я написал статью об отладке STM32 микроконтроллеров из под VSCode, с компиляцией в GCC и сборкой с помощью CMake. А в декабре мне в руки попали две единицы отечественных микроконтроллеров К1986ВЕ92FI (MDR1211FI1). Производитель имеет свою библиотеку SPL на C, а также неплохую базу примеров инициализации и применения различной периферии в Keil и IAR; однако я, average C++20+ enjoyer , решил попробовать перенести свой тулчейн на новое железо.
Отладочная плата
Разработанная плата представляет собой стандартный "джентльменский набор" – порты программирования, USB-C, три светодиода (индикатор питания, индикатор режима STANDBY и управляемый светодиод) и два кварцевых резонатора под HSE и LSE.

P.S.2 Будьте прилежны при пайке QFN микросхем, иначе есть риск обнаружить неработу из-за того, что ваш микроконтроллер чуть менее чем полностью опирается на термопад, тогда как контакты по периметру просто висят в воздухе
Программатор
Для отладки используется китайский J-Link V9/V11. Детально процесс настройки ПО SEGGER описан на сайте производителя МК. Приведу здесь только содержимое файлов, что расположены по пути %APPDATA%\SEGGER\JLinkDevices.
спойлер
Содержимое файла JLinkDevices.xml
<Device> <ChipInfo Vendor="Milandr" Name="K1986BE1QI" WorkRAMAddr="0x20100000" WorkRAMSize="0x4000" Core="JLINK_CORE_CORTEX_M1" /> <FlashBankInfo Name="K1986BE1QI Flash" BaseAddr="0x0" MaxSize="0x20000" Loader="Devices\Milandr\MDR32F1QI.FLM" LoaderType="FLASH_ALGO_TYPE_OPEN" AlwaysPresent="1"/> </Device> <Device> <ChipInfo Vendor="Milandr" Name="K1986BE92QI" WorkRAMAddr="0x20000000" WorkRAMSize="0x8000" Core="JLINK_CORE_CORTEX_M3"/> <FlashBankInfo Name="K1986BE92QI Flash" BaseAddr="0x8000000" MaxSize="0x20000" Loader="Devices\Milandr\MDR32F9Q2I.FLM" LoaderType="FLASH_ALGO_TYPE_OPEN" AlwaysPresent="1" /> </Device>
Содержимое файла Devices.xml
<Database> <Device> <ChipInfo Vendor="Milandr" Name="K1986BE1QI" WorkRAMAddr="0x20000000" WorkRAMSize="0x8000" Core="JLINK_CORE_CORTEX_M3"/> <FlashBankInfo Name="K1986BE1QI Flash" BaseAddr="0x8000000" MaxSize="0x20000" Loader="C:\Users\Filipp\AppData\Roaming\SEGGER\JLinkDevices\Milandr\MDR32F1QI.FLM" LoaderType="FLASH_ALGO_TYPE_OPEN" AlwaysPresent="1" /> </Device> <Device> <ChipInfo Vendor="Milandr" Name="K1986BE92QI" WorkRAMAddr="0x20000000" WorkRAMSize="0x8000" Core="JLINK_CORE_CORTEX_M3"/> <FlashBankInfo Name="K1986BE92QI Flash" BaseAddr="0x8000000" MaxSize="0x20000" Loader="C:\Users\Filipp\AppData\Roaming\SEGGER\JLinkDevices\Milandr\MDR32F9Q2I.FLM" LoaderType="FLASH_ALGO_TYPE_OPEN" AlwaysPresent="1" /> </Device> </Database>
Создание проекта
В дальнейшем описании предполагается, что у вас установлены приложения, и расширения VSCode, указанные в предыдущей статье. Также необходимо скачать с сайта производителя Software pack для Keil MDK 5 и Standard Peripherals Library.
В корень проекта скопируем файлMDR32F9Q2I.svd (брать из папки Standard Peripherals Library\SVD\MDR32FxQI), и папку Standard Peripherals Library\Libraries\SPL\MDR32FxQI, которую переименуем в MDR32FxQI_SPL. Также создадим две директории: CoreSupport и DeviceSupport. В папку CoreSupport скопируем все файлы из Standard Peripherals Library\Libraries\CMSIS\MDR32FxQI\CoreSupport\CM3, а в DeviceSupport – все файлы из Standard Peripherals Library\Libraries\CMSIS\MDR32FxQI\DeviceSupport\MDR32F9Q2I\inc и Standard Peripherals Library\Libraries\CMSIS\MDR32FxQI\DeviceSupport\MDR32F9Q2I\startup.
На данный момент директория проекта выглядит следующим образом:
./: MDR32F9Q2I.svd ./CoreSupport/: core_cm3.h core_cmFunc.h core_cmInstr.h ./DeviceSupport/: MDR32F9Q2I.h system_MDR32F9Q2I.c system_MDR32F9Q2I.h ./MDR32FxQI_SPL/: inc/ src/ MDR32FxQI_config.h
Минимальный код
Для запуска МК также потребуются linker-скрипт и startup-файл, назовём их MDR32F9Q2I.ld и startup_gcc_MDR32F9Q2I.s соответственно. За основу возьмём файлы для STM32F103C8. Подробное описание того, что в них происходит можно увидеть на примерах из видео1 и видео2.
MDR32F9Q2I.ld
/* Abstract: Linker script for MDR32F9Q2I 128 Kb FLASH + 32 Kb RAM */ ENTRY(Reset_handler); MEMORY { FLASH(rx) : ORIGIN = 0x08000000, LENGTH = 128K RAM(xrw) : ORIGIN = 0x20000000, LENGTH = 32K } _estack = ORIGIN(RAM) + LENGTH(RAM); /* end of RAM */ _Min_Heap_Size = 0x200; /* required amount of heap */ _Min_Stack_Size = 0x400; /* required amount of stack */ /* Define output sections */ SECTIONS { /* The startup code goes first into FLASH */ .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(4); } >FLASH /* The program code and other data goes into FLASH */ .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ *(.text*) /* .text* sections (code) */ *(.glue_7) /* glue arm to thumb code */ *(.glue_7t) /* glue thumb to arm code */ *(.eh_frame) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; /* define a global symbols at end of code */ } >FLASH /* Constant data goes into FLASH */ .rodata : { . = ALIGN(4); *(.rodata) /* .rodata sections (constants, strings, etc.) */ *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ . = ALIGN(4); } >FLASH .ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH .ARM : { __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; } >FLASH .preinit_array : { PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .); } >FLASH .init_array : { PROVIDE_HIDDEN (__init_array_start = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); } >FLASH .fini_array : { PROVIDE_HIDDEN (__fini_array_start = .); KEEP (*(SORT(.fini_array.*))) KEEP (*(.fini_array*)) PROVIDE_HIDDEN (__fini_array_end = .); } >FLASH /* used by the startup to initialize data */ _sidata = LOADADDR(.data); /* Initialized data sections goes into RAM, load LMA copy after code */ .data : { . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ *(.data) /* .data sections */ *(.data*) /* .data* sections */ . = ALIGN(4); _edata = .; /* define a global symbol at data end */ } >RAM AT> FLASH /* Uninitialized data section */ . = ALIGN(4); .bss : { /* This is used by the startup in order to initialize the .bss secion */ _sbss = .; /* define a global symbol at bss start */ __bss_start__ = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; } >RAM /* User_heap_stack section, used to check that there is enough RAM left */ ._user_heap_stack : { . = ALIGN(8); PROVIDE ( end = . ); PROVIDE ( _end = . ); . = . + _Min_Heap_Size; . = . + _Min_Stack_Size; . = ALIGN(8); } >RAM /* Remove information from the standard libraries */ /DISCARD/ : { libc.a ( * ) libm.a ( * ) libgcc.a ( * ) } .ARM.attributes 0 : { *(.ARM.attributes) } }
startup_gcc_MDR32F9Q2I.s
.syntax unified .cpu cortex-m3 .fpu softvfp .thumb /* start address for the initialization values of the .data section. defined in linker script */ .word _sidata /* start address for the .data section. defined in linker script */ .word _sdata /* end address for the .data section. defined in linker script */ .word _edata /* start address for the .bss section. defined in linker script */ .word _sbss /* end address for the .bss section. defined in linker script */ .word _ebss .global __Vectors .global Default_Handler /****************************************************************************** * * The minimal vector table for a Cortex M3. Note that the proper constructs * must be placed on this to ensure that it ends up at physical address * 0x0000.0000. * ******************************************************************************/ .section .isr_vector,"a",%progbits .type __Vectors, %object .size __Vectors, .-__Vectors __Vectors: .word _estack /* Top of Stack */ .word Reset_Handler /* Reset Handler */ .word NMI_Handler /* NMI Handler */ .word HardFault_Handler /* Hard Fault Handler */ .word MemManage_Handler /* MPU Fault Handler */ .word BusFault_Handler /* Bus Fault Handler */ .word UsageFault_Handler /* Usage Fault Handler */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word SVC_Handler /* SVCall Handler */ .word DebugMon_Handler /* Debug Monitor Handler */ .word 0 /* Reserved */ .word PendSV_Handler /* PendSV Handler */ .word SysTick_Handler /* SysTick Handler */ /* External Interrupts*/ .word CAN1_IRQHandler /* 0 CAN1 Handler */ .word CAN2_IRQHandler /* 1 CAN2 Handler */ .word USB_IRQHandler /* 2 USB Host Handler */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word DMA_IRQHandler /* 5 DMA Handler */ .word UART1_IRQHandler /* 6 UART1 Handler */ .word UART2_IRQHandler /* 7 UART2 Handler */ .word SSP1_IRQHandler /* 8 SSP1 Handler */ .word 0 /* Reserved */ .word I2C_IRQHandler /* 10 I2C Handler */ .word POWER_IRQHandler /* 11 POWER Handler */ .word WWDG_IRQHandler /* 12 WWDG Handler */ .word 0 /* Reserved */ .word Timer1_IRQHandler /* 14 Timer1 Handler */ .word Timer2_IRQHandler /* 15 Timer2 Handler */ .word Timer3_IRQHandler /* 16 Timer3 Handler */ .word ADC_IRQHandler /* 17 ADC Handler */ .word 0 /* Reserved */ .word COMPARATOR_IRQHandler /* 19 Comparator Handler */ .word SSP2_IRQHandler /* 20 SSP2 Handler */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word 0 /* Reserved */ .word BACKUP_IRQHandler /* 27 BKP Handler */ .word EXT_INT1_IRQHandler /* 28 EXT_INT1 Handler */ .word EXT_INT2_IRQHandler /* 29 EXT_INT2 Handler */ .word EXT_INT3_IRQHandler /* 30 EXT_INT3 Handler */ .word EXT_INT4_IRQHandler /* 31 EXT_INT4 Handler */ /******************************************************************************* * @brief This is the code that gets called when the processor first * starts execution following a reset event. Only the absolutely * necessary set is performed, after which the application * supplied main() routine is called. * @param None * @retval : None *******************************************************************************/ .section .text.Reset_Handler .weak Reset_Handler .type Reset_Handler, %function Reset_Handler: /* Call the clock system initialization function.*/ bl SystemInit /* Copy the data segment initializers from flash to SRAM */ ldr r0, =_sdata ldr r1, =_edata ldr r2, =_sidata movs r3, #0 b LoopCopyDataInit CopyDataInit: ldr r4, [r2, r3] str r4, [r0, r3] adds r3, r3, #4 LoopCopyDataInit: adds r4, r0, r3 cmp r4, r1 bcc CopyDataInit /* Zero fill the bss segment. */ ldr r2, =_sbss ldr r4, =_ebss movs r3, #0 b LoopFillZerobss FillZerobss: str r3, [r2] adds r2, r2, #4 LoopFillZerobss: cmp r2, r4 bcc FillZerobss /* Call static constructors */ bl __libc_init_array /* Call the application's entry point.*/ bl main bx lr .size Reset_Handler, .-Reset_Handler /******************************************************************************* * @brief This is the code that gets called when the processor receives an * unexpected interrupt. This simply enters an infinite loop, preserving * the system state for examination by a debugger. * * @param None * @retval : None *******************************************************************************/ .section .text.Default_Handler,"ax",%progbits Default_Handler: Infinite_Loop: b Infinite_Loop .size Default_Handler, .-Default_Handler /******************************************************************************* * * Provide weak aliases for each Exception handler to the Default_Handler. * As they are weak aliases, any function with the same name will override * this definition. * *******************************************************************************/ .weak NMI_Handler .thumb_set NMI_Handler,Default_Handler .weak HardFault_Handler .thumb_set HardFault_Handler,Default_Handler .weak MemManage_Handler .thumb_set MemManage_Handler,Default_Handler .weak BusFault_Handler .thumb_set BusFault_Handler,Default_Handler .weak UsageFault_Handler .thumb_set UsageFault_Handler,Default_Handler .weak SVC_Handler .thumb_set SVC_Handler,Default_Handler .weak DebugMon_Handler .thumb_set DebugMon_Handler,Default_Handler .weak PendSV_Handler .thumb_set PendSV_Handler,Default_Handler .weak SysTick_Handler .thumb_set SysTick_Handler,Default_Handler .weak CAN1_IRQHandler .thumb_set CAN1_IRQHandler,Default_Handler .weak CAN2_IRQHandler .thumb_set CAN2_IRQHandler,Default_Handler .weak USB_IRQHandler .thumb_set USB_IRQHandler,Default_Handler .weak DMA_IRQHandler .thumb_set DMA_IRQHandler,Default_Handler .weak UART1_IRQHandler .thumb_set UART1_IRQHandler,Default_Handler .weak UART2_IRQHandler .thumb_set UART2_IRQHandler,Default_Handler .weak SSP1_IRQHandler .thumb_set SSP1_IRQHandler,Default_Handler .weak I2C_IRQHandler .thumb_set I2C_IRQHandler,Default_Handler .weak POWER_IRQHandler .thumb_set POWER_IRQHandler,Default_Handler .weak WWDG_IRQHandler .thumb_set WWDG_IRQHandler,Default_Handler .weak Timer1_IRQHandler .thumb_set Timer1_IRQHandler,Default_Handler .weak Timer2_IRQHandler .thumb_set Timer2_IRQHandler,Default_Handler .weak Timer3_IRQHandler .thumb_set Timer3_IRQHandler,Default_Handler .weak ADC_IRQHandler .thumb_set ADC_IRQHandler,Default_Handler .weak COMPARATOR_IRQHandler .thumb_set COMPARATOR_IRQHandler,Default_Handler .weak SSP2_IRQHandler .thumb_set SSP2_IRQHandler,Default_Handler .weak BACKUP_IRQHandler .thumb_set BACKUP_IRQHandler,Default_Handler .weak EXT_INT1_IRQHandler .thumb_set EXT_INT1_IRQHandler,Default_Handler .weak EXT_INT2_IRQHandler .thumb_set EXT_INT2_IRQHandler,Default_Handler .weak EXT_INT3_IRQHandler .thumb_set EXT_INT3_IRQHandler,Default_Handler .weak EXT_INT4_IRQHandler .thumb_set EXT_INT4_IRQHandler,Default_Handler
main.c файл создадим максимально простым
#define __ASM __asm #define __NOP() __ASM volatile("nop") int main(){ while(1){ __NOP(); } }
Настройка VSCode
Для компиляции и запуска проекта потребуются также слегка изменить файлы .vscode/launch.json и .vscode/tasks.json
launch.json
{ "configurations": [ { "name": "Cortex Debug", "type": "cortex-debug", "request": "launch", "servertype": "jlink", "cwd": "${workspaceRoot}", // "executable": "build/mdr_test.elf", "executable": "${command:cmake.launchTargetPath}", "preLaunchTask": "CMake: build", "preRestartCommands": [ "load", "enable breakpoint", "monitor reset" ], "liveWatch": { "enabled": true, "samplesPerSecond": 2 }, "runToEntryPoint": "main", "showDevDebugOutput": "raw", "device": "K1986BE92QI", "svdFile": "${workspaceRoot}/MDR32F9Q2I.svd", } ] }
tasks.json
{ "version": "2.0.0", "tasks": [ { "type": "cmake", "label": "CMake: build", "command": "build", "targets": [ "ALL_BUILD" ], "problemMatcher": [], "group": "build" } ] }
CMake
На GitHub можно найти шаблон для CMakeLists.txt под ARM GCC проекты. Возьмём его за основу и слегка модифицируем.
CMakeLists.txt
cmake_minimum_required(VERSION 3.20) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS ON) set(CMAKE_C_STANDARD 11) set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_VERSION 1) # specify cross compilers and tools set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER arm-none-eabi-g++) set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) set(CMAKE_AR arm-none-eabi-ar) set(CMAKE_OBJCOPY arm-none-eabi-objcopy) set(CMAKE_OBJDUMP arm-none-eabi-objdump) set(SIZE arm-none-eabi-size) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-register") # project settings project(mldr_test C CXX ASM) add_compile_options(-mfloat-abi=soft) add_compile_options(-mcpu=cortex-m3 -mthumb) add_compile_options(-ffunction-sections -fdata-sections -fno-common -fmessage-length=0) add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions -fno-rtti>) include_directories( ${CMAKE_SOURCE_DIR}/CoreSupport ${CMAKE_SOURCE_DIR}/DeviceSupport ${CMAKE_SOURCE_DIR}/MDR32FxQI_SPL ) set(SOURCES startup_gcc_MDR32F9Q2I.s DeviceSupport/system_MDR32F9Q2I.c main.c ) set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/MDR32F9Q2I.ld) add_link_options(-Wl,-gc-sections,--print-memory-usage,-Map=${PROJECT_BINARY_DIR}/${PROJECT_NAME}.map) add_link_options(-mcpu=cortex-m3 -mthumb -mthumb-interwork) add_link_options(-T ${LINKER_SCRIPT}) add_executable(${PROJECT_NAME}.elf ${SOURCES} ${LINKER_SCRIPT}) target_compile_definitions(${PROJECT_NAME}.elf PRIVATE USE_MDR32F9Q2I ) set_target_properties(${PROJECT_NAME}.elf PROPERTIES EXPORT_COMPILE_COMMANDS ON CXX_STANDARD 20 CXX_STANDARD_REQUIRED True ) # Generate .hex and .bin files set(HEX_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.hex) set(BIN_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.bin) add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -Oihex $<TARGET_FILE:${PROJECT_NAME}.elf> ${HEX_FILE} COMMAND ${CMAKE_OBJCOPY} -Obinary $<TARGET_FILE:${PROJECT_NAME}.elf> ${BIN_FILE} COMMENT "Building ${HEX_FILE} Building ${BIN_FILE}" )
Модификация файлов производителя
Для успешного запуска необходимо добавить определения для gcc в файл system_MDR32F9Q2I.c. Находим следующий код:
/** @addtogroup __MDR32F9Q2I_System_Private_Defines MDR32F9Q2I System Private Defines * @{ */ #if defined (__ARMCC_VERSION) /* ARM Compiler */ extern uint32_t __Vectors; #define __VECTOR_TABLE_ADDRESS &__Vectors #elif defined (__ICCARM__) /* IAR Compiler */ extern uint32_t __vector_table; #define __VECTOR_TABLE_ADDRESS &__vector_table #elif defined (__CMCARM__) /* Phyton CMC-ARM Compiler */ extern uint32_t __Vectors; #define __VECTOR_TABLE_ADDRESS &__Vectors #endif
и добавляем перед #endif
#elif defined (__GNUC__) /* ARM GCC */ extern uint32_t __Vectors; #define __VECTOR_TABLE_ADDRESS &__Vectors
Аналогичным образом нужно модифицировать файл MDR32FxQI_config.h, чтобы получить следующий код:
#if defined (__ICCARM__) /* IAR Compiler */ #define __attribute__(name_section) #if defined (USE_MDR32F1QI) #pragma section = "EXECUTABLE_MEMORY_SECTION" #define IAR_SECTION(section) @ section #elif defined (USE_MDR32F9Q2I) #define IAR_SECTION(section) #endif #define __RAMFUNC __ramfunc #endif #if defined (__CMCARM__) /* Phyton CMC-ARM Compiler */ #define __attribute__(name_section) #define IAR_SECTION(section) #define __RAMFUNC __ramfunc #endif #if defined (__ARMCC_VERSION) /* ARM Compiler */ #define IAR_SECTION(section) #define __RAMFUNC #endif #if defined (__GNUC__) /* ARM GCC */ #define IAR_SECTION(section) #define __RAMFUNC #endif
можем запускаться
Пробуем мигать светодиодом
Возьмём за основу проект HelloWord из инструкции с сайта Миландра.
Добавим необходимые директории и файлы в CMakeLists.txt
include_directories( ${CMAKE_SOURCE_DIR}/CoreSupport ${CMAKE_SOURCE_DIR}/DeviceSupport ${CMAKE_SOURCE_DIR}/MDR32FxQI_SPL ${CMAKE_SOURCE_DIR}/MDR32FxQI_SPL/inc ) set(SOURCES startup_gcc_MDR32F9Q2I.s DeviceSupport/system_MDR32F9Q2I.c main.c MDR32FxQI_SPL/src/MDR32FxQI_rst_clk.c MDR32FxQI_SPL/src/MDR32FxQI_port.c )
А также модифицируем main.c под новые задачи
#include <MDR32FxQI_port.h> #include <MDR32FxQI_rst_clk.h> void Delay(int waitTicks); int main() { // Заводим структуру конфигурации вывода(-ов) порта GPIO PORT_InitTypeDef GPIOInitStruct; // Включаем тактирование порта C RST_CLK_PCLKcmd(RST_CLK_PCLK_PORTC, ENABLE); // Инициализируем структуру конфигурации вывода(-ов) порта значениями по // умолчанию PORT_StructInit(&GPIOInitStruct); // Изменяем значения по умолчанию на необходимые нам настройки GPIOInitStruct.PORT_Pin = PORT_Pin_2; GPIOInitStruct.PORT_OE = PORT_OE_OUT; GPIOInitStruct.PORT_SPEED = PORT_SPEED_SLOW; GPIOInitStruct.PORT_MODE = PORT_MODE_DIGITAL; // Применяем заполненную нами структуру для PORTC. PORT_Init(MDR_PORTC, &GPIOInitStruct); // while (1) { if (PORT_ReadInputDataBit (MDR_PORTC, PORT_Pin_2) == 0){ PORT_SetBits(MDR_PORTC, PORT_Pin_2); // светоидиот на нашей плате }else{ PORT_ResetBits(MDR_PORTC, PORT_Pin_2); } // Задержка Delay(100000); } } // Простейшая функция задержки, позднее мы заменим ее на реализацию через таймер void Delay(int waitTicks) { int i; for (i = 0; i < waitTicks; i++) { __NOP(); } }
Включаемся, прошиваемся, радуемся красивой мигалке

Пробуем собраться с C++
Не мудрствуя лукаво, переименуем main.c в main.cpp и попробуем собрать тот же самый проект. Наблюдаем exit code 1, а также несколько ошибок следующего вида:
[build] C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld.exe: C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/../lib/gcc/arm-none-eabi/13.2.1/thumb/v7-m/nofp\libg.a(libc_a-abort.o): in function `abort': [build] abort.c:(.text.abort+0xa): undefined reference to `_exit' [build] C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld.exe: C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/../lib/gcc/arm-none-eabi/13.2.1/thumb/v7-m/nofp\libg.a(libc_a-signalr.o): in function `_kill_r': [build] signalr.c:(.text._kill_r+0xe): undefined reference to `_kill' [build] C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld.exe: C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/../lib/gcc/arm-none-eabi/13.2.1/thumb/v7-m/nofp\libg.a(libc_a-signalr.o): in function `_getpid_r':
Дело в том, что в стандартных библиотеках arm-none-eabi-gcc не предусмотрены реализации этих функций. Чтобы решить данную проблему необходимо подключить дополнительно библиотеки NoSys и Nano. Добавим в CMakeLists.txt следующие строки (рекомендую расположить их там же, где уже расположены существующие add_compile_options и add_link_options):
add_compile_options( $<$<C_COMPILER_ID:GNU>:--specs=nosys.specs> $<$<C_COMPILER_ID:GNU>:--specs=nano.specs> ) add_link_options( $<$<C_COMPILER_ID:GNU>:--specs=nosys.specs> $<$<C_COMPILER_ID:GNU>:--specs=nano.specs> )
Пробуем. Видим, что компилятор выдаёт варнинги
[build] C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/../lib/gcc/arm-none-eabi/13.2.1/../../../../arm-none-eabi/bin/ld.exe: C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/../lib/gcc/arm-none-eabi/13.2.1/thumb/v7-m/nofp\libg_nano.a(libc_a-readr.o): in function `_read_r': [build] readr.c:(.text._read_r+0x10): warning: _read is not implemented and will always fail
однако проект собирается и успешно запускается

P.S.
Настройка clangd в VSCode
Clangd представляет собой language server наподобие Microsoft IntelliSense, однако я нахожу его более удобным ввиду дополнительных возможностей по настройке форматирования кода и проверки синтаксиса (.clang-format и .clang-tidy).
Чтобы он корректно работал в вашем vscode проекте необходимо проделать ряд действий
В настройках CMake Tools в разделе "Copy Compile Commands" прописать
${workspaceRoot}/compile_commands.jsonВ папку .vscode поместить файл settings.json следующего содержания (имя пользователя не забудьте заменить на своё)
{ "cortex-debug.variableUseNaturalFormat": false, "clangd.arguments": [ "--enable-config", "--log=verbose", "-header-insertion=never", "--query-driver=C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/arm-none-eabi-gcc.exe,C:/Users/Filipp/scoop/apps/gcc-arm-none-eabi/current/bin/arm-none-eabi-g++.exe", ] }
В CMakeLists.txt добавить команду
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
P.P.S
почему C++
На хабре уже не раз поднималась тема удобства и способов использования C++ в микроконтроллерах - например, в статьях уважаемого@lamerok. Для себя же я выделил следующие преимущества:
Классы позволяют использовать более простую структуру проекта, а также более наглядно "демонстрируют" работу с различными единицами одних и тех же сущностей
Благодаря шаблонам можно управлять настройками структур/классов/функций ещё на уровне компиляции. Один из простых примеров управление размером буфера в драйвере UART контроллера, притом что сам буфер инкапсулирован внутри класса и при этом выделяется статически на этапе компиляции, а не динамически при старте программы
В C++20 появились концепты, благодаря чему можно ещё более просто абстрагироваться от различных низкоуровневых реализаций интерфейсов, что обеспечивает более простую миграцию с одного микроконтроллера на другой
Используя возможности метапрограммирования можно существенно уменьшить размер прошивки, т.к. часть вычислений будет выполнена ещё на этапе компиляции, а в программе будет сохраняться лишь результат вычислений