Как подключить библиотеку cube ide

Обновлено: 05.07.2024

Вторая часть про настройку NUCLEO-F746ZG для работы по интерфейсу I2C. В предыдущей части был создан, настроен проект в STM32CubeMX, после чего сгенерирован исходный код для STM32CubeIDE. Основные файлы хранятся в этом репозитории .

Основной файл, с которым будем работать, называется main.c .

В этой части будет решены следующие задачи:

1. Поиск устройств на шине I2C

2. Отправка данных slave-устройству

3. Получение данных от slave-устройства

Формальная организация

На этапе создания проекта в Кубе мы оставили галку на "Keep User Code when re-generating ". Однако, чтобы написанный код сохранялся, нужно его размещать внутри блоков:

, где вместо I2C2_Init 2 может быть другое название другого блока кода.

Поиск устройств по шине

Следующая команда проверяет наличие устройств на шине I2C. Следует помнить, что значение искомого адреса перед отправкой нужно сдвинуть влево согласно даташиту.

* @param DevAddress Target device address: The device 7 bits address value
* in datasheet must be shifted to the left before calling the interface

Адрес у нас 7 битный, а значит количество возможных адресов у нас ограничено 127. В результате получаем следующий алгоритм:

Интерфейс I2C использует сигнал Acknowledgement (Подтверждение) для идентификации наличия устройства на опрашиваемом адресе. Ниже приведены примеры отсутствия и наличия ответа от опрашиваемого устройства

Подтверждение устройства по адресу 0x58 не было получено Подтверждение устройства по адресу 0x58 не было получено

Отправка данных устройству

Если мы нашли что-либо на шине устройства, то следующим шагом будет передача данных на это устройство.

HAL_I2C_Master_Transmit(&hi2c2, (TargetI2Cdevice<<1), ptI2Cbuffer2transmit, 4, 10);
2. ptI2Cbuffer2transmit -- буфер с 4 значениями, которые необходимо передать
3. Последнее значение, равное 10, указывает промежуток ожидания между отправкой данных

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

Верхний график иллюстрирует работу по линии SDA, нижний график по линии SCL. Отчетливо видны сигналы тактирования при передаче данных. Верхний график иллюстрирует работу по линии SDA, нижний график по линии SCL. Отчетливо видны сигналы тактирования при передаче данных.

Получение данных от устройства

Чтобы получить данные, нужна похожая команда:

HAL_I2C_Master_Receive(&hi2c2, (TargetI2Cdevice<<1), ptI2Cbuffer4receive, I2C_RECEIVE_CNT, 10);
1. ptI2Cbuffer4receive -- буффер, в который будут записаны данные
2. I2C_RECEIVE_CNT -- макрос, указывающий количество данных для получения

В моем случае, я получаю 80 значений по шине, поэтому график получается немного ненаглядный.

Тактирование на графике слилось в сплошную полосу из-за масштаба, зато заметно, что для передачи 80 значений потребовалось около 2 миллисекунд. Тактирование на графике слилось в сплошную полосу из-за масштаба, зато заметно, что для передачи 80 значений потребовалось около 2 миллисекунд.

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

На самом в устройстве этот разрыв приведен к ошибке типа Acknowledge Failure.

Немного о скорости стандартной библиотеки HAL и I2C. Изначально, поставим задачу: сколько можно передать данных за 10 мс, иначе при частоте отправке пачек 100 Гц?

Опытным путем было получено, что наиболее стабильная передача данных получается на данной частоте при следующих характеристиках:

Урок 13. Разработка и использование классов в C++. Создание класса обработки дискретных сигналов Debounce.

Уроки STM32

Продолжение темы программной обработки дискретных сигналов. Разработаем класс, реализующий представленные в предыдущем уроке алгоритмы. Заодно вспомним, что такое классы, и как их использовать.

Давайте разработаем класс, который реализует функции обработки дискретных сигналов по алгоритмам из предыдущего урока.

Попробуем “убить двух зайцев”:

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

Можете еще обратиться к уроку 7 курса Уроки Ардуино. Там тема классов рассматривается доступнее и более подробно.

Почему класс, а не функция?

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

В предыдущих уроках для простого чтения состояния входов мы использовали функцию, например, HAL библиотеки. В качестве аргументов передавали информацию, какой вход необходимо прочитать, получали результат, и все данные вычислений внутри функции забывали. Они нам были больше не нужны.

