LINUX.ORG.RU

Осваиваем STM32 снизу: часть 8 - используем CMSIS

 ,


0

1

Часть 1 Часть 2 Часть 3 Часть 4 Часть 5 Часть 6 Часть 7 Часть 8 Часть 9

Часть 8: используем CMSIS

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

Компания ARM предоставляет фреймворк CMSIS (Common Microcontroller Software Interface Standard), представляющий собой набор утилит, программных интерфейсов и других компонентов для написания программ для микроконтроллеров. Компания ST Microelectronics в свою очередь дополняет CMSIS компонентами, специфичными для микроконтроллеров STM32. По сути мы заменим все наши константы в коде на константы из CMSIS.

CMSIS это низкоуровневый фреймворк и принципиально его использование не отличается от того, что мы делали ранее. Вам также нужно внимательно читать руководство, изучать аппаратные регистры, понимать, какие биты в них нужно включать или выключать и тд. Компания ST Microelectronics также предоставляет для свободного использования библиотеки SPL, HAL и LL. SPL (Standard Peripheral Libraries) это устаревшая библиотека, она в настоящее время заменена библиотекой HAL. Библиотека HAL (Hardware Abstraction Layer) позволяет писать относительно высокоуровневый код, переносимый между процессорами. LL (Low Level) это низкоуровневая библиотека, её можно использовать вместо HAL. Также компания предоставляет IDE под названием STM32Cube, созданную на базе Eclipse, в которой можно генерировать проекты на базе HAL или LL и многое другое. Важно понимать, что чем больше библиотека делает за вас, тем больше размер вашей прошивки, тем медленней она работает. Для многих проектов это вполне приемлемо, а надёжность и скорость разработки стоит в приоритете.

В этой части мы это всё использовать не будем, ограничимся CMSIS.

У языка C, вообще говоря, есть своя стандартная библиотека. И хотя до сих пор мы её не использовали (а даже если бы и попробовали, то линкер выдал бы ошибку), в реальных программах хотя бы её часть может быть полезна. Для микроконтроллеров часто используется реализация стандартной библиотеки C под названием newlib. Некоторые файлы из CMSIS написаны с расчётом на то, что программа компонуется со стандартной библиотекой. Мы подключим эту библиотеку в следующей части, а в этой придётся чуть-чуть подредактировать файлы из CMSIS.

CMSIS с дополнениями от ST для серии STM32F1 можно скачать в гитхабе. Каталог Drivers/CMSIS это именно он. Makefile будет предполагать, что определена переменная окружения CMSIS_STM32F1, указывающая на этот каталог. Например можно собирать программу следующим образом:

export CMSIS_STM32F1=/opt/STM32CubeF1/Drivers/CMSIS
make

Для начала заглянем в $CMSIS_STM32F1/Device/ST/STM32F1xx/Include/stm32f1xx.h. В этом файле имеется следующая строка:

  /* #define STM32F103xB  */   /*!< STM32F103C8, STM32F103R8, STM32F103T8, STM32F103V8, STM32F103CB, STM32F103RB, STM32F103TB and STM32F103VB */

Как видно, кодовое обозначение STM32F103xB соответствует моделям процессоров STM32F103C8, STM32F103RB и некоторых других. У нашего микроконтроллера именно эта модель. Запомним это.

В каталоге $CMSIS_STM32F1/Device/ST/STM32F1xx/Source/Templates можно найти скрипты для линкера и код для старта. Скопируем файл $CMSIS_STM32F1/Device/ST/STM32F1xx/Source/Templates/gcc/linker/STM32F103XB_FLASH.ld в каталог с нашим проектом, его мы будем менять. Этот файл уже не должен выглядеть китайской грамотой, хотя всё же он содержит гораздо больше секций, чем наши скрипты в предыдущих частях, но принципиально ничего нового он не содержит. Для процессора STM32F103C8T6 нужно изменить длину региона FLASH со 128K до 64K. Также временно закомментируем секцию /DISCARD/ в конце файла, без неё наша программа не скомпонуется, пока мы не интегрируем в неё libc, а мы это планируем сделать позже.

Файл $CMSIS_STM32F1/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s тоже очень похож на то, что мы делали ранее. В первую очередь он вызывает функцию SystemInit, определённую в файле $CMSIS_STM32F1/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c, которая для нашего микропроцессора ничего не делает. После этого копируются данные сегмента .data из флеш-памяти в SRAM; обнуляется сегмент .bss, вызывается функция __libc_init_array и, наконец, вызывается функция main. Эти файлы в проект копировать не обязательно, их менять мы не будем.

Функция __libc_init_array определена в библиотеке newlib. Пока будем использовать вместо неё пустую заглушку.

Наша первая задача это написать код, который скомпилируется в пустую программу. После того, как разберёмся со сборкой, повторим программу из предыдущей части, печатающую "Hello world" в UART, но уже с использованием API из CMSIS. Также мы будем использовать SysTick таймер для задержек, чтобы познакомиться с тем, как определять обработчики исключений.

