На хабре уже есть немало информации об отладке МК в VSCode на Linux (тыц, тыц), также было написано как настроить тулчейн для работы под Windows в QT Creator, Eclipse, etc.

Пришло и моё время написать похожую статью, но для VS Code и под Widnows.

Инициализация проекта будет проводиться с помощью STM32CubeMX. Сборкой будет управлять CMake с тулчейном stm32-cmake. В качестве компилятора используется ARM GNU Toolchain. Тестовым стендом является NUCLEO-F446ZE.

Источниками вдохновения послужили:

Предисловие окончено, приступаем к настройке.

Установка необходимых утилит

Для удобства установки будем пользоваться пакетным менеджером Scoop.

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

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser;
irm get.scoop.sh | iex

Добавим необходимые репозитории

scoop bucket add main
scoop bucket add extras

И, наконец, сами программы:

scoop install meson
scoop install ninja
scoop install cmake
scoop install llvm
scoop install gcc-arm-none-eabi
scoop install stlink
scoop install openocd
scoop install git

meson, ninja, cmake, llvm и gcc-arm-none-eabi используются для конфигурации и сборки проекта, stlink и openocd являются gdb-серверами, git необходим для подключения различных тулчейнов.

P.S.

Если у вас уже есть что-то из этого и вы можете вызвать его через консоль (т.е. программа добавлена в path) то советую либо убрать её из скрипта либо удалить у себя, и установить через scoop.

Настройка VS Code

Для работы потребуются установить в VS Code следующие расширения:

Инициализация проекта

Открываем CubeMX и создаем проект для нашей платы. Всю периферию оставляем настроенной по умолчанию.

В параметрах проекта (Project Manajer) выбираем Make в качестве тулчейна

Настройки Project Manajer
Настройки Project Manajer

В параметрах генератора кода указываем подключение бибилотек только в виде ссылок

 Настройки Code Generator
Настройки Code Generator

Настройка системы сборки

Открываем папку проекта в VS Code и вызываем терминал командой Ctr+~
Скачиваем stm32-cmake

git clone --recurse-submodules -j8 https://github.com/ObKo/stm32-cmake.git

Также потребуются файлы .clang-format, .clang-tidy , fetch_svd.cmake , и CMakeLists.txt из репозитория stm32-template. Для удоства клонируем его в соседнюю директорию.

git clone https://github.com/Dooez/stm32-template.git ../stm32-template

.clang-format, .clang-tidy необходимы LLVM, а fetch_svd.cmake используется для поиска файла описания регистров конкретного микроконтроллера.

Отредактируем CMakeLists.txt под наш проект.

Изменим переменную MCU на STM32F446ZE

set(MCU STM32F446ZE)

По умолчанию "кубик" инициализирует на плате NUCLEO-F446ZE USART3, USB_OTG_FS и несколько GPIO. Добавим библиотеки в проект, для этого необходимо для сборки прописать команду target_link_libraries. Также добавим библиотеку CMSIS и, для уменьшения размеров прошивки, Newlib Nano и NoSys

target_link_libraries(${PROJECT_NAME}
    HAL::STM32::${MCU_FAMILY}::RCC
    HAL::STM32::${MCU_FAMILY}::GPIO
    HAL::STM32::${MCU_FAMILY}::UART
    HAL::STM32::${MCU_FAMILY}::CORTEX
    HAL::STM32::${MCU_FAMILY}::LL_USB
    HAL::STM32::${MCU_FAMILY}::PCD
    HAL::STM32::${MCU_FAMILY}::PCDEx
    CMSIS::STM32::${MCU_MODEL}
    STM32::Nano
    STM32::NoSys
)

Чтобы CMake мог увидеть файлы, сгенерированные кубиком, необходимо добавить их в Include Path и явно указать исполняемые c/cpp файлы.

add_executable(${PROJECT_NAME} 
Core/Src/main.c
Core/Src/stm32f4xx_it.c 
) 

target_include_directories(${PROJECT_NAME} PRIVATE 
${CMAKE_CURRENT_SOURCE_DIR} 
${CMAKE_CURRENT_SOURCE_DIR}/Core/Inc 
${CMAKE_CURRENT_SOURCE_DIR}/Core/Src
)

main.c содержит, собственно, функцию main(), а в stm32f4xx_it.c находится функция, которая считает количество срабатываний SysTick, без которой не будут работать такие функции как HAL_Delay()

Также для уменьшения размера исполняемого файла, добоавим следующие директивы компилятора:

