Stm32 без IDE — памятка 2: мигаем светодиодом и кое-что ещё
Итак, в прошлый раз мы (надеюсь, и вы тоже) собрали и запустили при помощи консольных программ проект, созданный (и первично собранный) в STM32 CubeIDE. Теперь немного освоимся в нём.
Напомню, у нас есть структура каталогов проекта с исходными файлами (обратим внимание на Core и Drivers), а также аналогичная ей структура в подкаталоге Debug с правилами для сборки исходников в исполняемый файл. (Debug — это на самом деле название профиля сборки; по умолчанию кубик создаёт ещё Release, но, как нетрудно догадаться, он неудобнее для отладки, так что мы его собирать не будем; принципиальной же разницы между ними нет.)
Прежде чем переходить к заявленной заадаче, давайте потыкаем палочкой систему сборки (дело в том, что нам понядобится добавить в неё некоторые новые файлы, так что идём последовательно).
Создадим (любым текстовым редактором; хотя, конечно, Writer или Word будут для этого весьма некстати) файл Core/Src/our_file.c следующего содержания:
int value = 0;
И Core/inc/our_file.h:
extern int value;
Затем в Core/Src/main.c после строки /* USER CODE BEGIN Includes */ добавим #include "our_file.h", а в суперцикл добавим value ++;
/* USER CODE BEGIN WHILE */
while (1)
{
value ++;
Попробуем собрать:
aleksei@RNWS-008 /home/adk/STM32CubeIDE/31-live/lesson1/Debug $ make all | grep -v arm-none-eabi-gcc
/opt/st/stm32cubeide_1.12.0/plugins/com.st.stm32cube.ide.mcu.externaltools.gnu-tools-for-stm32.10.3-2021.10.linux64_1.0.200.202301161003/tools/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-no
ne-eabi/bin/ld: ./Core/Src/main.o: in function `main':
/home/adk/STM32CubeIDE/31-live/lesson1/Debug/../Core/Src/main.c:96: undefined reference to `value'
collect2: error: ld returned 1 exit status
make: *** [makefile:64: lesson1.elf] Ошибка 1
Понятно, в чём дело: компилятор не знает о файле our_file.c, где идёт выделение памяти под переменную value. В GUI кубика мы в таком случае нажимаем ПКМ на нужном файле → Resource configuration → снимаем галочки «Exclude from build». Стало быть, некий аналог этих галочек есть и в генерируемых кубиком makefilах. Откроем файл Debug/Core/Src/subdir.mk. Здесь три переменных — C_SRCS, OBJS и C_DEPS. Первая содержит все исходные файлы текущего каталога, вторая — получающиеся из них объектные файлы. Добавим в них строчки с именем нашего файла и соответствующим объектником:
C_SRCS += ../Core/Src/our_file.c#
OBJS += ./Core/Src/our_file.o#
Признаться, мне больше нравится такой синтаксис, чем склеивание строк через бэкслеши, но если вы решите добавлять строки в сите кубика, не забывайте про пробелы перед бэкслешами. Последняя переменная (C_DEPS) нужна больше для внутренний надобностей CubeIDE, но для единнобразия можете добавить и строчку для неё (по аналогии — базовое имя файла + расширение .d). В системе сборки кубика есть небольшая нелогичность: чтобы добавить объект в сборку, должно не только присутствовать правило для его сборки в файле subdir.mk, но его ещё нужно внести в общий список объектов проекта. Откроем файл Debug/objects.list и добавим в него строку
"./Core/Src/our_file.o"
Теперь соберём ещё раз проект (make), переключимся на терминал с отладчиком (arm-none-eabi-gdb) и нажмём Ctrl-C, чтобы приостановить выполнение прогарммы (аналогично нажатию кнопки «Pause» в интерфейсе CubeIDE). Вновь дадим команду load. Отладчик при этом определит, что файл был обновлён с последнего запуска, и загрузит новый файл в микроконтроллер. Вновь запустим и приостановим программу:
(gdb) load
…
Transfer rate: 13 KB/sec, 1104 bytes/write.
(gdb) b main
Breakpoint 4 at 0x80005d8: file ../Core/Src/main.c, line 105.
(gdb) c
Continuing.
Breakpoint 4, main () at ../Core/Src/main.c:105
105 HAL_Init();
(gdb) p value
$5 = 0
(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
HAL_GetTick () at ../Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c:325
325 return uwTick;
(gdb) p value
$6 = 16850
(gdb)
Отлично. Работает.
Маленькое пояснение: сперва я установил бряк на начало функции main. Дошёл до неё командой c(ontinue). Проверил состояние переменной value (сразу после загрузки прошивки проверять value бессмысленно: она ещё не инициализирована). Продолжил выполнение программы и тут же остановил её уже вручную, комбинацией Ctrl+c. Отладчик написал мне, где я нахожусь, за что ему спасибо, а затем я ещё раз проверил, что переменная докуда-то досчитала.
Сейчас попробуем помигать светодиодом. Для этого нам понадобится подключить в Core/Src/main.c файл stm32f4xx_hal_gpio.h. А вот подключать соответствующие исходники не понадобится, так как конкретно GPIO используется много где, и кубик пропишет его даже для пустого проекта. Останется лишь добавить в наш main.c функцию инициализации (по аналогии с тем, как это делает кубик при создании функции MX_GPIO_Init()):
void our_gpio_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
И вызвать её в main(), где-нибудь во второй секции. А затем в суперцикле помигать HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_10), HAL_Delay(300);
В принципе, на этом всё, осталась пара замечаний:
1) то, что gpio тянется по умолчанию, это хорошо, но давайте рассмотрим чуть иной пример. Скажем, нам нужно подключить АЦП и померить что-нибудь. Если мы хотим работать на чуть более низком уровне, скажем, CMSIS, то нам достаточно подключить заголовочный файл stm32f401xc.h (он лежит среди доступных к подключению каталогов, а именно в Drivers/CMSIS/Device/ST/STM32F4xx/Include). Если же мы, как уважающие себя люди, привыкли оперировать горячими закусками, то заметим, что в Drivers/STM32F4xx_HAL_Driver/Inc лежат далеко не все нужные нам файлы. Здесь мы можем либо скопировать недостающее из репозитория (где он расположен, можно глянуть в кубике: Window→Preferences→STM32Cube→Frimware Updater), либо указать на нужный каталог в строке сборки в файле Debug/Core/Src/subdir.mk (после ключа -I). Здесь я предполагаю первый вариант и скопировал в проект файлы, относящиеся к АЦП: *adc.h и *adc_ex.h — в Drivers/STM32F4xx_HAL_Driver/Inc, а *adc.c и *adc_ex.c — в Drivers/STM32F4xx_HAL_Driver/Src (заметьте, там будут не только файлы HAL, но и LL; впрочем, …ll….h действительно нужен). Добавил их в соответствующий subdir.mk, а также в список объектов. Также неплохо бы добавить соответстующие файлы или шаблоны в команду очистки проекты (внизу файлы Debug/Drivers/STM32F4xx_HAL_Driver/Src/subdir.mk — цель clean-Drivers-2f-STM32F4xx_HAL_Driver-2f-Src) (я добавил ./Drivers/STM32F4xx_HAL_Driver/Src/*.su ./Drivers/STM32F4xx_HAL_Driver/Src/*.o ./Drivers/STM32F4xx_HAL_Driver/Src/*.d ./Drivers/STM32F4xx_HAL_Driver/Src/*.cyclo )
Скомпилировал. Успешно. Но если сейчас попробовать вызвать любую функцию АЦП, то компилятор выдаст ошибку:
/* USER CODE BEGIN PV */
ADC_HandleTypeDef hadc1;
/* USER CODE END PV */
...
/* USER CODE BEGIN 2 */
HAL_ADC_Start(& hadc1);
/* USER CODE END 2 */
aleksei@RNWS-008 ~/STM32CubeIDE/other/test/Debug $ make all | grep -v arm-none-eabi-gcc
/opt/st/stm32cubeide_1.12.0/plugins/com.st.stm32cube.ide.mcu.externaltools.gnu-tools-for-stm32.10.3-2021.10.linux64_1.0.200.202301161003/tools/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-no
ne-eabi/bin/ld: ./Core/Src/main.o: in function `main':
/home/aleksei/STM32CubeIDE/other/test/Debug/../Core/Src/main.c:88: undefined reference to `HAL_ADC_Start'
collect2: error: ld returned 1 exit status
make: *** [makefile:64: test.elf] Ошибка 1
Дело в том, что о нашем намерении собрать данную часть HAL нужно уведомить не только утилиту make, но и саму библиотеку. Для этого необходимо найти в файле Core/Inc/stm32f4xx_hal_conf.h строку #define HAL_ADC_MODULE_ENABLED и раскомментировать её. Зачем это сделано, признаться, не до конца понимаю, но у богатых свои причуды.
Собственно, на этом всё. Единственное, давайте на сладкое всё же запустим АЦП и при помощи отладчика извлечём какие-никакие данные.
/* USER CODE BEGIN PV */
ADC_HandleTypeDef hadc1;
int cnt = 0;
uint16_t data[1000] = {0,};
/* USER CODE END PV */
…
/* USER CODE BEGIN 0 */
static void my_ADC1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
ADC_ChannelConfTypeDef sC>
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2;
hadc1.Init.Resoluti>
hadc1.Init.ScanC>
hadc1.Init.C>
hadc1.Init.Disc>
hadc1.Init.ExternalTrigC>
hadc1.Init.ExternalTrigC>
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfC>
hadc1.Init.DMAC>
hadc1.Init.EOCSelecti>
HAL_ADC_Init(&hadc1);
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
/* USER CODE END 0 */
…
while (1)
{
HAL_ADC_Start(& hadc1);
HAL_ADC_PollForConversion(&hadc1, 1);
data[cnt] = HAL_ADC_GetValue(&hadc1);
cnt++;
if(cnt >= 1000)
{
cnt = 0;
}
И проверим в работе. Не забудем собрать, а затем — загрузим обновлённую прошивку и запустим её:
(gdb) load
…
Transfer rate: 14 KB/sec, 1102 bytes/write.
(gdb) c
Continuing.
^C
…
(gdb) b main.c:128
Breakpoint 4 at 0x80005e4: file ../Core/Src/main.c, line 129.
(gdb) c
Continuing.
Breakpoint 4, main () at ../Core/Src/main.c:129
129 HAL_ADC_Start(& hadc1);
(gdb)
Здесь я прервал программу в произвольном месте при помощи Ctrl+C, затем поставил точку останова где-то в main.Теперь посмотрим, что прочиталось в массив:
(gdb) p data
$10 = {245, 251, 259, 249, 257, 247, 254, 260, 248, 252, 257, 247, 254, 260, 247, 254, 260, 248, 252, 258, 245, 251, 259, 249, 257, 247, 254, 260, 247, 254, 260, 248, 252, 257, 247, 254, 260,
248, 252, 258, 245, 251, 260, 247, 254, 260, 247, 254, 260, 247, 254, 261, 252, 257, 247, 254, 260, 247, 254, 260, 248, 252, 258, 244, 247, 254, 260, 248, 252, 258, 245, 251, 259, 249, 257,
247, 254, 260, 247, 254, 260, 247, 256, 243, 249, 257, 247, 253, 262, 250, 255, 265, 257, 247, 254, 260, 247, 254, 260, 247, 254, 261, 251, 260, 247, 254, 260, 248, 252, 257, 247, 254, 260,
247, 254, 260, 248, 252, 257, 247, 254, 260, 247, 254, 260, 248, 252, 258, 245, 251, 259, 249, 257, 247, 254, 260, 248, 252, 257, 247, 254, 260, 248, 252, 257, 247, 254, 260, 248, 252, 257,
247, 254, 260, 247, 254, 260, 248, 252, 257, 247, 254, 260, 248, 252, 258, 245, 251, 259, 250, 255, 264, 253, 262, 250, 255, 265, 257, 247, 254, 260, 247, 254, 260, 248, 252, 257, 247, 254,
260, 247, 254, 260, 248, 252, 257, 247, 254, 260, 247...}
Что-то прочиталось, уже неплохо. А теперь — ради чего всё это:
(gdb) dump memory ~/tmp/test_data.bin data (data + sizeof (data))
(gdb)
Теперь мы получили в файле набор двоичных данных из массива data и можем относительно легко его разобрать, скажем, в таблицу примерно так:
Помимо двоичных данных команда dump умеет сохранять данные и в виде формата inetl hex и некоторых других.
К чему я это всё? А, на самом деле просто к тому, что команду dump очень полезно знать, даже если вы работаете в GUI STM32CubeIDE. Ведь её можно ввести во вкладке Debugger Console и получить слепок нужной вам области памяти.