Наш канал в telegram

Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 10. CMSIS, использование стандартных библиотек и функций.

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

Итак, CMSIS — это аббревиатура от Cortex Microcontroller Software Interface Standart, что можно перевести как стандарт программного интерфейса микроконтроллеров на ядре кортекс. Этот стандарт унифицирует такие вещи, как:

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

Файлы, касающиеся компонентов ядра разрабатываются и поставляются самой компанией ARM. Всё, что касается внешних модулей — разрабатывается производителями конкретных линеек и моделей контроллеров на ядре Cortex (в нашем случае компанией STM).

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

Заходим в Keil uVision и создаем новый проект. Всё делаем также, как описано в первой части этой статьи, пока не дойдём до окна «Manage Run-Time Environment». В этом окне нам необходимо указать галочками, какие компоненты CMSIS мы хотели бы использовать у себя в проекте, и Keil автоматически добавит в проект нужные файлы. Более того, Keil автоматически проанализирует используемые компоненты и сообщит, если для их работы не хватает ещё каких-нибудь компонентов.

Для первого примера будем считать, что мы хотим использовать только функции начальной настройки системы. В окне «Manage Run-Time Environment» (рисунок внизу) ставим галочку напротив компонента Device->Startup. В левом нижнем окошке («Validation output») Keil сразу сообщает нам, что для работы этого компонента нам понадобится компонент, содержащий описание ядра — CMSIS:CORE. Подключить недостающие компоненты можно автоматически — нажав на кнопку «Resolve». Жмём на «Resolve», видим, что предупреждения в окне «Validation Output» пропали, а в дереве компонентов Keil автоматически добавил нам в проект компонет CMSIS->CORE, и далее жмём кнопку «OK».

подключение компонентов CMSIS в окне Manage Run-Time Environment

Подключить какие-либо компоненты можно не только при создании проекта, но и в любой другой удобный момент, вызвав окошко «Manage Run-Time Invironment» из меню Project->Manage->Run-Time Environment… или щёлкнув по соответствующему значку на тулбаре вверху.

Кроме того, не забудьте после того, как проект создан, зайти в настройки проекта (Project -> Options…) и на вкладке Output отметить галочку Create HEX File, а на вкладке Debug выбрать Use Simulator (чтобы иметь возможность пользоваться отладчиком без подключения всяких демобордов).

Итак, проект мы создали и видим, что в него были добавлены 3 файла: RTE_Device.h, startup_stm32f10x_md.s, system_stm32f10x.c. Давайте по порядку откроем каждый из них и посмотрим, что там внутри.

Файл RTE_Device.h предназначен для быстрого конфигурирования системы. Всё, что мы здесь наконфигурим, в дальнейшем используется в библиотеках, описывающих работу тех или иных модулей контроллера. Мы пока никакими дополнительными библиотеками для модулей пользоваться не будем, поэтому нам этот файл, по сути, в проекте и не нужен, но тем не менее, раз уж он добавляется, давайте посмотрим как с ним работать (на будущее). Смысл здесь такой:

  • Во-первых, мы в этом файле с помощью директив define определяем будет ли использоваться тот или иной модуль. Если будет, то устанавливаем напротив define имя_модуля единицу, если нет, то устанавливаем там ноль (по умолчанию напротив всех модулей установлены нули).
  • Во-вторых, мы в этом же файле с помощью директив define описываем различные опции для выбранных модулей. Самому здесь тоже ничего придумывать не нужно, возможные опции уже описаны, нужно только выставить нули или единицы, чтобы обозначить нужны нам эти опции или нет.

Если открыть файл RTE_Device.h в проекте, то снизу мы увидим 2 вкладки: «text editor» и «Configuration Wizard». Первая вкладка позволяет просматривать и редактировать файл в текстовом виде, а вот вторая вкладка позволяет настраивать записи в файле с помощью специального визарда, тыкая по чекбоксам (мы тыкаем по чекбоксам, а в файле меняются записи). Это становится возможным благодаря специальной, понятной Keil uVision, разметке. Для других сред и компиляторов эта разметка будет восприниматься просто как комментарий. Как это выглядит можно увидеть на рисунках ниже.

файл RTE_Device в окне text editor
файл RTE_Device в окне Configuration Wizard

Переходим к файлу startup_stm32f10x_md.s. Открываем его и видим написанный на ассемблере готовый каркас программы, в котором описываются стек и куча (в секциях AREA STACK и AREA HEAP), таблица векторов исключений (в секции AREA RESET), заглушки под обработчики всех исключений, кроме Reset_Handler, базовый код обработчика Reset_Handler.