Для новой задачи такой способ не подходит. Нам постоянно надо использовать результаты предыдущих вычислений. Можно, конечно, создать глобальные переменные для функции. Или объявить внутри ее переменные со статической продолжительностью (static). Некрасиво, но терпимо, если бы мы обрабатывали только один сигнал. А в системе их может быть несколько.

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

Классы в C++.

Классы позволяют создавать новые типы объектов, объединяющие в себе данные и функции для работы с ними.

Классы состоят из свойств и методов. Свойства – это данные объекта. Методы – это функции для действий над его свойствами.

  • Свойства класса – это его переменные.
  • Методы класса – это функции.

Класс объявляется с помощью ключевого слова class.

Члены класса это переменные, функции, другие классы и т.п.

Думаю, все это вы знаете из уроков Ардуино. Займемся конкретным примером.

Создание класса обработки дискретных сигналов STM32.

Я решил назвать класс Debounce. В переводе - устранение дребезга, дрожания, выбросов.

Создадим проект Lesson13_1. С помощью STM32CubeMX установим в нем только конфигурацию системы тактирования (урок 5).

Преобразуем проект в C++ (урок 11).

Объявим наш класс, укажем свойства и методы для него. Все действия будем производить в файле main.cpp.

class Debounce
public:
Debounce(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, uint32_t filterTime); // конструктор
void scanStability(void); // метод ожидания стабильного состояния сигнала
void scanAverage(void); // метод фильтрации сигнала по среднему значению
void setTime(uint32_t filterTime); // метод установки времени фильтрации
uint8_t readFlagLow(void); // чтение признака СИГНАЛ В НИЗКОМ УРОВНЕ
uint8_t readFlagRising(void); // чтение признака БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
uint8_t readFlagFalling(void); // чтение признака БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ

uint8_t flagLow; // признак СИГНАЛ В НИЗКОМ УРОВНЕ
uint8_t flagRising; // признак БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
uint8_t flagFalling; // признак БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ

private:
GPIO_TypeDef *_GPIOx; // порт
uint16_t _GPIO_Pin; // маска вывода
uint32_t _filterTime; // время фильтрации
uint32_t _filterTimeCount; // счетчик времени фильтрации
>;

Я выбрал следующие методы и свойства.

Debounce(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, uint32_t filterTime)

Это конструктор. Он вызывается при создании объекта, инициализирует переменные, конфигурирует регистры порта.

  • GPIOx – выбор порта. (GPIOA, GPIOB, GPIOC … ).
  • GPIO_Pin – Номер вывода (используется маска).
  • filterTime – число подтверждений сигнала или время усреднения.

Конструктор сам конфигурирует заданный вывод на режим входа с подтягивающим резистором.

Debounce button(GPIOA, 1 << 14, 30); // экземпляр класса Debounce, вывод PA14, число подтверждений 30

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

А сейчас, нам необходимы функции обработки сигнала, которые должны регулярно вызываться с заданным периодом. Они будут формировать признаки состояния. У нас 2 алгоритма обработки, поэтому объявим 2 метода.

void scanStability(void); // метод ожидания стабильного состояния сигнала
void scanAverage(void); // метод фильтрации сигнала по среднему значению

Эти методы не имеют аргументов, ничего не возвращают.

Для задания числа подтверждений сигнала создадим еще один метод. В принципе этот параметр можно устанавливать через конструктор при создании объекта, но вдруг возникнет необходимость изменять его оперативно.

void setTime(uint32_t filterTime); // метод установки времени фильтрации

Результатом работы класса будут свойства-признаки:

uint8_t flagLow; // признак СИГНАЛ В НИЗКОМ УРОВНЕ
uint8_t flagRising; // признак БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
uint8_t flagFalling; // признак БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ

Первый из них ( flagLow ) показывает текущее состояние сигнала. При низком уровне сигнала он равен 1. Как правило, активным назначают именно низкий уровень. Так меньше влияние помех на сигнал.

Признаки flagRising и flagFalling формируются на перепады (фронты) сигнала.

  • flagRising - изменение уровня сигнала с низкого на высокий;
  • flagFalling - изменение уровня сигнала с высокого на низкий.

Это признаки с памятью. Они выставляются в активное состояние в методах обработки сигнала. Сбрасываться признаки должны в программе при отработке события.

if(button.flagFalling != 0)
// кнопку нажимали
button.flagFalling = 0; // сброс признака

Правила хорошего тона для объектно-ориентированного программирования говорят, что у объектов не должно быть свойств типа public, т.е. доступных из любого места программы. К свойствам класса необходимо обращаться только через его методы.