main.c:

#include "stm32f103xb.h"

void __libc_init_array(void)
{
}

int main(void)
{
}

Мы включаем заголовочный файл stm32f103xb.h, который находится в каталоге $CMSIS_STM32F1/Device/ST/STM32F1xx/Include/, а также определяем две пустых функции. Определение функции __libc_init_array нужно для компиляции без libc, а функция main вызывается сразу после инициализации.

В Makefile будем использовать некоторые дополнительные возможности make. Ниже некоторые выдержки из Makefile:

CPPFLAGS := -I$(CMSIS_STM32F1)/Device/ST/STM32F1xx/Include
CPPFLAGS += -I$(CMSIS_STM32F1)/Include
CPPFLAGS += -DSTM32F103xB
CFLAGS := -mcpu=cortex-m3 -g -std=c17 -Wall -Wextra -Wpedantic
CFLAGS += -Os
...

CC := arm-none-eabi-gcc

...

main.o: main.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

Сначала мы определяем переменные с некоторыми значениями. После этого мы используем эти переменные в правилах. Переменной $@ автоматически присваивается название цели, т.е. main.o в нашем случае. Переменной $< автоматически присваивается название первой зависимости, т.е. main.c в нашем случае. Аргумент компилятора -I добавляет указанный каталог в путь поиска для заголовочных файлов.

Ещё одно правило:

usarts.elf: $(LDSCRIPT) startup_stm32f103xb.o system_stm32f1xx.o main.o
	$(LD) -o $@ -T $^

Тут используется переменная $^, которая принимает значение, равное списку всех зависимостей.

Оставшиеся объектные файлы компилируются из исходников, которые расположены внутри каталога $CMSIS_STM32:

startup_stm32f103xb.o: $(CMSIS_STM32F1)/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s
	$(AS) -o $@ $<

system_stm32f1xx.o: $(CMSIS_STM32F1)/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

Полная сборка выглядит так:

$ export CMSIS_STM32F1=/opt/STM32CubeF1/Drivers/CMSIS
$ make
arm-none-eabi-as -o startup_stm32f103xb.o /opt/STM32CubeF1/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s
arm-none-eabi-gcc -I/opt/STM32CubeF1/Drivers/CMSIS/Device/ST/STM32F1xx/Include -I/opt/STM32CubeF1/Drivers/CMSIS/Include -DSTM32F103xB  -mcpu=cortex-m3 -Os -c -o system_stm32f1xx.o /opt/STM32CubeF1/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c
arm-none-eabi-gcc -I/opt/STM32CubeF1/Drivers/CMSIS/Device/ST/STM32F1xx/Include -I/opt/STM32CubeF1/Drivers/CMSIS/Include -DSTM32F103xB  -mcpu=cortex-m3 -Os -c -o main.o main.c
arm-none-eabi-ld -o usarts.elf -T STM32F103XB_FLASH.ld startup_stm32f103xb.o system_stm32f1xx.o main.o

Посмотрим на размер секций получившегося файла:

$ arm-none-eabi-objdump -h usarts.elf

usarts.elf:     file format elf32-littlearm

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .isr_vector   0000010c  08000000  08000000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .text         000000c0  0800010c  0800010c  0000110c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .rodata       00000018  080001cc  080001cc  000011cc  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .data         00000004  20000000  080001e4  00002000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 .bss          00000000  20000004  080001e8  00002004  2**0
                  ALLOC
  5 ._user_heap_stack 00000604  20000004  080001e8  00002004  2**0
                  ALLOC
  6 .ARM.attributes 0000002f  00000000  00000000  00002004  2**0
                  CONTENTS, READONLY
  7 .comment      00000045  00000000  00000000  00002033  2**0
                  CONTENTS, READONLY

Интересно отметить, что размер секции .isr_vector равен 0x10c, а не 0x130, как в наших ранних программах. Если внимательно изучить Reference Manual, выяснится, что на нашем микроконтроллере устройств гораздо меньше, и столько места под прерывания нам и не нужно было. Как видите, у производителя настройки лучше подготовленны для конкретного микроконтроллера.

На код ушло 0xc0 = 192 байта. Существенную часть этого размера занимает функция SystemCoreClockUpdate. Интересно, что в нашем коде эта функция пока вообще не вызывается. Если почитать документацию, то можно понять, что цель этой функции - запросить значение текущей частоты процессора и записать это значение в глобальную переменную SystemCoreClock. Можно скомпилировать проект так, чтобы компоновщик смог удалить неиспользуемые функции, но пока смиримся с тем, что у нас прошивка получилась на 192 байта больше, чем нужно. Чуть позже эта функция пригодится.

Теперь осталось самое простое - написать саму программу.

Инициализация USART выглядит следующим образом:

static void enable_usart(void)
{
    // enable port B clock
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // configure PB10 as alternate function output push-pull 2MHz
    uint32_t gpiob_crh = GPIOB->CRH;
    gpiob_crh &= ~GPIO_CRH_MODE10_0;
    gpiob_crh |= GPIO_CRH_MODE10_1;
    gpiob_crh &= ~GPIO_CRH_CNF10_0;
    gpiob_crh |= GPIO_CRH_CNF10_1;
    GPIOB->CRH = gpiob_crh;

    // enable USART3 clock
    RCC->APB1ENR |= RCC_APB1ENR_USART3EN;

    // set baud rate
    USART3->BRR = 0x0341;

    // enable USART3 and transmitter
    uint32_t usart3_cr1 = USART3->CR1;
    usart3_cr1 |= USART_CR1_UE;
    usart3_cr1 |= USART_CR1_TE;
    USART3->CR1 = usart3_cr1;
}

Этот код похож на то, что мы делали ранее, но теперь мы используем константы из CMSIS. Давайте попробуем понять, как это работает строка RCC->APB2ENR |= RCC_APB2ENR_IOPBEN. Ниже выписано всё: имеющие отношение к рассматриваемому вопросу (не совсем в правильном порядке):

#define RCC                 ((RCC_TypeDef *)RCC_BASE)
#define RCC_BASE              (AHBPERIPH_BASE + 0x00001000UL)
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x00020000UL)
#define PERIPH_BASE           0x40000000UL

typedef struct
{
...
  __IO uint32_t APB2ENR;
...
} RCC_TypeDef;

#define     __IO    volatile

#define RCC_APB2ENR_IOPBEN                   RCC_APB2ENR_IOPBEN_Msk
#define RCC_APB2ENR_IOPBEN_Msk               (0x1UL << RCC_APB2ENR_IOPBEN_Pos)
#define RCC_APB2ENR_IOPBEN_Pos               (3U)

Выходит, что выражение RCC->APB2ENR раскрывается в ((RCC_TypeDef *) 0x40021000UL)->APB2ENR, а выражение RCC_APB2ENR_IOPBEN раскрывается в 1 << 3 Обратите внимание на то, что поле RCC_TypeDef.APB2ENR объявлено с модификатором volatile, это гарантирует, что компилятор не уберёт код, читающий или пишущий по этому адресу.

Остальной код работает по похожему принципу, его подробно разбирать не будем.

Для того, чтобы понять, как добавить обработчик исключения для SysTick таймера, заглянем в файл $(CMSIS_STM32F1)/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s. Со 129 строки начинается вектор прерываний:

g_pfnVectors:

  .word _estack
  .word Reset_Handler
  .word NMI_Handler
  .word HardFault_Handler
...
  .word SysTick_Handler
...

Символы вроде Reset_Handler, SysTick_Handler определяют адреса обработчиков соответствующих прерываний. Также можно найти реализации этих обработчиков по умолчанию:

Default_Handler:
Infinite_Loop:
  b Infinite_Loop
...
  .weak SysTick_Handler
  .thumb_set SysTick_Handler,Default_Handler

Здесь объявляется вечный цикл с символом Default_Handler, а затем объявляется символ SysTick_Handler с тем же значением. Этот символ является т.н. слабым символом. Это означает, что если другой файл объявит символ SysTick_Handler, то будет использован именно символ из другого файла, а не будет ошибка компоновки, как это было бы в случае обычного символа. Т.е. в нашей прошивке уже определены и настроены обработчики всех возможных исключений, которые, при возникновении этих исключений, просто перейдут в вечный цикл. Таким образом, если в процессе отладки вы увидели, что код попал в этот цикл, это означает, что в вашем процессоре возникло исключение, для которого вы не предусмотрели обработчика.

Чтобы добавить обработчик, достаточно лишь объявить в нашем коде на C функцию с именем SysTick_Handler:

void SysTick_Handler(void)
{
    send_hello();
}

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

Если прошить этот код, он должен работать так же, как предыдущий, разве что с более точным таймером.

Полный код доступен на гитхабе.

★★★★

Проверено: hobbit ()
Последнее исправление: vbr (всего исправлений: 3)

В статье в выдержке из Makefile косяк: указана архитектура CPU как armv7, тогда как на самом деле должна быть armv7-m (тумба-онли же). Из-за этого проект не слинкуется с ошибкой:

arm-elf-eabi-ld: main.o: в функции «send_hello»:
main.c:10:(.text+0xc): undefined reference to `__aeabi_idivmod'

Компилятор пытается заюзать подпрограмму деления, которая обычно лежит в каком-нибудь libgcc.a или libc.a.

На житхабе Makefile другой, там вместо пары -march и -mtune просто указан -mcpu (что, кстати, видно в листинге в статье), такой проблемы быть не должно (не проверял).

Кроме того, в коде на житхабе используется asm, а это «гнутое» расширение, которое не работает при указании «стандартного» стандарта языка C (что сделано в Makefile в статье и не сделано в Makefile в репозитории).

Это бы как-то к одному знаменателю привести, что ли…

Да, и ссылка на проект на житхаб в конце статьи поломана.

Barracuda72 ★★
()
Ответ на: комментарий от Barracuda72

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

vbr ★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.