target_compile_options(${PROJECT_NAME} PUBLIC -Os -fno-exceptions -fno-rtti)

Настройка проекта под VS Code

Нажимаем сочетание клавиш Ctrl+Shift+P и в появившейся строке находим
Preferences: Open Workspace Settings (JSON)

В создавшемся файле .vscode/settings.json указаны параметры для расширений и корректного отображения кода. Пишем:

{
  "cmake.generator": "Ninja",
  "cmake.configureEnvironment": {
    "CMAKE_EXPORT_COMPILE_COMMANDS": "on"
  },
  "C_Cpp.default.intelliSenseMode": "gcc-arm",
  "cortex-debug.gdbPath": "arm-none-eabi-gdb",
  "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools"
}

Далее по тому же сочетанию находим Tasks: Configure Task и выбираем cmake build

В создавшийся файл .vscode/tasks.json добавляем задания для прошивки и очистки памяти микроконотроллера с помощью st-flash. Итоговый файл tasks.json выглядит следующим образом:

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "cmake",
      "label": "CMake: build",
      "command": "build",
      "targets": [
          "ALL_BUILD"
      ],
      "problemMatcher": [],
      "group": "build"
    },
    {
      "type": "shell",
      "label": "flash",
      "command": "st-flash",
      "args": [
        "--reset",
        "write",
        "${input:workspaceFolderForwardSlash}/build/${workspaceFolderBasename}.bin",
        "0x8000000"
      ],
      "options": {
        "cwd": "${workspaceFolder}/build"
      },
      "dependsOn": "CMake: build",
      "problemMatcher": [],
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "detail": "Builds project and flashes firmware."

    },
    {
      "type": "shell",
      "label": "erase",
      "command": "st-flash",
      "args": [
        "--connect-under-reset",
        "erase"
      ],
      "detail": "mass erase of chip"
    }
  ],
  
  "inputs": [
    {
      "id": "workspaceFolderForwardSlash",
      "type": "command",
      "command": "extension.commandvariable.transform",
      "args": {
        "text": "${workspaceFolder}",
        "find": "\\\\",
        "replace": "/",
        "flags": "g"
      }
    }
  ]
}
Также при желании можно добавить команду для прошивки с помощью OpenOCD

Для STM32F4 она выглядит следующим образом

{
      "type": "shell",
      "label": "flash-openocd",
      "command": "openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c 'program ${input:workspaceFolderForwardSlash}/build/${workspaceFolderBasename}.bin verify reset exit' ",
      "dependsOn": "CMake: build",
      "problemMatcher": [],
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "detail": "Builds project, connects to the openOCD server and flashes new firmware."
    }

Далее необходимо сконфигурировать расширение CMake для VS Code

Нажимаем сочетание клавиш Ctrl+Shift+P и в появившейся строке находим
CMake: Configure и выбираем конфигурацию под arm-none-eabi

После конфигурации автоматически сгенерируется файл .vscode/launch.json, рассмотрим его поподробнее:

{
  "configurations" : 
  [
    {
      "cwd" : "${workspaceRoot}",
      "device" : "STM32F446ZE",
      "executable" : "${workspaceRoot}/build/${workspaceFolderBasename}.elf",
      "name" : "Cortex Debug (generated)",
      "preLaunchTask" : "CMake: build",
      "preRestartCommands" : [ "load", "enable breakpoint", "monitor reset" ],
      "request" : "launch",
      "runToEntryPoint" : "main",
      "servertype" : "stutil",
      "showDevDebugOutput" : "raw",
      "svdFile" : "${workspaceRoot}/build/_deps/st-svd-archive-src/STM32F4_svd_V1.8/STM32F446.svd",
      "type" : "cortex-debug"
    }
  ],
  "version" : "0.2.0"
}

svdFile – путь до файла, который необходим, чтобы просматривать регистры периферии МК

Картинка

"preLaunchTask": CMake: build – компилирует проект перед прошивкой МК.

preRestartCommands – отправляет команды через GDB при нажатии на кнопку перезапуска отладки

Скрипт fetch_svd.cmake по умолчанию использует в качетсве GDB-сервера stutils. Примеры конфигурации под OpenOCD и JLink можно посмотреть на вики cortex-debug в приложенных ссылках.

Переходим к коду (наконец-то)

Не мудрствуя лукаво, пойдём мигать светодиодом. (и ещё немного поиграемся с выделением памяти). Изменим main() следующим образом

#include "stdlib.h"