Но вызов любой функции очень затратная по времени процедура. Поэтому я разместил признаки в блок public. Для критичных по времени выполнения программ можно использовать прямое обращение к ним.

Узнать состояние сигнала можно и через методы.

uint8_t readFlagLow(void); // чтение признака СИГНАЛ В НИЗКОМ УРОВНЕ
uint8_t readFlagRising(void); // чтение признака БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
uint8_t readFlagFalling(void); // чтение признака БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ

Методы для чтения признаков flagRising и flagFalling сбрасывают соответствующие признаки, если они были активными.

if(button.readFlagFalling() != 0)
// кнопку нажимали

Теперь надо разработать тела методов. Не буду их приводить. Посмотрите в окончательном варианте проекта в конце урока.

Для обращения к регистрам используется библиотека CMSIS. Таким образом, класс не зависит от подключенных библиотек HAL или LL. Да и на обработку сигналов тратится значительно меньше времени.

Проверка.

Все класс готов. Проверяем его на той же схеме с кнопкой и светодиодом.

Схема макета

Макет

Объявим экземпляр нашего класса ниже его определения.

Debounce button(GPIOB, 1 << 12, 10); // экземпляр класса Debounce

Мы создали объект button. Задали вывод PB12 и установили 10 подтверждений.

Известным нам способом конфигурируем вывод PB13 на выход. К нему подключен светодиод.

/* USER CODE BEGIN SysInit */

__HAL_RCC_GPIOB_CLK_ENABLE(); // разрешение порта B

/* конфигурация вывода PB13 на активный выход */
GPIO_InitStruct.Pin = GPIO_PIN_13; // номер вывода
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // режим выход
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM; // средняя скорость выхода
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

Конфигурацию вывода кнопки PB12 мы не устанавливаем. Это должен сделать класс Debounce.

В основном цикле мы вызываем метод обработки scanStability( ) и задаем задержку 1 мс.

Это не совсем нормальное использование класса. Мы создавали его для работы в фоновом режиме, а сами вызываем в основном цикле с зависанием в HAL_Delay() . Но для проверки подойдет. Главное, что метод обработки вызывается регулярно, в нашем случае с периодом 1 мс. Считайте пока, что эти строки мы не видим. Они вызываются где-то в другом месте.

Теперь мы можем использовать признаки состояния кнопки.

Сделаем функциональный аналог предыдущих программ: кнопка нажата – светодиод светится, отжата – погашен.

/* USER CODE BEGIN WHILE */

while (1)

if(button.readFlagLow() != 0)
// кнопка нажата
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET); // сброс вывода PB13
>
else
// кнопка отжата
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); // установка вывода PB13
>

Можно компилировать и загружать программу в плату. У меня работает.

Давайте проверим алгоритм устранения дребезга.

При создании объекта button мы задали 10 подтверждений. С учетом того, что период вызова метода scanStability() составляет 1 мс, время подтверждения равно 10 мс. Задержку с таким временем глазом заметить невозможно. Давайте увеличим его до 1 сек.

Debounce button(GPIOB, 1 << 12, 1000); // экземпляр класса Debounce

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

Попробуем тоже самое для алгоритма фильтрации сигнала по среднему значению. Изменим метод обработки.

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

Проверим, как класс выделяет фронты сигналов.

Debounce button(GPIOB, 1 << 12, 10); // экземпляр класса Debounce

Инверсия светодиода по отрицательному фронту, т.е. на нажатие кнопки.

while (1)

if(button.readFlagFalling() != 0)
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
>

Инверсия светодиода по положительному фронту, т.е. на отжатие кнопки.

if(button.readFlagRising() != 0)
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
>

Я проверил тоже самое для метода scanAverage() и для других выводов платы.

Полный проект урока можно загрузить по ссылке:

Зарегистрируйтесь и оплатите. Всего 60 руб. в месяц за доступ ко всем ресурсам сайта!

Ну все. Классом можно пользоваться.

В следующем уроке научимся создавать библиотеки для STM32. Оформим класс Debounce библиотекой. И научимся подключать пользовательские библиотеки к проекту.

STM32 + CMSIS + STM32CubeIDE

Здесь я расскажу как создать минимальный проект на CMSIS с использованием «родной» IDE для микроконтроллеров STM – STM32CubeIDE.

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



Объектом прошивки выбран не очень распространенный микроконтроллер STM32F072 с ядром ARM Cortex-M0, для более привычных STM32F103 на ARM Cortex-M3, с поправкой на ядро, процесс идентичен.

    , я использую Windows версию, но также доступны версии под Mac и Linux