Для этого файла также доступен Configuration Wizard, однако в нём можно настроить только размеры стека и кучи.

Если вы ничего не будете переделывать, то именно в соответствии с кодом из файла startup_stm32f10x_md.s будет стартовать написанная вами программа. Вершина стека будет установлена в соответствии со значением переменной __initial_sp и далее управление будет передано на метку Reset_Handler.

А что же у нас там за код после метки Reset_Handler? Сначала вызывается внешняя функция SystemInit (команды LDR R0,=SystemInit и BLX R0), а затем происходит переход на метку __main (команды LDR R0,=__main и BX R0). То есть именно здесь задаётся, что пользовательская программа должна начинаться с метки __main.

Кроме того, мы видим, что все обработчики, включая Reset_Handler, помечены директивой [WEAK]. Это так называемое «слабое» объявление. Если компилятор встретит где-то ещё в программе объявление этих же функций, но уже без директивы [WEAK], то он будет использовать код, соответствующий более «сильному» объявлению, а «слабо» объявленный код просто проигнорирует. Таким образом мы можем написать свои обработчики для любого исключения (вместо «слабо» объявленных заглушек), включая начальную загрузку после старта (объявив свой Reset_Handler), или переделать непосредственно имеющиеся «слабо» объявленные обработчики.

Код начальной загрузки системы в файле startup_stm32f10x_md.s

Теперь открываем файл system_stm32f10x.c. Описание в шапке файла гласит, что этот файл предоставляет для использования в пользовательских проектах две функции (SystemInit И SystemCoreClockUpdate) и одну глобальную переменную (SystemCoreClock).

Читаем шапку дальше. Функция SystemInit() выбирает источник тактирования, настраивает PLL и делители шин AHB/APBx. С ней мы уже сталкивались, — именно она вызывается в файле startup_stm32f10x_md.s сразу после старта. Про переменную SystemCoreClock написано, что она может использоваться пользовательскими приложениями для настройки таймера SysTick или других параметров. Функция SystemCoreClockUpdate() обновляет переменную SystemCoreClock и должна вызываться каждый раз при изменении частоты ядра внутри программы.

Ещё ниже можно увидеть такую запись: «Uncomment the line corresponding to the desired System Clock (SYSCLK) frequency», что переводится как: «Раскомментируйте строчку, соответствующую желаемой системной частоте», и список дефайнов. По умолчанию в этом списке раскомменчена строка #define SYSCLK_FREQ_72MHz.

Эти строки предлагают выбор между просто внешним генератором (SYSCLK_FREQ_HSE) и несколькими вариантами значений, которые можно получить из внешнего генератора при помощи PLL. По-умолчанию считается, что внешний кварц у нас на 8 МГц (такой же, как и внутренний генератор). В зависимости от выбранного варианта в проект будут добавлены коды, по-разному настраивающие источники PLL, всякие делители и множители.

Давайте теперь вернёмся к функциям SystemInit() и SystemCoreClockUpdate().

Для начала проанализируем Си-шный код функции SystemInit():

Текст под катом

	EXPORT	__main
	AREA	|.text|, CODE, READONLY
__main
	END

[свернуть]

Смотрим код функции SetSysClock:

Текст под катом

#ifdef SYSCLK_FREQ_HSE
	SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
	SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
	SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
	SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
	SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
	SetSysClockTo72();
#endif

[свернуть]

Как видите, именно тут будет добавлен тот или иной код, в зависимости от того, как мы хотим настроить системную частоту (какой дефайн мы ранее раскомментили).