uint8_t* data;
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART3_UART_Init();
  
  /* USER CODE BEGIN 2 */
  data = new uint8_t[16];
  uint8_t const_data[16];
  
  for(int i = 0; i < 16; i++){
    data[i] = i+1;
    const_data[i] = i+1;
  }
  /* USER CODE END 2 */

  /* Infinite loop */
  while (1)
  {
    HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
    HAL_Delay(50);
  }
}

Компиляция проекта осуществляется нажатием клавиши F7 либо сочетанием Ctrl+Shift+B. Так как ранее мы в launch.json указали сборку перед прошивкой, то нам будет достаточно нажать F5 и перейти сразу к отладке. Рассмотрим интерфейс:

Отладочный GUI в VS Code
Отладочный GUI в VS Code

Первая кнопка осуществляет программный сброс (software reset) устройства
Вторая запускает код (горячая клавиша F5)
Третья, четвёртая и пятая – "шаг" вперёд к следующей функции, "шаг" вперёд к следующей инструкции (т.е. с погружением) и выполнение код до выхода из функции.
Шестая клавиша осуществляет пересборку проекта и перезапуск прошивки.
А седьмая останавливает отладку.

Окно слева содержит следующие разделы:

Отладочный GUI в VS Code
Отладочный GUI в VS Code
  • Cortex Registers – регистры процессора

  • Cortex Peripherals – регистры периферии (например, там можно смотреть и изменять состоянием регистров GPIO и мигать светодиодом с помощью мышки, хехе)

  • Breakpoints – список выставленных прерываний. Отмечу, что у разных микроконтроллеров и отладчиков допустимо различное число брейкпоинтов (Например, у ST-Link V2.1 их всего 6)

  • В CallStack можно посмотреть очередь вызова (вплоть до main, что логично)

  • Раздел Variables позволяет просматривать как локально объявленные переменные, так и глобальные, например uwTick, показывающую количество милисекунд от момента запуска МК

  • В Memory View можно посмотреть в любой доступный раздел памяти МК

Адреса недоступные к прочтению (по причине отсутствия в этом мире на данном МК) показаны тильдой
Адреса недоступные к прочтению (по причине отсутствия в этом мире на данном МК) показаны тильдой
Рассмотрим возможности Watch Window (и заодно сравним его с Keil MDK)

Массив const_data был объявлен статически, и его можно посмотреть просто по названию, тут всё как везде

А теперь попробуем посмотреть содержимое динамически выделенного массива:

Здесь, так же как и везде, дебаггер отобразит лишь первый элемент (в кавычках можно увидеть содержимое до первого \0 ). Однако, в отличие от, например, Keil MDK, мы можем явно указать, как именно следует воспринимать данный указатель:

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

Также мы можем переопределить этот указатель написав, например, такой запрос:
*(uint16_t*)data@8
Тогда в Watch Window будет показано отображение массива типа short, а не uchar