Создание нового проекта — File/New/STM32Project. После некоторого раздумия появляется окно выбора микроконтроллера, в моем случае это STM32F072RB в корпусе LQFP64, выбираю нужную строку, жму далее. Далее предлагается выбрать имя проекта, расположение, язык программирования C/C++, исполняемый файл/статическая библиотека и будет-ли проект сгенерирован с помощью CubeMX или сами с усами. Генерация кубом, в данном случае не нужна, поэтому тип проекта Empty — финиш.




Слева, в окне Project Explorer, появилось дерево проекта, правда он не совсем Empty, как заказывали. Впринципе, если устраивает сгенерированная структура папок, можно добавить туда файлы из библиотеки CMSIS и работать дальше, но здесь я покажу как можно привести структуру проекта в гармонию со своим чувством прекрасного, поэтому удаляется всё, кроме скрипта линкера т.е. файла c расширением .ld — он еще пригодится.



Все манипуляции с папками и файлами можно проводить как в проводнике так и внутри IDE, нажав правой кнопкой на название проекта, к примеру: правая кнопка –> new –> Folder. Если структура проекта изменялась вне IDE, то нужно просто обновить проект: правая кнопка –> Refresh.

Мой вариант структуры проекта выглядит так:

  • Startup – здесь будет храниться скрипт линкера, тот самый, оставшийся от сгенерированного проекта, а также startup файл взятый из CMSIS
  • CMSIS\src и CMSIS\inc – здесь будут лежать исходники, файлы с расширением .c в папке scr и заголовочные файлы с расширением .h в папке inc соответственно, относящиеся к библиотеке CMSIS
  • Core\src и Core\inc – здесь будет расположен собственно сам проект, для начала стоит положить туда main.c и main.h


Теперь нужно перенести файлы библиотеки CMSIS в проект. Библиотека состоит из файлов ядра и файлов периферии. Файлы ядра начинаются с core_ или cmsis_ они общие для всех микроконтроллеров, использующих данное ядро. Файлы периферии содержат в названии наименование микроконтроллера stm32 и специфичны для конкретного производителя, в данном случае, компании STM.

В распакованном виде архив содержит папку STM32Cube_FW_F0_V1.11.0, все пути указаны относительно этой папки. Итого, нужно скопировать:

  • Drivers\CMSIS\Include\cmsis_compiler.h
  • Drivers\CMSIS\Include\cmsis_gcc.h
  • Drivers\CMSIS\Include\cmsis_version.h
  • Drivers\CMSIS\Include\core_cm0.h
  • Drivers\CMSIS\Device\ST\STM32F0xx\Include\stmf0xx.h
  • Drivers\CMSIS\Device\ST\STM32F0xx\Include\stm32f072xb.h
  • Drivers\CMSIS\Device\ST\STM32F0xx\Include\system_stm32f0xx.h
  • Drivers\CMSIS\Device\ST\STM32F0xx\Source\Templates\system_stm32f0xx.c
  • Drivers\CMSIS\Device\ST\STM32F0xx\Source\Templates\gcc\startup_stm32f072xb.s


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

Правая кнопка по названию проекта -> Properties -> C/C++ Build -> Settings -> Tool Settings -> MCU GCC Linker -> General – здесь нужно указать новое расположение скрипта линкера с помощью кнопки Browse…


Также нужно указать пути к файлам проекта

Properties -> C/C++ General -> Includes
Properties -> C/C++General -> Source Location

В Includes пути к папкам inc, а в Source Location логично было-бы к папкам src, но если так сделать, то в дереве проекта будут отдельно добавлены эти папки. Чтобы не загромождать визуально дерево, в Source Location можно указать корневые папки Core, CMSIS и Startup.



Для того чтобы проект скомпилировался нужно раскомментировать в файле stm32f0xx.h строку с названием микроконтроллера ну и конечно же в main.c добавить функцию main.




Собственно всё. Безошибочная компиляция и сразу же куда-то подевалось целых полтора килобайта памяти ОЗУ она же RAM, и сразу же вспоминается стек и куча, в процессе создания проекта они нигде не упоминались. Величина стека и кучи указана в файле скрипта линкера, тот что с расширением .ld, их можно и нужно изменять в соответствии с требованиями проекта. Эти значения находятся в начале файла в виде меток _Min_Heap_Size/_Min_Stack_Size с указанием размера в шестнадцатеричном виде.