Мы, если вы помните, оставили всё по умолчанию (раскомментирована строка #define SYSCLK_FREQ_72MHz), следовательно в наш код будет добавлена функция SetSysClockTo72().

Смотрим её код:

Текст под катом

RCC->CR |= ((uint32_t)RCC_CR_HSEON); включаем HSE (поднимаем флаг HSEON)
do
{ HSEStatus = RCC->CR & RCC_CR_HSERDY; ждём, пока включится HSE (установится флаг HSERDY)
  StartUpCounter++;                    или счётчик досчитает до HSE_STARTUP_TIMEOUT
} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
 
if((RCC->CR & RCC_CR_HSERDY) != RESET)	/* если флаг RCC_CR_HSERDY не сброшен */
{ HSEStatus = (uint32_t)0x01; значит HSE включен
}
else
{ HSEStatus = (uint32_t)0x01; иначе HSE выключен
}
 
if(HSEStatus == (uint32_t)0x01)
{ FLASH->ACR |= FLASH_ACR_PRFTBE; включаем буфер предварительной выборки (поднимаем флаг PRFTBE)
  FLASH->ACR &=(uint32_t)((uint32_t)~FLASH_ACR_LATENCY); сбрасываем 3 бита настройки LATENCY
  FLASH->ACR |=(uint32_t)FLASH_ACR_LATENCY_2; включаем задержку 2 цикла на чтение из flash 
 
  RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; выключаем делитель для SYSCLK
  RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; выключаем делитель для APB2
  RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2; выключаем делитель для APB1
 
  RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC|RCC_CFGR_PLLXTPRE|RCC_CFGR_PLLMULL)); сбрасываем все биты PLLMULL, делитель HSE, источник PLL - HSI/2
  RCC->CFGR |= (uint32(RCC_CFGR_PLLSRC_HSE|RCC_CFGR_PLLMULL9)); источник PLL - HSE, PLLMUL=0111 (x9), PLLCLK=HSE*9=72MHz
  RCC->CR |= RCC_CR_PLLON; включаем PLL
  while((RCC->CR & RCC_CR_PLLRDY) == 0) /* ждём, пока PLL устаканится (взведётся флаг PLLRDY) */
  { }
 
  RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); выбираем PLL в качестве источника тактирования
  RCC-CFGR |= (uint32_t)RCC_CFGR_SW_PLL;
  while((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08) /* ждём, пока тактирование перейдёт на PLL */
  { }
}
else
{ /* тут написано, что мы можем создать в этом месте код, обрабатывающий ситуацию, когда мы не смогли запуститься на внешнем кварце */
}

[свернуть]

Функцию SystemCoreClockUpdate() предлагаю изучить вам самомтоятельно, там всё примерно так же, — унылый Си-шный код.

Всё это отлично, но мы пока так и не увидели файла или файлов с именами и адресами областей памяти, исключений, регистров, битов, переменных и констант. Что ж давайте искать. Смотрим ещё раз внимательно файл system_stm32f10x.c и почти в самом начале натыкаемся на инклюд файла stm32f10x.h. Находим его через поиск в папке Keil и открываем в текстовом редакторе.

Здесь-то как раз и расположены все определения доступных в контроллере имён и адресов периферии, соответствующие стандарту CMSIS. Здесь же инклюдятся файлы хидеров, содержащие аналогичные описания для самого ядра АРМ, макросы для доступа к ресурсам ядра и тому подобное.

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

  • SET_BIT(REG, BIT) для операции (REG) |= (BIT)
  • CLEAR_BIT(REG, BIT) для операции (REG) &= ~(BIT)
  • READ_BIT(REG, BIT) для операции (REG) & (BIT)
  • CLEAR_REG(REG, BIT) для операции (REG) = (0x0)
  • WRITE_REG(REG, VAL) для операции (REG) = (VAL)
  • READ_REG(REG) для операции (REG)

Вообще, если на этой стадии попробовать скомпилить проект, то у вас после компиляции появятся в менеджере проекта (слева), в раскрывающемся под файлом system_stm32f10x.c списке, все так или иначе подтягиваемые к этому файлу стандартные файлы хидеров. Так что можно было и не искать где они лежат. С этими хидерами нас ждёт небольшой сюрприз, но, как говорится, всему своё время.

Давайте теперь накодим чего-нибудь с использованием описанных выше библиотек. Проект мы выше уже создали, библиотеки подключили, осталось только добавить пользовательский код. Для тестов напишем такую же программку, как в статье про работу с портами ввода/вывода. То есть наша программка будет контролировать уровень на ноге PA9 и повторять его на ноге PA8.

Сначала добавим к проекту файл, который будет содержать пользовательский код. Щёлкаем в проводнике проекта правой кнопкой по папке Source Group 1 и выбираем пункт Add New Item to Group ‘Source Group 1’… В появившемся окне слева выбираем тип добавляемого файла (мы будем писать на асме, так что выбираем Asm File (.s)), придумываем ему имя (назовем его, например, test) и нажимаем кнопку «Add». Далее дважды щёлкаем по добавленному файлу в проводнике чтобы он открылся во встроенном редакторе.

Первым делом напишем каркас:

	EXPORT	__main
	AREA	|.text|, CODE, READONLY
__main
	END

Немного пояснений. Во-первых, нам нужно экспортировать метку __main, чтобы мог нормально скомпилиться файл startup_stm32f10x_md.s, в который эта метка импортируется. Во вторых, нужно прописать, в какой области должен располагаться написанный нами код (мы поместим его в ту же область, где находится стартовый код). Вот и всё, можно писать саму программу.

А вот здесь, собственно, и обещанный сюрприз, — стандартные файлы хидеров не получится проинклюдить в ассемблерный код, только в Си или Си++. — Хорошенький стандарт, скажете вы, и будете совершенно правы. Я, если честно, и сам немного от такого офигел. И тем не менее, легкое гугленье говорит, что стандартных хидеров для ассемблера не существует. Асм вообще последнее время преподносится как язык только для гиков, так что… Ну да ладно, не будем разводить холивор.

Если мы хотим продолжать кодить на ассемблере, то выход в любом случае только один, — писать хидеры самому. Самые ленивые, естественно, никакие хидеры не пишут (тем более, что их нужно чуть более, чем до… очень много), а пишут вместо этого скрипты для выдирания хидеров нужного формата из стандартных. Все эти выдерки той или иной степени кривизны можно найти в интернете (приводить не буду, во избежание криков, что чьи-то менее кривые вырезки были незаслуженно проигнорированы).

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

А зачем же мы тогда вообще разводили весь этот сыр-бор со стандартными библиотеками? Ну… Во-первых, — мы избавились от процедуры инициализации системы тактирования. Вау! Вот это круто (произносить с сарказмом). Во-вторых, — подглядели, как эту самую инициализацию делают другие (по стандарту, так сказать). Ну и, наконец, в третьих, — мы же можем вызывать в своей программе стандартные функции из подключаемых библиотек (правда тут придётся разобраться, куда для используемых функций складывать входные данные и откуда забирать выходные). Механизм передачи параметров в вызывающую функцию и возврат результатов её работы описан в стандарте AAPCS (Procedure Call Standart for the ARM Architecture). О-па, ещё один стандарт, ну а куда без них.

Есть конечно же и другой путь — перейти на Си. Это конечно не для нас, фу-фу-фу, но примерчик для сравнения всё же будет. И выглядеть он для нашей задачи будет примерно так:

Текст под катом

#include "stm32f10x.h" /* си-шный код сжуёт стандартные хидеры */
 
int main(void) {
/* сюда будет передано управление после старта и настройки системы тактирования */
 
PortInit:
   RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; /* включаем тактирование PortA */
   /* настраиваем GPIO8 */
   GPIOA->CRH |= GPIO_CRH_MODE8;       /* MODE8=11 */
   GPIOA->CRH &= ~GPIO_CRH_CNF8;       /* CNF8=00, output push-pull */
   /* настраиваем GPIO9 */
   GPIOA->CRH &= ~GPIO_CRH_MODE9;      /* MODE9=00 */
   GPIOA->CRH &= ~GPIO_CRH_CNF9;       /* CNF9=00, input analog */
   GPIOA->CRH |= GPIO_CRH_CNF9_0;      /* CNF9=01, input floating */
 
Work:
   if((GPIOA->IDR & GPIO_IDR_IDR9)!=0) /* если бит 9 установлен */
   { /* переключаем выход PORTA8 в единицу */
     GPIOA->BSRR |= GPIO_BSRR_BS8;     /* установить bit8 в регистре GPIOA_BSRR */
   }
   else
   { /* переключаем выход PORTA8 в ноль */
     GPIOA->BSRR |= GPIO_BSRR_BR8;     /* установить bit24 в регистре GPIOA_BSRR */
   }
}

[свернуть]

Как видите, библиотеки могут очень сильно облегчить Вам жизнь, но взамен от Вас требуется знать, где что в этих библиотеках настраивать и искать, какие они предоставляют стандартные функции, как передавать в них данные, где забирать результаты, и ещё много, много всего. А кто говорил, что будет легко?. У меня на сегодня всё, вопросы как обычно на форум или в комменты.

  1. Часть 1. Установка MDK, создание проекта, основы Keil uVision
  2. Часть 2. Команды и директивы ассемблера, структура и синтаксис программы. Первая программа для STM32.
  3. Часть 3. Карта памяти контроллеров STM32, методы работы с памятью.
  4. Часть 4. Регистры, старт и режимы работы контроллеров STM32.
  5. Часть 5. Как залить прошивку в контроллер.
  6. Часть 6. Настройка системы тактирования.
  7. Часть 7. Работа с портами ввода-вывода.
  8. Часть 8. Процедуры на ассемблере для STM32.
  9. Часть 9. Система прерываний.
  10. Часть 10. CMSIS, использование стандартных библиотек и функций.
  11. Приложение 1. Набор инструкций THUMB-2 и особенности их использования.
  12. Приложение 2. Таблица векторов прерываний для семейств STM32F101, STM32F102, STM32F103.

Добавить комментарий