Комментарии (27)


  1. BARSRAB
    28.01.2023 20:51
    -2

    Ээээ, а зачем танцы с бубном и VSCode, когда можно работать прямо в VisualStudio с VisualGDB?..


    1. GennPen
      28.01.2023 21:25
      +6

      Например, потому что VisualGDB платный.


      1. lazbaphilipp Автор
        29.01.2023 09:00
        +2

        Кроме того, VSCode весит сильно меньше Visual Studio и имеет довольно много приятностей из расширений.


  1. juramehanik
    28.01.2023 22:45
    +2

    Спасибо, есть пара интересностей, но столько утилит и фреймворков чтоб светодиодом комфортнее помигать....

    Куб и так генерирует мейкфайл, которому достаточно указать путь для gcc в tasks.json , хотелось бы увидеть пример кода или проекта, где имеет смысл наворачивать столько фарша помимо мейка. Или щас в ойти прям увольняют пожизненно, если не дай бох руками мейкфайл править?


    1. lazbaphilipp Автор
      29.01.2023 09:04

      Не понял про увольнение.

      Я пробовал собирать и мейкфайлом из кубика в том числе. Никаких проблем с этим не было.

      Насчёт фарша - тут из отличий от "обычного" проекта только cmake. Естественно, он не обязателен для отладки в VSCode, однако он даёт определённый набор преимуществ в плане подключения библиотек и автонастройки расширения c/c++


      1. juramehanik
        29.01.2023 10:41

        Ничем обидеть не хотел, просто очень часто слышу мол использование чистого make это плохой подход старых дедов и вообще плохо плохо как goto. Просто при этом не приводятся примеры проектов на на bare metall где наворачивание cmake оправдано.


        1. lazbaphilipp Автор
          29.01.2023 12:15
          +1

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

          Однако я могу привести пример (пусть и только в виде описания). В одном из проектов у меня есть несколько очень похожих по структуре и функционалу устройств. Бо́льшая часть кода у них одинаковая, различие лишь в одном файле - парсере команд. При использовании CMake я могу держать парсер обоих устройств в одном проекте и собирать сразу два исполняемых файла без дублирования кода.

          В упомянутых в статье видео EbeddedGeek показаны настройки VSCode для работы с чистым Makefile. Также в рунете есть статья по модификации его для совместной компиляции c и cpp файлов.
          В целом, всё неплохо, кубик не удаляет и не переписывает пользовательские строки в мейкфайле(только что проверил), однако нужно настраивать c_cpp_properties и launch ручками и искать svd файл(что тоже не проблема, в целом, так как это делается один раз при создании проекта).

          И ещё небольшая поправка к первому вашему комментарию: сейчас установщик ARM GNU Toolchain прописывает путь до компилятора в $Path так что достаточно написать имя компилятора.


    1. DSarovsky
      29.01.2023 10:41

      Если задача только собрать проект (и тесты, например, запустить), то cmake скрывает необходимость установки фреймворков, так как умеет с github-а подтянуть, остается лишь компилятор поставить.


  1. kenny5660
    29.01.2023 05:15
    +4

    Для VsCode и STM32 есть плагин PlatfromIO, по факту бесплатный аналог VisualGDB, о котором говорили выше. Но PlatfromIO по моему мнению обладает куда большим функционалом, статический анализатор, анализ использованной памяти, удаленная отладка через tcp/ip, запуск Unit тестов, как на мк так и нативно на ПК (только аппаратно-независимый код).


    1. lazbaphilipp Автор
      29.01.2023 09:07

      В PlatformIO платная отладка. Статический анализатор есть и без него, кстати. Он появляется при подключении пакета расширений C/C++


      1. DSarovsky
        29.01.2023 10:25
        +2

        Только удаленная отладка платная, локально все хорошо (что логично ведь это простой openocd). Единственное, что пока в Platformio напрягает - это несвежесть компилятора, что решается путем скачивания и замены файлов в одной директории.

        P.S. Чтобы два раза не вставать: использованный ObKo/stm32-cmake нормально из коробки подгружает библиотеки при их отсутствии? Мне не удалось, пришлось править в одном месте явный вызов MakeAvailable подставить.


        1. lazbaphilipp Автор
          29.01.2023 10:51

          Что имеется в виду под отсутствием?
          Отсутствие в папке проекта? Я в своём примере их и так не включал. Я проверил его работу на двух семействах - F1 и F4, пока проблем не было, всё скачивалось.

          Если же вопрос про отсутствие библиотек в репозитории, откуда он их подгружает, то тут не могу ничего сказать.


          1. DSarovsky
            29.01.2023 11:15

            Не могу назвать себя спецом в cmake, но насколько понял из исходников, сценарий там такой примерно:

            1. Если указаны STM32_CUBE_${FAMILY}_PATH или STM32_CMSIS_${FAMILY}_PATH, то взять оттуда.

            2. Если не указаны, то считать, что кубовские библиотеки лежат по пути /opt/STM32Cube${FAMILY}

            В utilities.cmake есть функции stm32_fetch_XXX, подтягивающие CMSIS/HAL с github, но я так и не нашел, где они вызываются и вставил руками.

            Вопрос такой в итоге: без установленных заранее CMSIS/HAL у вас всё нормально собралось?


            1. lazbaphilipp Автор
              29.01.2023 11:52

              Понял.
              Для проверки я удалил репозиторий CubeMX из системы, однако отмечу, что, как можно увидеть в CMakeLists, переменные эти у меня не объявлены. Папки /opt/ в Windows тоже нет.

              Результат такой: в момент конфигурации у меня ненадолго возрастает до максимума нагрузка на сеть, если подпапка /build отсутствует в проекте. Данные подтягиваются, проект собирается.


              1. DSarovsky
                29.01.2023 12:01

                Спасибо, что-то я делал не так, значит, пойду разбираться:)


      1. lazbaphilipp Автор
        29.01.2023 10:38

        Перепроверил. Похоже, что я неправ – и отладка, и весь IDE в целом бесплатны.

        Меня смутили некоторые особенности его работы, однако в остальном - прекрасный инструмент


  1. Avlaak
    30.01.2023 00:23

    По своему опыту понял, что для меня пока что ничего кроме Keil не подходит под серьёзные проекты, даже допотопность Keil нивелируется компилятором, в сравнении с ARM Compiler 6 все конкурирующие компиляторы проигрывают, это начинаешь понимать только когда проект большой и сложный, когда вопрос памяти и скорости работы стоит во главе угла. Пробовал связку VS code + Embedded IDE (https://em-ide.com/) с подключением ARM Compiler, но всё равно есть проблемы со скоростью отладки, хотя как у редактора кода у VS code нет конкурентов сейчас.


    1. lazbaphilipp Автор
      30.01.2023 00:57

      Я сравнил объем кода, генерируемого armclang v6.17 (из кейла 5.38а самого последнего) и arm-none-eabi-gcc в тулчейне с newlib nano.

      И тут такой момент: при оптимизации с флагом -O0 действительно код из кейла меньше где-то на 0.7 КБ, однако при оптимизации -Oz + LTO код из кейла оказывается больше тоже почти на килобайт.

      Скорость работы я не проверял, надо собрать оба варианта с -Os и запустить какой-нибудь алгоритм достаточно сложный


      1. mctMaks
        30.01.2023 12:18

        вы сейчас сравниваете LLVM (ака clang) и GCC, отсюда и разница в объемах результирующего кода. Для интереса, сравните выхлоп 5 версии компилятора (он на основе gcc как раз) с arm-none-eabi-gcc. Разница должна быть минимальной в таком кейсе.

        хотя кейлы как раз 6 версию своего компилятора активно продвигают по типу он "быстрее\компактней"


        1. lazbaphilipp Автор
          30.01.2023 12:23

          ARM сейчас вообще кейл поддерживают просто потому что, а продвигают они свою Web IDE.

          Да, действительно, я сравниваю два разных по принципу алгоритма, однако именно в этом и суть. Зачем сравнивать одинаковое?

          Изначально в моём ответе был упомянут и ARMCC, главная проблема которого в том, что он очень очень очень медленно работает. Ну и ещё он не поддерживает даже c++11, как я понял по опыту. Но прошивки на его выходе получаются действительно компактными. Возможно, за счёт того, что код, который он всё же может обработать, это си и си с классами.


          1. mctMaks
            30.01.2023 14:20

            если я правильно помню, адекватная поддержка С++ у кейла появилась именно в armclang.

            Да, действительно, я сравниваю два разных по принципу алгоритма, однако именно в этом и суть. Зачем сравнивать одинаковое?

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

            Изначально в моём ответе был упомянут и ARMCC, главная проблема которого в том, что он очень очень очень медленно работает

            Давно ушел от кейла, однако пробовал решение от Segger (их собственная сборка на основе armclang) которое выдает более компактную прошивку, по сравнению с тем же gcc. Разница на одном и том же проекте составила более 2кБ. Основное отличие при просмотре ассемблерного кода (у меня по крайней мере) в использовании одной инструкции вместо двух при загрузке int32 в регистры, однако скорости это не добавляет, так как инструкция все равно выполняется за 2 такта.


            1. lazbaphilipp Автор
              30.01.2023 15:18

              Почему два такта? У Cortex-M 32битное АЛУ, но 16 битная шина?


              1. mctMaks
                31.01.2023 10:51

                Хороший вопрос, не задумывался. Исходил из описания инструкций отсюда:

                https://developer.arm.com/documentation/ddi0439/b/Programmers-Model/Instruction-set-summary/Cortex-M4-instructions

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


                1. lazbaphilipp Автор
                  31.01.2023 10:58

                  Про какие команды речь? Судя по тому, что написано в референсе по ссылке, все Load/Store команды выполняются не меньше двух тактов. Полагаю, что segger используют другие флаги для компилятора, и оптимизируют где-то ещё.


                  1. mctMaks
                    31.01.2023 11:53
                    +1

                    оптимизация одинаковая стоит.

                    GCC выдавал две инструкции (не помню какие именно, возможно использовался MOV два раза, что эквивалентно одному LDR), а CLANG - одну.

                    По тактам время исполнения получилось одинаковым. Кода получаем меньше (число инструкций то другое), а время исполнения при этом не меняется.

                    Будет пауза на работе, попробую повторить эксперимент


  1. devprodest
    30.01.2023 23:19

    Кстати и сам clang нормально собирает под arm bare metal, только нужно ему библиотеки сишные подсунуть во время линковки хоть даже от гцц или же собрать себе из исходников ллвмные.


    1. lazbaphilipp Автор
      31.01.2023 11:00

      Ну да, транслятор из IR кода LLVM в ARM уже есть давно.

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