В качестве примера, приведу небольшой проект традиционного мигания светодиодом.

Светодиод будет мигать на отладочной плате STM32F072B-DISCO, тактирование осуществляться от внутреннего генератора HSI48 частотой 48 МГц, а в качестве источника задержки использоваться таймер SysTick, генерирующий прерывания с периодом в 1 мс, при помощи которых отсчитывается точное время задержки. Светодиод подключен к выводу 6 порта С, настроенного на выход push-pull.

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

Локальный запуск юнит-тестов в STM32CubeIDE под Windows


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

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

Существует три способа запуска юнит-тестов для встраиваемых платформ:

  1. Запуск непосредственно на целевой платформе. В этом случае можно работать с аппаратурой устройства, и код будет работать точно так же, как и в боевых условиях. Однако для тестирования будет нужен физический доступ к устройству. Кроме того, цикл тестирования получится достаточно долгим из-за необходимости постоянно загружать код в устройство.
  2. Запуск на эмуляторе. Данный способ хорош в основном тем, что позволяет работать, даже когда целевая платформа недоступна (например потому, что ее еще не сделали). Недостатки – ограниченная точность воспроизведения поведения железа (и окружающего мира), а также трудность создания такого эмулятора.
  3. Запуск на хост-машине (локально). Не получится работать с аппаратурой (можно вместо этого использовать тестовые заглушки), зато тесты будут быстро запускаться и отрабатывать, и не нужен доступ к целевому устройству. Хороший пример для использования этого способа – тестирование реализации на микроконтроллере какого-нибудь вычислительного алгоритма, который сам по себе не зависит от аппаратуры, но использует данные датчиков устройства. Тестировать алгоритм с реальным источником данных будет очень неудобно, гораздо лучше один раз записать эти измерения и гонять тесты уже на сохраненных данных. Этот сценарий с локальным запуском тестов и будет рассматриваться далее.

В этой публикации приведен способ настройки юнит-тестов в среде STM32CubeIDE, основанной на Eclipse и предназначенной для разработки для контроллеров семейства STM32. Язык разработки – С, но сами тесты пишутся на С++. Тесты будут запускаться на хост-машине c Windows с использованием Cygwin. В качестве тестового фреймворка используется Google Test. Результаты будут отображаться в специальном окне плагина для юнит-тестирования, и их можно будет запустить одной кнопкой из проекта для STM32:


Описанный способ подойдет и для других сред разработки на основе Eclipse, если конечно добрые производители не слишком сильно их урезали в угоду удобству разработчиков. Также этот метод будет работать и c CubeIDE под Linux, при этом не потребуется возиться с Cygwin.

Вам понадобятся

  1. Cygwin 3.0.7 x86 (поскольку тесты для 32-битного микроконтроллера, будем и на 64-битной платформе использовать 32-битное окружение)
  2. STM32CubeIDE 1.0.2 для Windows.
  3. Google Test Framework 1.8.1

Установка Cygwin и STM32CubeIDE

Cygwin

Устанавливаем Cygwin, версия x86. В инсталляторе выбираем дополнительные пакеты: gcc-core, g++, binutils, automake, autoconf, cmake, libtool, gdb, make. Можно ставить последние стабильные версии пакетов.


Также нужно прописать переменные среды:

PATH: …;C:\<path_to_Cygwin>\Cygwin\bin; C:\<path_to_Cygwin>\Cygwin\lib
classpath: C:\<path_to_Cygwin>\Cygwin\lib

STM32CubeIDE

Среда устанавливается как обычно. Желательно ставить CubeIDE после Cygwin, потому что в этом случае Cube сам подхватит существующий Cygwin тулчейн.

Сначала создадим проект С++ для x86 Cygwin платформы. Он нам понадобится, чтобы, во-первых, проверить работоспособность тулчейна, а во-вторых, мы будем использовать его как «донора» конфигурации сборки для основного проекта.

Выбираем File > New > C/C++ Project. Выбираем C++ Managed Build. Создаем проект типа hello world для тулчейна Cygwin GCC:


Далее нужно будет выбрать, какие конфигурации сборки создавать. Достаточно только Debug.
Теперь можно проверить, что проект собирается, выбрав Project > Build All. Также желательно проверить и отладку под Cygwin, запустив Run > Debug As > Local C/C++ Application. Приложение выведет «Hello world» в консоль внутри CubeIDE.

