Наш магазин на eBay Наш магазин на AliExpress Наш канал в telegram

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

Стандарт CMSIS

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

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

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

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

Подключение к проекту модулей и библиотек CMSIS

В общих чертах, я думаю, понятно, а теперь давайте попробуем разобраться как всё это можно использовать в наших проектах. Создадим какой-нибудь проект, попробуем подключить к нему различные компоненты, соответствующие стандарту 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 — конфигурирование контроллера

Файл 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 — таблица векторов, заглушки обработчиков, код запуска

Переходим к файлу 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 — настройка системы тактирования

Теперь открываем файл 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():

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

/* сначала всё выключаем */
RCC->CR|  =(uint32_t)0x00000001; - включаем внутренний генератор 8 МГц (HSI)
RCC->CFGR&=(uint32_t)0xF0FF0000; - выбираем HSI источником тактирования, сбрасываем предделители AHB, APB1, APB2, ADC, отключаем HSI от PLL, выключаем тактирование MCO
RCC->CR&  =(uint32_t)0xFEF6FFFF; - выключаем HSE, CSS, PLL
RCC->CR&  =(uint32_t)0xFFFBFFFF; - выключаем HSEBYP
RCC->CFGR&=(uint32_t)0xFF80FFFF; - сбрасываем предделитель HSE для PLL и множитель PLL
RCC->CIR  =0x009F0000;           - выключаем все прерывания и сбрасываем флаги ожидания модуля RCC
/* а потом включаем */
SetSysClock();
/* ну и тут ещё есть настройка места расположения таблицы прерываний (SRAM / FLASH) но это уже не важно */

[свернуть]

Смотрим код функции 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() предлагаю изучить вам самостоятельно, там всё примерно так же, — унылый Си-шный код.

stm32f10x.h — определения имён областей, регистров, битов, констант…

Всё это отлично, но мы пока так и не увидели файла или файлов с именами и адресами областей памяти, исключений, регистров, битов, переменных и констант. Что ж давайте искать. Смотрим ещё раз внимательно файл 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 списке, все так или иначе подтягиваемые к этому файлу стандартные файлы хидеров. Так что можно было и не искать где они лежат. С этими хидерами нас ждёт небольшой сюрприз, но, как говорится, всему своё время.

Пример использования CMSIS в программах

Давайте теперь накодим чего-нибудь с использованием описанных выше библиотек. Проект мы выше уже создали, библиотеки подключили, осталось только добавить пользовательский код. Для тестов напишем такую же программку, как в статье про работу с портами ввода/вывода. То есть наша программка будет контролировать уровень на ноге 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 */
   }
   goto Work;
}

[свернуть]
Программа на Си для той же задачи, но с использованием прерываний

Основная программа и подпрограммы настройки и обработчика прерываний от линии PA9 разделены в разные файлы.

Файл основной программы:

#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->CRL &= ~GPIO_CRL_MODE9;	/* MODE9=00 */
	GPIOA->CRL &= ~GPIO_CRL_CNF9;	/* CNF9=00, input analog */
	GPIOA->CRL |= GPIO_CRL_CNF9_0;	/* CNF9=01, input floating */
 
	EXT9_5_Interrupt_Init();	/* вызов функции инициализации прерывания от PA9 */
	__enable_irq();			/* Разрешаем все немаскированные прерывания */
 
Work:
	 goto Work;
}

Файл, содержащий подпрограммы настройки и обработки прерывания:

#include "stm32f10x.h" /* си-шный код сжуёт стандартные хидеры */
 
void EXT9_5_Interrupt_Init(void) {
	/* Включаем тактирование AFIO (иначе мы не сможем писать в регистры AFIO и не сможем выбрать */
	/* никакой другой источник для EXTI9 кроме PA9, поскольку для этого нужно писать в AFIO_EXTICR3) */
	RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
	AFIO->EXTICR[3] |= AFIO_EXTICR2_EXTI9_PA; /* Выбираем пин PA9 в качестве источника прерывания линии 9 (EXTI9) */
	EXTI->IMR |= EXTI_IMR_MR9;		/* Выставляем битовую маску для разрешения прерываний от линии 9 (EXTI9) */
	EXTI->RTSR |= EXTI_RTSR_TR9;		/* Выбираем передний фронт для сигнала на линии 9 (EXTI9) */
	EXTI->FTSR |= EXTI_FTSR_TR9;		/* Выбираем задний фронт для сигнала на линии 9 (EXTI9) */
	NVIC_EnableIRQ(EXTI9_5_IRQn);		/* Разрешаем прерывания от EXTI9_5 */
}
 
void EXTI9_5_IRQHandler(void) {
   /* код обработчика прерывания от пина PA9 */
   EXTI->PR |= EXTI_PR_PR9; 		/* сразу сбрасываем pending bit линии 9 (EXTI9), чтобы не пропустить очередную сработку */
   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 */
   }
}

[свернуть]

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

P.S. Описание части встроенных стандартных функций CMSIS, таких как NVIC_EnableIRQ(), NVIC_DisableIRQ(), __enable_irq(), __disable_irq() и тому подобных, можно найти в документе «Cortex-M3 Generic User Guide», который всегда доступен в менеджере проекта, на вкладке Books. Я правда не нашёл где прописан код этих функций и как именно они инклюдятся в проект. Хотя всё работает.

  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. Часть 11. Подключение и использование драйверов из пакета StdPeriph
  12. Часть 12. Работа с модулями USART и UART.
  13. Часть 13. Работа с модулями ADC
  14. Часть 14. Использование DMA
  15. Часть 15. Таймеры. Глава 1 — Введение. Простейшие таймеры
  16. Часть 15. Таймеры. Глава 2 — Таймеры общего назначения TIM9 — TIM14
  17. Часть 15. Таймеры. Глава 3 — Таймеры общего назначения TIM2 — TIM5
  18. Часть 15. Таймеры. Глава 4 — Продвинутые таймеры TIM1, TIM8
  19. Часть 16. Создание устройства USB HID в Keil uVision при помощи библиотечного компонента USB
  20. Приложение 1. Набор инструкций THUMB-2 и особенности их использования
  21. Приложение 2. Таблица векторов прерываний для семейств STM32F101, STM32F102, STM32F103
  22. Приложение 3. Драйвера и функции библиотеки StdPeriph

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