Для того, чтобы отладчик мог показывать исполняемые строки в файлах исходного кода, нужно настроить отображение путей. В окне Window > Preferences во вкладке С/С++ > Debug нужно выбрать Source Lookup Path и добавить новое отображение: Add > Path Mapping. В окне нужно назвать как-нибудь новое отображение и добавить строчки для дисков, которые есть в системе:



Для красивого запуска тестов нам также понадобится плагин для Eclipse с поддержкой юнит-тестов для С++. Он ставится прямо из STM32CubeIDE: меню Help > Install New Software, далее выбрать репозиторий Eclipse Repository и установить плагин С/С++ Unit Testing Support.


Сборка библиотеки Google Test

После успешной сборки файл статической библиотеки будет лежать в ./googlemock/lib/libgtest.a, а заголовочные файлы будут находиться в каталоге ./googletest/include/gtest/. Их нужно будет скопировать в наш проект (или прописать путь к этим файлам в настройках проекта).

Создание проекта для STM32

Проект для отладочной платы STM32L476G-DISCO. Пример будет не слишком изощренным – на плате есть два светодиода, пусть показывают двоичный счетчик от 00 до 11. Реализуем для счетчика отдельный модуль, описанный в паре .h и .c файлов, и напишем для него тест.
Проект можно создавать как обычно, с помощью конфигуратора Cube, главное убедиться, что выводы PB2 и PE8 настроены как цифровые выходы. При создании проекта лучше будет указать тип – С++, это понадобится для компиляции тестов (основной код будет по-прежнему компилироваться С-компилятором). Сконвертировать проект из C можно будет и позже, нажав на название проекта ПКМ и выбрав «Convert to C++».

Для компиляции под МК и для тестов нам понадобятся две разные конфигурации сборки. В этих конфигурациях будут собираться разные наборы файлов – в основную попадут модули для работы с железом и тестируемые модули, а в тестовую – те же тестируемые модули и файлы тестов. Поэтому создадим в корне проекта разные каталоги – Application c кодом приложения для МК (можно просто переименовать директорию Src, которую создал Cube), Common для модулей, не зависящих от железа (которые мы будем тестировать) и Tests для тестов. Директории можно исключать из сборки, кликнув ПКМ по их названию, меню Resource Configuration > Exclude from build.

Добавим в каталог Common наш модуль счетчика:

Директории Common и Tests нужно добавить в путь поиска include-файлов: свойства проекта (Properties) > С/С++ General > Paths and Symbols > Includes.

Добавим в main работу со светодиодами

Проект должен компилироваться и запускаться, а светодиоды – мигать.

Написание тестов

Теперь то, ради чего все затевалось.

Создадим новую конфигурацию сборки через свойства проекта – Properties > C/C++ Build > Settings > Manage Configurations. CubeIDE просто так не даст создать конфигурацию для сборки под Cygwin, поэтому скопируем ее из проекта, который мы создали ранее:


Теперь нужно переключиться на эту конфигурацию и настроить пути к файлам исходников и заголовочным файлам. В свойствах проекта во вкладке Paths and Symbols прописываем (при добавлении записи лучше ставить галку в поле «add to all languages»):

  • Includes – Tests/Inc, Common/Inc
  • Libraries – gtest
  • Library Paths – Tests/Lib
  • Source Location — /<prj_name>/Common и /<prj_name>/Tests (заменить <prj_name> на имя проекта)

Далее копируем в проект библиотеку gtest – файл .a в директорию Tests/Lib, а заголовочные файлы в папке gtest – в папку Tests/Inc. В папке Tests создаем новый файл main.cpp, в котором будут запускаться тесты. Его содержимое стандартное:

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

Создаем такой файл теста test_platform.cpp:


Структура проекта должна иметь примерно такой вид:

Теперь напишем тесты для нашего модуля светодиодного счетчика. Файлы тестов можно расположить в папке Tests:

Чтобы результаты тестов отображались в красивом окошке, нужно создать новую конфигурацию запуска в меню Run > Debug Configurations. Установленный плагин позволяет создавать конфигурации типа C/C++ Unit. Создадим ее, назовем Run Tests, выберем используемую конфигурацию сборки «Test» и снимем галку «stop on startup at» на вкладке Debugger. После этого конфигурацию можно запустить.

Для появления окна с результатами его нужно выбрать в Window > Show View > Other > C/C++ > C/C++ Unit.


Готово! Теперь проект можно компилировать и запускать под целевой МК как обычно. Когда нужно будет запустить локальные тесты, при запуске конфигурации Run Tests проект автоматически будет пересобран под x86, среда выполнит тесты и покажет результат.

Читайте также: