Как настроить смартфоны и ПК. Информационный портал
  • Главная
  • Вконтакте
  • Подключение стандартной библиотеки периферии к любому семейству STM32.

Подключение стандартной библиотеки периферии к любому семейству STM32.

При создании первого приложения на микроконтроллере STM32 можно пойти несколькими путями. Первый, классический, берём точное описание контроллера на сайте www.st.com , которое фигурирует под названием «Reference Manual» и читаем описание регистров периферии. Дальше пробуем записывать их и смотрим, как работает периферия. Чтение этого документа очень полезно, но на первом этапе освоения микроконтроллера от этого можно отказаться, как это не странно. Инженеры STMicroelectronics написали библиотеку драйверов стандартной периферии. Более того, они написали множество примеров использования данных драйверов, что может свести программирование вашего приложения к нажатию клавиш Ctrl+C и Ctrl+V, с последующей небольшой правкой примера использования драйвера под свои потребности. Таким образом, подключение библиотеки драйверов периферии к вашему проекту является вторым методом построения приложения. Кроме скорости написания есть и другие достоинства этого метода: универсальность кода и применение других фирменных библиотек, таких как USB, Ethernet, управление приводом и т.д., которые предоставляются в исходниках и использующих стандартный драйвер периферии. Недостатки данного метода тоже есть: Там где можно обойтись одной строкой кода стандартный драйвер периферии STM32 напишет 10. Сама библиотека периферии также предоставляется в виде исходных файлов, так что можно проследить какой бит какого регистра меняет та или иная функция. При желании можно будет перейти от второго метода написания программы к первому, закомментировав часть кода, использующего стандартную библиотеку на свою, управляющую непосредственно регистром периферии. В результате такого действия вы выиграете в скорости управления, объёме ОЗУ и ПЗУ, а потеряете в универсальности кода. В любом случае, инженеры компании "Промэлектроника" рекомендуют использовать библиотеку стандартной периферии хотя бы на первом этапе.

Наибольшие трудности ожидают разработчика при подключении библиотеки к своему проекту. Если не знать, как это делать, можно потратить много времени на это мероприятие, что противоречит самой идее использования готового драйвера. Материал посвящён подключению стандартной библиотеки к любому семейству STM32.

Каждое семейство STM32 имеет свою библиотеку стандартной периферии. Это связано с тем, что сама периферия разная. Например, периферия контроллеров STM32L в качестве одной из задач имеет функцию энергосбережения, что тянет за собой добавление функций управления. Классическим примером, можно считать АЦП, который в STM32L имеет возможность аппаратного отключения, при длительном отсутствии команды преобразования – одно из следствий задачи энергосбережения. АЦП контроллеров семейств STM32F не имеют такой функции. Собственно говоря, в силу наличия аппаратной разницы периферии имеем разные библиотеки драйверов. Кроме очевидного различия функций контроллера имеет место улучшение периферии. Так, периферия контроллеров семейств, которые были выпущены позже, может быть более продуманной и удобной. Например, периферия контроллеров STM32F1 и STM32F2 имеет различия по управлению. На взгляд автора управление периферией STM32F2 более удобное. И это понятно почему: STM32F2 семейство было выпущено позже и это позволило разработчикам учесть некоторые нюансы. Соответственно, для данных семейств – индивидуальные библиотеки управления периферией. Идея вышесказанного проста: на странице микроконтроллера, который вы собираетесь использовать, находится подходящая ему библиотека периферии.

Несмотря на различие периферии в семействах драйверы скрывают 90% различий внутри себя. Например, функция настройки, упомянутого выше АЦП, для всех семейств выглядит одинаково:

void ADC_Init(ADC_Nom, ADC_Param) ,

где ADC_Nom – номер АЦП в виде ADC1, ADC2, ADC3 и т. д.

ADC_Param – указатель структуры данных, каким образом надо настроить АЦП (от чего запускаться, сколько каналов оцифровывать, выполнять ли это циклически и т.д.)

10% различий семейств, в данном примере, которые придётся поправить при переходе с одного семейства STM32 на другое, спрятаны в структуре ADC_Param. В зависимости от семейства количество полей этой структуры может быть разным. Общая же часть, имеет одинаковый синтаксис. Таким образом, перевод приложения для одного семейства STM32, написанного на базе стандартных библиотек периферии на другое, весьма прост. В части универсализации решений на микроконтроллерах STMicroelectronics неотразим!

Итак, мы скачали библиотеку для применяемого STM32. Что дальше? Далее, нам надо создать проект и подключить к нему требуемые файлы. Создание проекта рассмотрим на примере среды разработки IAR Embedded Workbench. Запускаем среду разработки и заходим на вкладку “Project”, выбираем пункт создания проекта “Create project”:

В появившемся новом проекте вводим настройки, наведя курсор на название проекта, нажав правую клавишу мыши и выбрав в выпавшем меню «Options»:

Области памяти ОЗУ и ПЗУ:

При нажатии кнопки “Save” среда предложит записать новый файл описания контроллера в папку проекта. Автор рекомендует создавать каждому проекту индивидуальный файл *.icp и хранить его в папке с проектом.

Если вы собираетесь внутрисхемно отлаживать свой проект, что рекомендуется, то вводим тип применяемого отладчика:

На вкладке выбранного отладчика указываем интерфейс подключения отладчика (в нашем случае выбран ST-Link) к контроллеру:



С этого момента наш проект без библиотек готов к компиляции и загрузке в контроллер. В других средах, таких как Keil uVision4, Resonance Ride7 и т. п. потребуется выполнить эти же действия.

Если в файле main.c написать строку:

#include "stm32f10x.h" или

#include "stm32f2xx.h" или

#include "stm32f4xx.h" или

#include "stm32l15x.h" или

#include "stm32l10x.h" или

#include "stm32f05x.h"

с указанием расположения данного файла, либо копированием данного файла в папку проекта, то произойдёт ассоциация некоторых областей памяти с регистрами периферии соответствующего семейства. Сам файл находится в папке библиотеки стандартной периферии в разделе: \CMSIS\CM3\DeviceSupport\ST\STM32F10x (или похожее по названию для других семейств). С этого момента вы заменять адрес регистра периферии в виде числа его названием. Даже если вы не собираетесь использовать функции стандартной библиотеки, рекомендуется сделать такое подключение.

Если вы собираетесь использовать в своём проекте прерывания, то рекомендуется подключить стартовый файл с расширением *.s, который находится по пути \CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\iar, или подобном, для других семейств. Важно отметить, что для каждой среды файл свой. Соответственно, если мы используем IAR EWB, то должны взять файл из папки IAR. Это обусловлено небольшим различием синтаксиса сред. Поэтому, чтобы проект запустился сразу, инженеры STMicroelectronics написали несколько вариантов стартовых файлов для нескольких наиболее популярных сред разработки. Большинство семейств STM32 имеют один файл. Семейство STM32F1 имеет несколько запускающих фалов:

  • startup_stm32f10x_cl.s – для микроконтроллеров STM32F105/107
  • startup_stm32f10x_xl.s - для микроконтроллеров STM32F101/STM32F103 768кб и более
  • startup_stm32f10x_hd.s - для микроконтроллеров STM32F101/STM32F103 c памятью Flash 256-512 кб
  • startup_stm32f10x_md.s - для микроконтроллеров STM32F101/ STM32F102/STM32F103 c памятью Flash 64-128 кб
  • startup_stm32f10x_ld.s - для микроконтроллеров STM32F101/ STM32F102/STM32F103 c памятью Flash менее 64кб
  • startup_stm32f10x_hd_vl.s для микроконтроллеров STM32F100 c памятью Flash 256-512 кб
  • startup_stm32f10x_md_vl.s для микроконтроллеров STM32F100 c памятью Flash 64-128 кб
  • startup_stm32f10x_ld_vl.s для микроконтроллеров STM32F100 c памятью Flash 32кб и менее

Итак, в зависимости семейства, подсемейства и среды разработки добавляем запускающий файл в проект:

Именно здесь оказывается микроконтроллер при старте программы. Прерывание последовательно вызывает функцию SystemInit(), а затем __iar_program_start. Вторая функция обнуляет либо записывает заранее заданные значения глобальных переменных, после чего осуществляет переход в программу пользователя main(). Функция SystemInit() настраивает тактирование микроконтроллера. Именно она даёт ответы на вопросы:

  • Надо ли переключаться на внешний кварц (HSE)?
  • Как умножить частоту от HSI/HSE?
  • Требуется ли подключение очереди загрузки команд?
  • Какая требуется задержка при загрузке команды (из-за низкой скорости работы Flash памяти)
  • Как поделить тактирование шин периферии?
  • Требуется ли разместить код во внешней ОЗУ?

Функцию SystemInit() можно прописать вручную в своём проекте. Если оформить эту функцию как пустую, то контроллер будет работать на внутреннем RC-генераторе с частотой порядка 8МГц (в зависимости от типа семейства). Вариант 2 – подключить к проекту файл system_stm32f10x.c (либо похожий по названию в зависимости от типа используемого семейства), который расположен в библиотеке по пути: Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x. В этом файле имеется функция SystemInit(). Обратите внимание на частоту внешнего кварца HSE_VALUE. Данный параметр выставляется в заголовочном файле stm32f10x.h. Стандартное значение 8 и 25МГц, в зависимости от семейства STM32. Основная задача функции SystemInit() – переключить тактирование на внешний кварц и умножить определённым образом данную частоту. Что произойдёт, если значение HSE_VALUE указано 8МГц, ядро должно тактироваться частотой 72МГц, а по факту на плате стоит кварц 16МГц? В результате таких некорректных действий ядро получит тактирование 144МГц, которые могут оказаться за пределами гарантированной работы системы на STM32. Т.е. при подключении файла system_stm32f10x.c потребуется указать значение HSE_VALUE. Всё это означает, что файлы system_stm32f10x.c, system_stm32f10x.h и stm32f10x.h (или похожие по названию для других семейств) должны быть индивидуальными для каждого проекта. И

нженеры STMicroelectronics создали инструмент Clock Configuration Tool, который позволяет правильно настроить тактирование системы. Это файл Excel, генерирующий файл system_stm32xxx.c (похожий по названию для заданного семейства семейств), после задания входных и выходных параметров системы. Рассмотрим его работу на примере STM32F4 семейства.

Варианты: внутренний RC-генератор, внутренний RC-генератор с умножением частоты, либо внешний кварц с умножением частоты. После выбора источника тактирования вводим параметры желаемой конфигурации системы, такие как входную частоту (при использовании внешнего кварца), частоту тактирования ядра, делители частоты тактирования шин периферии, работу буфера выборки команд и другие. Нажав на кнопку “Generate”, получаем окно


Подключение файла system_stm32f4xx.c и его аналогов потребует подключение ещё одного файла стандартной библиотеки периферии. Для управления тактированием имеется целый набор функций, которые вызываются из файла system_stm32xxxxxx.c. Эти функции расположены в файле stm32f10x_rcc.c и его заголовке. Соответственно, подключая к проекту файл файла system_stm32xxxxxx.c требуется подключить stm32f10x_rcc.c, иначе линкер среды сообщит об отсутствии описания функций с именем RCC_xxxxxxx. Указанный файл находится в библиотеке периферии по пути: Libraries\STM32F10x_StdPeriph_Driver\src, а его заголовок \Libraries\STM32F10x_StdPeriph_Driver\inc.

Подключение заголовочных файлов драйвера периферии происходит в файле stm32f10x_conf.h, на который ссылается stm32f10x.h. Файл stm32f10x_conf.h – это просто набор заголовочных файлов драйверов конкретных периферий контроллера, подлежащих включению в проект. Изначально все заголовки «#include» отмечены как комментарии. Подключение заголовочного файла периферии заключается в снятии комментария с соответствующего названия файла. В нашем случае – это строка #include "stm32f10x_rcc.h". Очевидно, что файл stm32f10x_conf.h индивидуален для каждого проекта, т.к. разные проекты используют разную периферию.

И последнее. Надо указать несколько директив препроцессору компилятора и пути к заголовочным файлам.



Пути к заголовочным файлам могут быть иными, в зависимости от расположения библиотеки периферии относительно папки проекта, а вот наличие “ USE_STDPERIPH_DRIVER” – обязательно при подключении драйверов периферии стандартной библиотеки.

Итак, мы подключили стандартную библиотеку, к проекту. Более того, мы подключили один из стандартных драйверов периферии к проекту, выполняющих управление тактированием системы.

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



Таким образом, подключение заголовочного файла stm32f10x.h в приложении тянет за собой подключение других заголовочных файлов и файлов кода. Некоторые из представленных на рисунке описаны выше. Несколько слов об остальных. Файлы STM32F10x_PPP.x – это файлы-драйверы периферии. Пример подключения такого файла показан выше, это RCC – периферия управления тактированием системы. Если мы хотим подключить драйверы другой периферии, то название подключаемых файлов получается заменой «PPP» названием периферии, например АЦП - STM32F10x_ADC.с, или порты ввода-вывода STM32F10x_GPIO.с, или ЦАП - STM32F10x_DAC.с. В целом интуитивно понятно какой файл нужно подключить при подключении заданной периферии. Файлы «misc.c», «misc.h» - это по большому счёту те же STM32F10x_PPP.x, только выполняют управление ядром. Например настройку векторов прерываний, который встроен в ядро или управление таймером SysTick, который является частью ядра. Файлы xxxxxxx_it.c описывают вектора немаскируемых прерываний контроллера. Они могут быть дополнены векторами прерываний периферии. Файл core_m3.h описывает ядро CortexM3. Данное ядро стандартизовано и может встречаться в микроконтроллерах других производителей. Для кросс-платформенной универсализации компания STMicroelectronics провела работу по созданию отдельной библиотеки ядра CortexM, после чего компания ARM стандартизировала её и распространила на других производителей микроконтроллеров. Так что переход на STM32 с контроллеров других производителей с ядром CortexM будет чуть проще.

Итак, мы можем подключить библиотеку стандартной периферии к любому семейству STM32. Того, кто научился это делать ждёт приз: очень простое программирование микроконтроллеров. Библиотека кроме драйверов в виде исходных файлов содержит множество примеров применения периферии. Для примера, рассмотрим создание проекта с участием выходов сравнения таймера. При традиционном подходе мы будем внимательно изучать описание регистров данной периферии. Но сейчас мы можем изучить текст работающей программы. Заходим в папку примеров стандартной периферии, которая находится по пути ProjectSTM32F10x_StdPeriph_Examples. Здесь находятся папки примеров с названием применяемой периферии. Заходим в папку «TIM». Таймеры в STM32 имеют множество функций и настроек, поэтому одним примером возможности контроллера продемонстрировать невозможно. Поэтому внутри указанного каталога имеется множество примеров применения таймеров. Нас интересует генерация ШИМ сигнала таймером. Заходим в папку «7PWM_Output». Внутри имеется описание работы программы на английском языке и набор файлов:

main.c stm32f10x_conf.h stm32f10x_it.h stm32f10x_it.c system_stm32f10x.c

Если проект не имеет прерываний, то содержательная часть полностью расположена в файле main.c. Копируем эти файлы в каталог проекта. Скомпилировав проект, мы получим программу для STM32, которая настроит таймер и порты ввода-вывода на генерацию 7-ми ШИМ сигналов от таймера 1. Далее, мы можем приспособить уже написанный код под свою задачу. Например, уменьшить число ШИМ сигналов, изменить скважность, направление счёта и т.п. Функции и их параметры хорошо описаны в файле stm32f10x_stdperiph_lib_um.chm. Названия же функций и их параметров легко ассоциируются с их назначением для тех, кто немного знает английский язык. Для наглядности приводим часть кода взятого примера:

/* Time Base configuration */ TIM_TimeBaseStructure.TIM_Prescaler = 0; // предделение счётных импульсов отсутствует (16-битный регистр) TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // направление счёта вверх TIM_TimeBaseStructure.TIM_Period = TimerPeriod; // счёт выполнять до значения TimerPeriod (константа в программе) TIM_TimeBaseStructure.TIM_ClockDivision = 0; // предделение счётных отсутствует TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // счётчик переполнений для генерации событий (не используется в программе) TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // ввод значений TimeBaseStructure в регистры таймера 1 (ввод данных в эту // переменную - выше) /* Channel 1, 2,3 and 4 Configuration in PWM mode */ // настройка ШИМ выходов TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; // режим работы ШИМ2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // разрешитьвыход ШИМ сигналов таймера TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; // разрешить комплиментарный выходШИМтаймера TIM_OCInitStructure.TIM_Pulse = Channel1Pulse; // ширина импульс Channel1Pulse – константа в программе TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; // настройка полярности выхода TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; // настройка полярности комплиментарного выхода TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set; // установка безопасного состояния выхода ШИМ TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset; // установка безопасного состояния комплиментарного выхода ШИМ TIM_OC1Init(TIM1, &TIM_OCInitStructure); // ввод значений переменной TIM_OCInitStructure в регистры ШИМ канала 1 // таймера1 TIM_OCInitStructure.TIM_Pulse = Channel2Pulse; // меняем ширину импульса в переменной OCInitStructure и вводим её в TIM_OC2Init(TIM1, &TIM_OCInitStructure); // регистры ШИМ канала 2 таймера1 TIM_OCInitStructure.TIM_Pulse = Channel3Pulse; // меняем ширину импульса в переменной OCInitStructure и вводим её в TIM_OC3Init(TIM1, &TIM_OCInitStructure); // регистры ШИМ канала 3 таймера1 TIM_OCInitStructure.TIM_Pulse = Channel4Pulse; // меняем ширину импульса в переменной OCInitStructure и вводим её в TIM_OC4Init(TIM1, &TIM_OCInitStructure); // регистры ШИМ канала 4 таймера1 /* TIM1 counter enable */ TIM_Cmd(TIM1, ENABLE); // запускаем таймер1 /* TIM1 Main Output Enable */ TIM_CtrlPWMOutputs(TIM1, ENABLE); // разрешаем работу выходов сравнения таймера 1

В правой части автор оставил комментарий на русском языке к каждой строке программы. Если открыть этот же пример в описании функций библиотек stm32f10x_stdperiph_lib_um.chm, то мы увидим, что все используемые параметры функций имеют ссылку на собственное описание, где будут указаны их возможные значения. Сами функции тоже имеют ссылку на собственное описание и исходный код. Это очень полезно, т.к. зная, что функция делает, мы можем проследить, каким образом она это делает, на какие биты регистров периферии и как она воздействует. Это, во-первых, ещё один источник информации для освоения контроллера, основанный на практическом использовании контроллера. Т.е. вы сначала решите техническую задачу, а потом изучите само решение. Во-вторых, это поле для оптимизации программы тому, кого библиотека не устроит по скорости работы и объёму кода.



Итак, на ноги мы уже встали, в смысле на выводы микроконтроллера на плате STM32VL Discovery у нас подключено все что надо, говорить мы научились, на языке программирования Си, пора бы и в первый класс проект создать.

Написание программы

Закончив с созданием и настройкой проекта, можно приступить к написанию реальной программы. Как повелось у всех программистов, первой программой написанной для работы на компьютере, является программа, выводящая на экран надпись «HelloWorld», так и у всех микроконтроллерщиков первая программа для микроконтроллера производит мигание светодиода. Мы не будем исключением из этой традиции и напишем программу, которая будет управлять светодиодом LD3 на плате STM32VL Discovery.

После создания пустого проекта в IAR, он создает минимальный код программы:

Теперь наша программа будет всегда «крутиться» в цикле while .

Для того, чтобы мы могли управлять светодиодом, нам необходимо разрешить тактирование порта к которому он подключен и настроить соответствующий вывод порта микроконтроллера на выход. Как мы уже рассматривали ранее в первой части, за разрешение тактирования порта С отвечает битIOPCEN регистра RCC_APB2ENR . Согласно документу «RM0041 Reference manual .pdf » для разрешения тактирования шины порта С необходимо в регистре RCC_APB2ENR установить бит IOPCEN в единицу. Чтобы при установке данного бита, мы не сбросили другие, установленные в данном регистре, нам необходимо к текущему состоянию регистра применить операцию логического сложения (логического «ИЛИ») и после этого записать полученное значение в содержимое регистра. В соответствии со структурой библиотеки ST, обращение к значению регистра для его чтения и записи производится через указатель на структуру RCC -> APB 2 ENR . Таким образом, вспоминая материал со второй части, можно записать следующий код, выполняющий установку бита IOPCEN в регистре RCC_APB2ENR :

Как можно убедиться, из файла «stm32f10x.h», значение бита IOPCEN определено как 0x00000010, что соответствует четвертому биту (IOPCEN ) регистра APB2ENR и совпадает со значением указанным в даташите.

Теперь аналогичным образом настроим вывод 9 порта С . Для этого нам необходимо настроить данный вывод порта на выход в режиме push-pull. За настройку режима порта на вход/выход отвечает регистр GPIOC_CRH , мы его уже рассматривали в , его описание также находится в разделе «7.2.2 Port configuration register high» даташита. Для настройки вывода в режим выхода с максимальным быстродействием 2МГц, необходимо в регистре GPIOC_CRH установить MODE9 в единицу и сбросить бит MODE9 в нуль. За настройку режима работы вывода в качестве основной функции с выходом push-pull отвечают биты CNF 9 иCNF 9 , для настройки требуемого нам режима работы, оба эти бита должны быть сброшены в ноль.

Теперь вывод порта, к которому подключен светодиод, настроен на выход, для управления светодиодом нам необходимо изменить состояние вывода порта, установив на выходе логическую единицу. Для изменения состояния вывода порта существует два способа, первый это запись непосредственно в регистр состояния порта измененного содержимого регистра порта, так же как мы производили настройку порта. Данный способ не рекомендуется использовать в виду возможности возникновения ситуации, при которой в регистр порта может записаться не верное значение. Данная ситуация может возникнуть если во время изменения состояния регистра, с момента времени когда уже было произведено чтение состояния регистра и до момента когда произведется запись измененного состояния в регистр, какое либо периферийное устройство или прерывание произведет изменение состояния данного порта. По завершению операции по изменению состояния регистра произойдет запись значения в регистр без учета произошедших изменений. Хотя вероятность возникновения данной ситуации является очень низкой, все же стоит пользоваться другим способом, при котором описанная ситуация исключена. Для этого в микроконтроллере существует два регистра GPIOx_BSRR и GPIOx_BRR . При записи логической единицы в требуемый бит регистра GPIOx_BRR произойдет сброс соответствующего вывода порта в логический ноль. Регистр GPIOx_BSRR может производить как установку, так и сброс состояния выводов порта, для установки вывода порта в логическую единицу необходимо произвести установку битов BSn , соответствующих номеру необходимого бита, данные биты располагаются в младших регистрах байта. Для сброса состояния вывода порта в логический нуль, необходимо произвести запись битов BRn соответствующих выводов, эти биты располагаются в старших разрядах регистра порта.

Светодиод LD3 подключен к выводу 9 порта С . Для включения данного светодиода, нам необходимо подать на соответствующем выводе порта логическую единицу, чтобы «зажечь» светодиод.

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

//Не забываем подключить заголовочный файл с описанием регистров микроконтроллера

#include "stm32f10x.h"

void Delay (void );

void Delay (void )
{
unsigned long i;
for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void )
{


RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

//очистим разряды MODE9 (сбросить биты MODE9_1 и MODE9_0 в нуль)
GPIOC->CRH &= ~GPIO_CRH_MODE9;

//Выставим бит MODE9_1, для настройки вывода на выход с быстродействием 2MHz
GPIOC->CRH |= GPIO_CRH_MODE9_1;

//очистим разряды CNF (настроить как выход общего назначения, симметричный (push-pull))
GPIOC->CRH &= ~GPIO_CRH_CNF9;

while (1)
{

//Установка вывода 9 порта С в логическую единицу («зажгли» светодиод)
GPIOC->BSRR = GPIO_BSRR_BS9;


Delay();


GPIOC->BSRR = GPIO_BSRR_BR9;


Delay();

}
}

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

Наша первая работоспособная программа написана, при её написании, для работы и настройки периферии, мы пользовались данными из официального даташита «RM0041 Reference manual .pdf », данный источник информации о регистрах микроконтроллера является самым точным, но для того чтобы им пользоваться приходится перечитывать много информации, что усложняет написание программ. Для облегчения процесса настройки периферии микроконтроллера, существуют различные генераторы кода, официальной утилитой от компании ST представлена программа Microxplorer , но она пока еще малофункциональна и по этой причине сторонними разработчиками была создана альтернативная программа «STM32 Генератор программного кода » . Данная программа позволяет легко получить код настройки периферии, используя удобный, наглядный графический интерфейс (см. рис. 2).


Рис. 2 Скриншот программы STM32 генератор кода

Как видно из рисунка 2, сгенерированный программой код настройки вывода светодиода совпадает с кодом написанным нами ранее.

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

Видео режима отладки программы мигания светодиодом

Видео работы программы мигания светодиодом на плате STM32VL Discovery

Библиотечные функции работы с периферией

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

Теперь напишем нашу программу с использованием библиотеки ST. В программе требуется произвести настройку портов ввода/вывода, для использования библиотечных функций настройки портов, необходимо произвести подключение заголовочного файла «stm32f10x_gpio.h » (см. табл. 1). Подключение данного файла можно произвести расскоментированием соответствующей строки в подключенном заголовочном конфигурационном файле «stm32f10x_conf.h ». В конце файла «stm32f10x_gpio.h » имеется список объявлений функций для работы с портами. Подробное описание всех имеющихся функций можно прочитать в файле «stm32f10x_stdperiph_lib_um.chm », краткое описание наиболее часто применяемых приведено в таблице 2.

Таблица 2.Описание основных функций настройки портов

Функция

Описание функции, передаваемых и возвращаемых параметров

GPIO_DeInit (
GPIO_TypeDef* GPIOx)

Производит установку значений регистров настройки порта GPIO x на значения по умолчанию

GPIO_Init (
GPIO_TypeDef* GPIOx,

Производит установку регистров настройки порта GPIOx в соответствии с указанными параметрами в структуре GPIO_InitStruct

GPIO_StructInit (
GPIO_InitTypeDef* GPIO_InitStruct)

Заполняет все поля структуры GPIO_InitStruct, значениями по умолчания

uint8_t GPIO_ReadInputDataBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin);

Чтение входного значения вывода GPIO_Pin порта GPIOx

uint16_t GPIO_ReadInputData (
GPIO_TypeDef* GPIOx)

Чтение входных значений всех выводов порта GPIOx

GPIO_SetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Установка выходного значения вывода GPIO_Pin порта GPIOx в логическую единицу

GPIO_ResetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Сброс выходного значения вывода GPIO_Pin порта GPIOx в логический ноль

GPIO_WriteBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin,
BitAction BitVal)

Запись значения BitVal в вывод GPIO_Pin порта GPIOx

GPIO_Write(
GPIO_TypeDef* GPIOx,
uint16_t PortVal)

Запись значения PortVal в порт GPIOx

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

Список переменных, включенных в структуры для функций работы с портами, описаны в том же файле несколько выше описания функций. Так, например, структура «GPIO_InitTypeDef » имеет следующую структуру:

typedef struct
{

uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */

GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */

GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */

}GPIO_InitTypeDef;

Первое поле данной структуры содержит переменную «GPIO _ Pin » типа unsigned short , в данную переменную необходимо записывать флаги номеров соответствующих выводов, для которых предполагается произвести необходимую настройку. Можно произвести настройку сразу несколько выводов, задав в качестве параметра несколько констант через оператор побитовое ИЛИ (см. ). Побитовое ИЛИ «соберёт» все единички из перечисленных констант, а сами константы являются маской, как раз предназначенной для такого использования. Макроопределения констант указаны в этом же файле ниже.

Второе поле структуры «GPIO_InitTypeDef » задает максимально возможную скорость работы выхода порта. Список возможных значений данного поля перечислен выше:

Описание возможных значений:

  • GPIO_Mode_AIN - аналоговый вход (англ. Analog INput);
  • GPIO_Mode_IN_FLOATING - вход без подтяжки, болтающийся (англ. Input float) в воздухе
  • GPIO_Mode_IPD - вход с подтяжкой к земле (англ. Input Pull-down)
  • GPIO_Mode_IPU - вход с подтяжкой к питанию (англ. Input Pull-up)
  • GPIO_Mode_Out_OD - выход с открытым стоком (англ. Output Open Drain)
  • GPIO_Mode_Out_PP - выход двумя состояниями (англ. Output Push-Pull - туда-сюда)
  • GPIO_Mode_AF_OD - выход с открытым стоком для альтернативных функций (англ. Alternate Function). Используется в случаях, когда выводом должна управлять периферия, прикрепленная к данному выводу порта (например, вывод Tx USART1 и т.п.)
  • GPIO_Mode_AF_PP - то же самое, но с двумя состояниями

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

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

//Объявляем структуру

/*
Прежде чем начать заполнение полей структуры, рекомендуется проинициализировать содержимое структуры данными по умолчанию, это делается в целях предотвращения записи неверных данных, если по какой либо причине не все поля структуры были заполнены.

Для передачи значений структуры в функцию необходимо перед именем структуры поставить символ &. Данный символ говорит компилятору, что необходимо передавать функции не сами значения, содержащиеся в структуре, а адрес в памяти, по которому располагаются данные значения. Это делается для того, чтобы уменьшить количество необходимых действий процессора по копированию содержимого структуры, а также позволяет экономить оперативную память. Таким образом, вместо передачи в функцию множества содержащихся в структуре байт, будет передан только один, содержащий адрес структуры.
*/

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */

GPIO_Init_struct.GPIO_Pin=GPIO_Pin_9;

/* Подобным образом заполним поле GPIO_Speed */

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

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

Теперь напишем нашу программу мигания светодиодом с использованием только библиотечных функций.

//Не забываем подключить заголовочный файл с описание регистров микроконтроллера

#include "stm32f10x.h"
#include "stm32f10x_conf.h"

//объявляем функцию программной задержки

void Delay (void );

//сама функция программной задержки

void Delay (void )
{
unsigned long i;
for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void )
{

//Разрешаем тактирование шины порта С
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

//Объявляем структуру для настройки порта
GPIO_InitTypeDef GPIO_Init_struct;

//Заполняем структуру начальными значениями
GPIO_StructInit(&GPIO_Init_struct);

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

// Подобным образом заполним поля GPIO_Speed и GPIO_Mode
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

//Передаем заполненную структуру, для выполнения действий по настройке регистров
GPIO_Init(GPIOC, &GPIO_Init_struct);

//Наш основной бесконечный цикл
while (1)
{
//Установка вывода 9 порта С в логическую единицу ("зажгли" светодиод)
GPIO_SetBits(GPIOC, GPIO_Pin_9);

//Добавляем программную задержку, чтобы светодиод светился некоторое время
Delay();

//Сброс состояния вывода 9 порта С в логический ноль
GPIO_ResetBits(GPIOC, GPIO_Pin_9);

//Добавляем снова программную задержку
Delay();
}
}

ссылке .

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

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

Обработчик прерывания

Микроконтроллеры имеют одну замечательную способность – останавливать выполнение основной программы по какому-то определенному событию, и переходить к выполнению специальной подпрограммы – обработчику прерывания . В качестве источников прерывания могут выступать как внешние события – прерывания по приему/передаче данных через какой либо интерфейс передачи данных, или изменение состояния вывода, так и внутренние – переполнение таймера и т.п.. Список возможных источников прерывания для микроконтроллеров серии STM32 приведен в даташите «RM0041 Reference manual » в разделе «8 Interrupts and events ».

Поскольку обработчик прерывания также является функцией, то и записываться она будет как обычная функция, но чтобы компилятор знал, что данная функция является обработчиком определенного прерывания, в качестве имени функции следует выбрать заранее определенные имена, на которые указаны перенаправления векторов прерывания. Список имен этих функций с кратким описанием находится в ассемблерном файле «startup_stm32f10x_md_vl.s ». На один обработчик прерывания может приходиться несколько источников вызывающих прерывания, например функция обработчик прерывания «USART1_IRQHandler » может быть вызвана в случае окончания приема и окончания передачи байта и т.д..

Для начала работы с прерываниями следует настроить и проинициализировать контроллер прерываний NVIC. В архитектуре Cortex M3 каждому прерыванию можно выставить свою группу приоритета для случаев, когда возникает несколько прерываний одновременно. Затем следует произвести настройку источника прерывания.

В поле NVIC_IRQChannel указывается, какое именно прерывание мы хотим настроить. Константа USART1_IRQn обозначает канал, отвечающий за прерывания, связанные с USART1. Она определена в файле «stm32f10x.h », там же определены другие подобные константы.

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

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

Теперь в настройках модуля необходимо установить параметры, по которым данный модуль будет генерировать прерывание. Для начала следует произвести включение прерывания, это делается вызовом функции name _ITConfig() , которая находится заголовочном файле периферийного устройства.

//Разрешаем прерывания по окончанию передачи байта по USART1
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

//Разрешаем прерывания по окончанию приема байта по USART1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

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

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

Для выполнения различных, небольших, повторяющихся с точным периодом действий, в микроконтроллерах с ядром Cortex-M3 имеется специально предназначенный для этого системный таймер. В функции данного таймера входит только вызов прерывания через строго заданные интервалы времени. Как правило, в вызываемом этим таймером прерывании, размещают код для измерения продолжительности различных процессов. Объявление функции настройки таймера размещено в файле «core _ cm 3. h ». В качестве передаваемого функции аргумента указывается число тактов системной шины между интервалами вызова обработчика прерывания системного таймера.

SysTick_Config(clk);

Теперь разобравшись с прерываниями, перепишем нашу программу, используя в качестве времязадающего элемента системный таймер. Поскольку таймер «SysTick » является системным и им могут пользоваться различные функциональные блоки нашей программы, то разумным будет вынести функцию обработки прерывания от системного таймера в отдельный файл, из этой функции вызывать функции для каждого функционального блока по отдельности.

Пример файла «main.с» программы мигания светодиода с использованием прерывания:

//Подключаем заголовочный файл с описанием регистров микроконтроллера

#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "main.h"

unsigned int LED_timer;

//Функция, вызываемая из функции-обработчика прерываний системного таймера

void SysTick_Timer_main(void )
{
//Если переменная LED_timer еще не дошла до 0,
if (LED_timer)
{
//Проверяем ее значение, если оно больше 1500 включим светодиод
if (LED_timer>1500) GPIOC->BSRR= GPIO_BSRR_BS9;

//иначе если меньше или равно 1500 то выключим
else GPIOC->BSRR= GPIO_BSRR_BR9;

//Произведем декремент переменной LED_timer
LED_timer--;
}

//Ели же значение переменной дошло до нуля, зададим новое значение 2000
else LED_timer=2000;
}

//Наша главная функция

void main(void )
{

//Разрешаем тактирование шины порта С
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

//Объявляем структуру для настройки порта
GPIO_InitTypeDef GPIO_Init_struct;

//Заполняем структуру начальными значениями
GPIO_StructInit(&GPIO_Init_struct);

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

// Подобным образом заполним поля GPIO_Speed и GPIO_Mode
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

//Передаем заполненную структуру, для выполнения действий по настройке регистров
GPIO_Init(GPIOC, &GPIO_Init_struct);

//выбираем приоритетную группу для прерываний
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

//Настраиваем работу системного таймера с интервалом 1мс
SysTick_Config(24000000/1000);

//Наш основной бесконечный цикл
while (1)
{
//В этот раз тут пусто, все управление светодиодом происходит в прерываниях
}
}

Часть исходного кода в файле «stm32f10x_it.c»:


#include "main.h"

/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/

void SysTick_Handler(void )
{
SysTick_Timer_main();
}

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

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

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

До этого момента мы использовали стандартную библиотеку ядра - CMSIS. Для настройки какого-либо порта на нужный режим работы нам приходилось обращаться к , чтобы найти отвечающий за определенную функцию регистр, а также искать по большому документу другую связанную с этим процессом информацию. Дело примет еще большие мучительные и рутинные обороты, когда мы приступим к работе с таймером или АЦП. Количество регистров там значительно больше, чем у портов ввода-вывода. Ручная настройка отнимает немало времени и повышает шанс допустить ошибку. Поэтому многие предпочитают работать со стандартной библиотекой периферии - StdPeriph. Что же она дает? Всё просто - повышается уровень абстракции, вам не нужно лезть в документацию и думать о регистрах в большинстве своеём. В этой библиотеке все режимы работы и параметры периферии МК описаны в виде структур. Теперь для настройки периферийного устройства необходимо лишь вызвать функцию инициализации устройства с заполненной структурой.

Ниже приведена картинка со схематичным изображением уровней абстракции.

Мы работали с CMSIS (которая находится «ближе» всего к ядру), чтобы показать, как устроен микроконтроллер. Следующим шагом является стандартная библиотека, пользоваться которой мы научимся сейчас. Дальше идут драйвера устройств. Под ними понимаются *.c \ *.h -файлы, которые обеспечивают удобный программный интерфейс для управления каким-либо устройством. Так, например, в этом курсе мы предоставим вам драйверы для микросхемы max7219 и WiFi-модуля esp8266.

Стандартный проект будет включать в себя следующие файлы:


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

Файлы библиотеки можно найти на странице, посвященной целевому МК (для нас это stm32f10x4), в разделе Design Resources (в среде CooCox IDE эти файлы скачиваются из репозитория среды разработки). Каждой периферии соответствуют два файла - заголовочный (*.h) и исходного кода (*.c). Детальное описание можно найти в файле поддержки, который лежит в архиве с библиотекой на сайте.

  • stm32f10x_conf.h - файл конфигурации библиотеки. Пользователь может подключить или отключить модули.
  • stm32f10x_ppp.h - заголовочный файл периферии. Вместо ppp может быть gpio или adc.
  • stm32f10x_ppp.c - драйвер периферийного устройства, написанный на языке Си.
  • stm32f10x_it.h - заголовочный файл, включающий в себя все возможные обработчики прерываний (их прототипы).
  • stm32f10x_it.c - шаблонный файл исходного кода, содержащий сервисные рутинные прерывания (англ. interrupt service routine , ISR) для исключительных ситуаций в Cortex M3. Пользователь может добавить свои ISR для используемой периферии.

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

  • PPP - акроним для периферии, например, ADC.
  • Системные, заголовочные файлы и файлы исходного кода - начинаются с stm32f10x_ .
  • Константы, используемые в одном файле, определены в этом файле. Константы, используемые в более чем одном файле, определены в заголовочных файлах. Все константы в библиотеке периферии чаще всего написаны в ВЕРХНЕМ регистре.
  • Регистры рассматриваются как константы и именуются также БОЛЬШИМИ буквами.
  • Имена функций, относящихся к периферии, содержат акроним, например, USART_SendData() .
  • Для настройки каждого периферийного устройства используется структура PPP_InitTypeDef , которая передается в функцию PPP_Init() .
  • Для деинициализации (установки значения по умолчанию) можно использовать функцию PPP_DeInit() .
  • Функция, позволяющая включить или отключить периферию, именуется PPP_Cmd() .
  • Функция включения/отключения прерывания именуется PPP_ITConfig .

С полным списком вы опять же можете ознакомиться в файле поддержки библиотеки. А теперь давайте перепишем мигание светодиода с использованием стандартной библиотеки периферии!

Перед началом работы заглянем в файл stm32f10x.h и найдем строчку:

#define USE_STDPERIPH_DRIVER

Если вы будете настраивать проект с нуля, используя файлы библиотеки из скачанного архива, то вам будет необходимо раскомментировать данную строчку. Она позволит использовать стандартную библиотеку. Данное определение (макрос) скомандует препроцессору подключить файл stm32f10x_conf.h:

#ifdef USE_STDPERIPH_DRIVER #include "stm32f10x_conf.h" #endif

В этом файле подключаются модули. Если вам нужны только конкретные - отключите остальные, это сэкономит время при компиляции. Нам, как вы уже могли догадаться, нужны модули RTC и GPIO (однако в будущем потребуются также _bkp.h , _flash , _pwr.h , _rtc.h , _spi.h , _tim.h , _usart.h):

#include "stm32f10x_flash.h" // for init_pll() #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h"

Как и в прошлый раз, для начала нужно включить тактирование порта B. Делается это функцией, объявленной в stm32f10x_rcc.h:

Void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

Перечисление FunctionalState определено в stm32f10x.h:

Typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState;

Объявим структуру для настройки нашей ножки (найти её можно в файле stm32f10x_gpio.h):

GPIO_InitTypeDef LED;

Теперь нам предстоит её заполнить. Давайте посмотрим на содержание этой структуры:

Typedef struct { uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; } GPIO_InitTypeDef;

Все необходимые перечисления и константы можно найти в этом же файле. Тогда переписанная функция init_leds() примет следующий вид:

Void led_init() { // Включаем тактирование RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // Объявляем структуру и заполняем её GPIO_InitTypeDef LED; LED.GPIO_Pin = GPIO_Pin_0; LED.GPIO_Speed = GPIO_Speed_2MHz; LED.GPIO_Mode = GPIO_Mode_Out_PP; // Инициализируем порт GPIO_Init(GPIOB, &LED); }

Перепишем функцию main() :

Int main(void) { led_init(); while (1) { GPIO_SetBits(GPIOB, GPIO_Pin_0); delay(10000000); GPIO_ResetBits(GPIOB, GPIO_Pin_0); delay(10000000); } }

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

Необходимое для разработки программное обеспечение. В этой статье я расскажу как его правильно настроить и связать. Все коммерческие среды такие как IAR EWARM или Keil uVision обычно сами выполняют эту интеграцию, однако в нашем случае все придется настраивать вручную, потратив на это немало времени. Преимуществом является то, что у вас есть шанс понять как это все работает изнутри, и в дальнейшем гибко настраивать все под себя. Перед началом настройки рассмотрим структуру среды в которой мы будем работать:

Eclipse будет использован для удобного редактирования файлов реализации функций (.c ), заголовочных файлов (.h ), а также файлов ассемблера (.S ). Под "удобным" я понимаю использование автодополненения кода, подсветки синтаксиса, рефакторинга, навигации по функциям и их прототипам. Файлы автоматически скармливаются нужным компиляторам, которые генерируют объектный код (в файлах .o ). Пока что этот код не содержит абсолютных адресов переменных и функций и по этому для выполнения не пригоден. Полученные объектные файлы собираются воедино сборщиком (linker-ом). Чтобы знать, какие участки адресного пространства использовать, сборщик использует специальный файл (.ld ), который называется линкер-скриптом. Он обычно содержит определение адресов секций и их размеров (секция кода, отображаемая на флеш, секция переменных, отображаемая на ОЗУ и т.д.).

В конце концов linker генерирует.elf файл (Executable and Linkable Format), который содержит в себе кроме инструкций и данных отладочную информацию (Debugging information), используемую отладчиком. Для обычной прошивки программой vsprog этот формат не подходит, поскольку для этого нужен более примитивный файл образа памяти (например Intel HEX - .hex). Для его генерации тоже есть инструмент из набора Sourcery CodeBench (arm-none-eabi-objcopy), и он отлично интегрируются в eclipse с помощью установленного ARM-плагина.

Для осуществления самой отладки используются три программы:

  1. сам eclipse, дающий возможность программисту "визуально" использовать отладку, ходить по строкам, наводить курсором мышки на переменные для просмотра их значений, и прочие удобности
  2. arm-none-eabi-gdb - GDB клиент - отладчик, который скрыто управляется eclips-ом(через stdin) в качестве реакции на действия, указанные в п.1. В свою очередь GDB подключается к Debug-серверу OpenOCD, и все поступающие на вход команды транслируются отладчиком GDB в команды, понятные для OpenOCD. Канал GDB <-> OpenOCD реализуется по протоколу TCP.
  3. OpenOCD - это debug-сервер, который может общаться непосредственно с программатором. Он запускается перед клиентом и ожидает подключения по TCP.

Данная схема может показаться вам весьма бесполезной: зачем использовать клиент и сервер в отдельности и выполнять лишний раз трансляцию команд, если все это можно было бы делать одним отладчиком? Дело в том, что такая архитектура теоретически позволяет удобно делать взаимозамену клиента и сервера. Например, если вам будет нужно вместо versaloon использовать другой программатор, который не будет поддерживать OpenOCD, а будет поддерживать другой специальный Debug-сервер (например texane/stlink для программатора stlink - который находится в отладочной плате STM32VLDiscovery), то вы просто вместо запуска OpenOCD будете запускать нужный сервер и все должно работать, без каких-либо дополнительных телодвижений. В то же время возможна обратная ситуация: допустим вы захотели использовать вместо связки Eclipse + CodeBench, среду IAR EWARM вместе с versaloon. У IAR есть свой встроенный Debug-клиент, который успешно свяжется с OpenOCD и будет им рулить, а также получать в ответ нужные данные. Однако все это иногда остается только в теории, поскольку стандарты общения клиента и сервера регламентированы не жестко, и местами могут отличатся, однако указанные мною конфигурации с st-link+eclipse и IAR+versaloon мне удавались.

Обычно клиент и сервер запускаются на одной машине и подключение к серверу происходит по адресу localhost:3333 (Для openocd), или localhost:4242 (для texane/stlink st-util). Но никто не мешает открыть порт 3333 или 4242 (и пробросить этот порт на роутере во внешнюю сеть) и ваши коллеги из другого города смогут подключится и отладить вашу железку. Данный трюк часто используется ембеддерами, работающими на удаленных объектах, доступ к которым ограничен.

Приступаем

Запускаем eclipse и выбираем File->New->C Project, выбираем тип проекта ARM Linux GCC (Sorcery G++ Lite) и имя "stm32_ld_vl" (Если у вас STV32VLDiscovery то логичнее будет назвать "stm32_md_vl"):

Нажимаем Finish, сворачиваем или закрываем окно Welcome. Итак, проект создан, и в вашем workspace должна появиться папка stm32_ld_vl. Теперь ее нужно наполнить необходимыми библиотеками.

Как вы поняли из названия проекта, я буду создавать проект для вида линейки low-density value line (LD_VL). Чтобы создать проект для других микроконтроллеров вы должны заменить все файлы и define-ы в названии которых присутствует _LD_VL (или _ld_vl ) на нужные вам, в соответствии с таблицей:

Вид линейки Обозначение Микроконтроллеры (х может менятся)
Low-density value line _LD_VL STM32F100x4 STM32F100x6
Low-density _LD STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
Medium-density value line _MD_VL STM32F100x8 STM32F100xB
Medium-density
_MD
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
High density Value line _HD_VL STM32F100xC STM32F100xD STM32F100xE
High density _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
XL-density _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
Connectivity line _CL STM32F105xx и STM32F107xx

Чтобы понять логику таблицы, вы должны быть знакомы с маркировкой STM32 . То есть, если у вас VLDiscovery то дальше вам придется заменять все что связано с _LD_VL на _MD_VL, поскольку в дискавери распаян чип STM32F100RB, относящийся к Medium-density value line.

Добавление в проект библиотек CMSIS и STM32F10x Standard Peripherals Library

CMSIS (Cortex Microcontroller Software Interface Standard) - стандартизированная библиотека работы с микроконтроллерами Cortex, выполняющая реализацию уровня HAL (Hardware Abstraction Layer), тоесть позволяет абстрагироваться от деталей работы с регистрами, поиска адресов регистров по даташитам и т.д. Библиотека представляет собой набор из исходников на языке С и Asm. Ядерная (Core) часть библиотеки одинакова для всех Cortex-ов (Будь это ST, NXP, ATMEL, TI или еще кто другой), и разрабатывается компанией ARM. Другая же часть библиотеки отвечает за периферию, которая естественно различна у разных производителей. Поэтому в конечном итоге полная библиотека все равно распространяется производителем, хотя ядерную часть все же можно скачать отдельно на сайте ARM. Библиотека содержит определения адресов, код инициализации тактового генератора (удобно настраиваемый define-ами), и все прочее, что избавляет программиста от ручного введения в свои проекты определения адресов всяческих регистров периферии и определения битов значений этих регистров.

Но ребята из ST пошли дальше. Помимо поддержки CMSIS они предоставляют еще одну библиотеку для STM32F10x под названием Standard Peripherals Library (SPL ), которая может использоваться в дополнение к CMSIS. Библиотека обеспечивает более быстрый и удобный доступ к периферии, а также контролирует (в некоторых случаях) правильность работы с периферией. Поэтому данную библиотек часто называют набором драйверов к периферийным модулям. Она сопровождается пакетом примерчиков, разделенных по категориям для разной перифериии. Библиотека также есть не только для STM32F10x, но и под другие серии.

Скачать всю SPL+CMSIS версии 3.5 можно тут: STM32F10x_StdPeriph_Lib_V3.5.0 или на сайте ST. Разархивируйте архив. Создайте папки CMSIS и SPL в папке проекта и начнем копировать файлы к себе в проект:

Что копировать

Куда копировать (учитывая,
что папка проекта stm32_ld_vl)

Описание файла
Libraries/CMSIS/CM3/
CoreSupport/core_cm3.c
stm32_ld_vl/CMSIS/core_cm3.c Описание ядра Cortex M3
Libraries/CMSIS/CM3/
CoreSupport/core_cm3.h
stm32_ld_vl/CMSIS/ core_cm3.h Заголовки описания ядра

ST/STM32F10x/system_stm32f10x.c
stm32_ld_vl/CMSIS/ system_stm32f10x.c Функции инициализации и
управления тактовой частотой
Libraries/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/system_stm32f10x.h
stm32_ld_vl/CMSIS/ system_stm32f10x.h Заголовки к этим функциям
Libraries/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/stm32f10x.h
stm32_ld_vl/CMSIS/ stm32f10x.h Основное описание периферии
Libraries/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/startup/gcc_ride7/
startup_stm32f10x_ld_vl.s
stm32_ld_vl/CMSIS/ startup_stm32f10x_ld_vl.S
(!!! Внимание расширение файла CAPITAL S)
Файл с таблицей векторов
прерываний и init-ами на asm
Project/STM32F10x_StdPeriph_Template/
stm32f10x_conf.h
stm32_ld_vl/CMSIS/stm32f10x_conf.h Шаблон для настройки
периферийных модулей

inc/*
stm32_ld_vl/SPL/inc/* Заголовочные файлы SPL
Libraries/STM32F10x_StdPeriph_Driver/
src/*
stm32_ld_vl/SPL/src/* Реализация SPL

После копирования зайдите в Eclipse и сделайте Refresh в контекстном меню проекта. В результате в Project Explorer вы должны получить такую же структуру как на картинке справа.

Возможно вы заметили, что в папке Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/ есть папки для разных IDE (в разных IDE используются разные компиляторы). Я выбрал IDE Ride7, так как в ней используется компилятор GNU Tools for ARM Embedded, совместимый с нашим Sourcery CodeBench.

Вся библиотека конфигурируется с помощью препроцессора (с помощью define-ов), это позволят решить все необходимые ветвления еще на стадии компиляции (вернее даже перед ней) и избежать нагрузки в работе самого контроллера (которая наблюдалась бы, если бы конфигурирование выполнялось в RunTime). Например все оборудование различное для разных линеек и поэтому чтобы библиотека "узнала", какую линейку вы хотите использовать, вас просят раскомментировать в файле stm32f10x.h один из define-ов (соответствующих вашей линейке):

/* #define STM32F10X_LD */ /*!< STM32F10X_LD: STM32 Low density devices */
/* #define STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */
/* #define STM32F10X_MD */ /*!< STM32F10X_MD: STM32 Medium density devices */

И так далее...

Но делать этого я не советую. Файлы библиотек мы трогать пока не будем, а define мы сделаем позже с помощью настроек компилятора в Eclipse. И тогда Eсlipse будет вызвать компилятор с ключем -D STM32F10X_LD_VL , что для препроцессора абсолютно эквивалентно ситуации если бы вы расскомментировали "#define STM32F10X_LD_VL" . Таким образом код мы менять не будем, в следствии, при желании, когда-нибудь вы сможете вынести библиотеку в отдельную директорию и не копировать в папку каждого нового проекта.

Linker-скрипт

В контекстном меню проекта выбираем New->File->Other->General->File, Next. Выбираем корневую папку проекта (stm32_ld_vl). Вводим имя файла "stm32f100c4.ld" (или "stm32f100rb.ld" для дискавери). Теперь копируем и вставляем в eclipse:

ENTRY(Reset_Handler) MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 4K } _estack = ORIGIN(RAM) + LENGTH(RAM); MIN_HEAP_SIZE = 0; MIN_STACK_SIZE = 256; SECTIONS { /* Interrupt vector table */ .isr_vector: { . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); } >FLASH /* The program code and other data goes into FLASH */ .text: { . = ALIGN(4); /* Code */ *(.text) *(.text*) /* Constants */ *(.rodata) *(.rodata*) /* ARM->Thumb and Thumb->ARM glue code */ *(.glue_7) *(.glue_7t) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; } >FLASH .ARM.extab: { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH .ARM: { __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; } >FLASH .ARM.attributes: { *(.ARM.attributes) } > FLASH .preinit_array: { PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .); } >FLASH .init_array: { PROVIDE_HIDDEN (__init_array_start = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); } >FLASH .fini_array: { PROVIDE_HIDDEN (__fini_array_start = .); KEEP (*(.fini_array*)) KEEP (*(SORT(.fini_array.*))) PROVIDE_HIDDEN (__fini_array_end = .); } >FLASH _sidata = .; /* Initialized data */ .data: AT (_sidata) { . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ *(.data) *(.data*) . = ALIGN(4); _edata = .; /* define a global symbol at data end */ } >RAM /* Uninitialized data */ . = ALIGN(4); .bss: { /* This is used by the startup in order to initialize the .bss secion */ _sbss = .; /* define a global symbol at bss start */ __bss_start__ = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; } >RAM PROVIDE(end = _ebss); PROVIDE(_end = _ebss); PROVIDE(__HEAP_START = _ebss); /* User_heap_stack section, used to check that there is enough RAM left */ ._user_heap_stack: { . = ALIGN(4); . = . + MIN_HEAP_SIZE; . = . + MIN_STACK_SIZE; . = ALIGN(4); } >RAM /DISCARD/ : { libc.a(*) libm.a(*) libgcc.a(*) } }

Данный линкер-скрипт будет предназначен именно для контроллера STM32F100C4 (у которого 16 Кб флеша и 4 Кб ОЗУ), если у вас другой, то придется поменять параметры LENGTH у областей FLASH и RAM в начале файла (для STM32F100RB, который в Discovery: Flash 128K и ОЗУ 8К).

Сохраняем файл.

Настройка сборки (C/C++ Build)

Заходим в Project->Properties->C/C++ Build-> Settings->Tool Settings, и начинаем настраивать инструменты сборки:

1) Target Precessor

Выбираем под какое именно ядро Cortex компилятор будет работать.

  • Processor: cortex-m3

2) ARM Sourcery Linux GCC C Compiler -> Preprocessor

Добавляем два define-a путем передачи их через ключ -D компилятору.

  • STM32F10X_LD_VL - определяет линейку (о этом define-е я писал выше)
  • USE_STDPERIPH_DRIVER - указание библиотеке CMSIS, что она должна использовать драйвер SPL

3) ARM Sourcery Linux GCC C Compiler -> Directories

Добавляем пути к includ-ам библиотек.

  • "${workspace_loc:/${ProjName}/CMSIS}"
  • "${workspace_loc:/${ProjName}/SPL/inc}"

Теперь, например, если мы напишем:

#include "stm32f10x.h

То компилятор должен сначала поискать файл stm32f10x.h в директории проекта (он это делает всегда), он его там не найдет и приступит к поиску в папке CMSIS, путь к которой мы указали, ну и найдет его.

4) ARM Sourcery Linux GCC C Compiler -> Optimization

Включим оптимизацию функций и данных

  • -ffunction-sections
  • -fdata-sections

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

5) ARM Sourcery Linux GCC C Compiler -> General

Добавляем путь к нашему linker-скрипту: "${workspace_loc:/${ProjName}/stm32f100c4.ld}" (или как он у вас называется).

И ставим опции:

  • Do not use standard start files - не использовать стандартные файлы запуска.
  • Remove unused sections - удалить неиспользованные секции

Все, настройка закончена. OK.

С момента создания проекта мы много всего сделали, и кое-чего Eclipse мог не заметить, по этому нам нужно сказать ему чтобы он пересмотрел структуру файлов проекта. Для этого из контекстного меню проекта нужно сделать Index -> rebuild .

Hello светодиоды на STM32

Пора создать главный файл проекта: File -> New -> C/C++ -> Source File. Next. Имя файла Source file: main.c.

Копируем и вставляем в файл следующее:

#include "stm32f10x.h" uint8_t i=0; int main(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // Enable PORTB Periph clock RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Enable TIM2 Periph clock // Disable JTAG for release LED PIN RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE; // Clear PB4 and PB5 control register bits GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 | GPIO_CRL_CNF5); // Configure PB.4 and PB.5 as Push Pull output at max 10Mhz GPIOB->CRL |= GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tick/sec TIM2->ARR = 1000; // 1 Interrupt/1 sec TIM2->DIER |= TIM_DIER_UIE; // Enable tim2 interrupt TIM2->CR1 |= TIM_CR1_CEN; // Start count NVIC_EnableIRQ(TIM2_IRQn); // Enable IRQ while(1); // Infinity loop } void TIM2_IRQHandler(void) { TIM2->SR &= ~TIM_SR_UIF; //Clean UIF Flag if (1 == (i++ & 0x1)) { GPIOB->BSRR = GPIO_BSRR_BS4; // Set PB4 bit GPIOB->BSRR = GPIO_BSRR_BR5; // Reset PB5 bit } else { GPIOB->BSRR = GPIO_BSRR_BS5; // Set PB5 bit GPIOB->BSRR = GPIO_BSRR_BR4; // Reset PB4 bit } }

Хоть мы подключали библиотек SPL, тут она использована не была. Все обращения к полям вроде RCC->APB2ENR полностью описаны в CMSIS.

Можно выполнять Project -> Build All. Если все получилось, то в папке Debug проекта должен появится файл stm32_ld_vl.hex. Он был автоматически сгенерирован из elf встроенными инструментами. Прошиваем файл и видим как мигают светодиоды с частотой раз в секунду:

Vsprog -sstm32f1 -ms -oe -owf -I /home/user/workspace/stm32_ld_vl/Debug/stm32_ld_vl.hex -V "tvcc.set 3300"

Естественно вместо /home/user/workspace/ вы должны вписать свой путь к workspace.

Для STM32VLDiscovery

Код немного отличается от того, который я дал выше для своей отладочной платки. Отличие заключается в пинах, на которых "висят" светодиоды. Если у меня в плате это были PB4 и PB5, то в Discovery это PC8 и PC9.

#include "stm32f10x.h" uint8_t i=0; int main(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Enable PORTC Periph clock RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Enable TIM2 Periph clock // Clear PC8 and PC9 control register bits GPIOC->CRH &= ~(GPIO_CRH_MODE8 | GPIO_CRH_CNF8 | GPIO_CRH_MODE9 | GPIO_CRH_CNF9); // Configure PC8 and PC9 as Push Pull output at max 10Mhz GPIOC->CRH |= GPIO_CRH_MODE8_0 | GPIO_CRH_MODE9_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tick/sec TIM2->ARR = 1000; // 1 Interrupt/sec (1000/100) TIM2->DIER |= TIM_DIER_UIE; // Enable tim2 interrupt TIM2->CR1 |= TIM_CR1_CEN; // Start count NVIC_EnableIRQ(TIM2_IRQn); // Enable IRQ while(1); // Infinity loop } void TIM2_IRQHandler(void) { TIM2->SR &= ~TIM_SR_UIF; //Clean UIF Flag if (1 == (i++ & 0x1)) { GPIOC->BSRR = GPIO_BSRR_BS8; // Set PC8 bit GPIOC->BSRR = GPIO_BSRR_BR9; // Reset PC9 bit } else { GPIOC->BSRR = GPIO_BSRR_BS9; // Set PC9 bit GPIOC->BSRR = GPIO_BSRR_BR8; // Reset PC8 bit } }

Под Windows, прошить полученный hex(/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) можно утилитой от ST.

Ну а под linux утилитой st-flash. НО!!! Утилита не хавает hex формата Intel HEX (который генерируется по дефолту), поэтому крайне важно в настройках создания Flash-образа, выбрать формат binary:

Расширение файла при этом не поменяется (останется hex как и было), но формат файла изменится. И только после этого можно выполнять:

St-flash write v1 /home/user/workspace/stm32_md_vl/Debug/stm32_md_vl.hex 0x08000000

Кстати, на счет расширения и формата: обычно бинарные файлы помечают расширением.bin, в то время как файлы формата Intel HEX именуют расширением.hex. Отличие в этих двух форматах скорей техническое, чем функциональное: бинарный формат содержит просто байты инструкций и данных, которые будут просто записываться в контроллер программатором "как есть". IntelHEX же имеет не бинарный формат, а текстовый: точно те же байты разбиты по 4 бита и представлены посимвольно в формате ASCII, причем использованы только символы 0-9, A-F (bin и hex - системы счисления с кратными основаниями, то есть 4 бита в bin можно представить одной цифрой в hex). Так что формат ihex более чем в 2 раза превышает размер обычного бинарного файла (каждые 4 бита заменяются байтом + переносы строк для удобного чтения), но его можно читать в обычном текстовом редакторе. Поэтому, если вы собираетесь отправить этот файл кому-то, или использовать его в других программах-программаторах, то желательно переименовать его в stm32_md_vl.bin, дабы не вводить в заблуждение тех, кто будет смотреть на его имя.

Итак мы настроили сборку прошивки для stm32. В следующий раз я расскажу как

Когда только начинаешь программировать микроконтроллеры или давно не занимался программированием, то разбираться в чужом коде довольно не легко. Вопросы "Что это такое?" и "Откуда это взялось?" возникают чуть ли не на каждом сочетании букв и цифр. И чем быстрее приходит понимание логики "что? зачем? и откуда?", тем легче проходит изучение чужого кода, в том числе и примеров. Правда иногда для этого приходиться не один день "попрыгать по коду" и "полистать мануалов".

У всех микроконтроллеров STM32F4xx довольно много периферии. За каждым периферийным устройством микроконтроллеров закреплена определённая, конкретная и неперемещаемая область памяти. Каждая область памяти состоит из регистров памяти, причём эти регистры могут быть 8-разрядными, 16-разрядными, 32-разрядными или ещё как, зависит от микроконтроллера. В микроконтроллере STM32F4 эти регистры 32-разрядные и каждый регистр имеет своё назначение и свой конкретный адрес. Ничто не мешает в своих программах обращаться к ним напрямую, указывая адрес. По какому адресу размещен тот или иной регистр и к какому периферийному устройству он относиться указывается в карте памяти. Для STM32F4 такая карта памяти есть в документе DM00031020.pdf, который можно найти на сайте st.com. Документ называется

RM0090
Reference manual
STM32F405xx/07xx, STM32F415xx/17xx, STM32F42xxx and STM32F43xxx advanced ARM-based 32-bit MCUs

В разделе 2.3 Memory map на странице 64 начинается таблица с адресами областей регистров и их принадлежностью к периферийному устройству. В той же таблице есть ссылка на раздел с более подробным распределением памяти для каждой периферии.

Слева в таблице указан диапазон адресов, в середине название периферии и в последнем столбце - где находиться более подробное описание распределения памяти.

Так для портов ввода-вывода общего назначения GPIO в таблице распределения памяти можно найти что для них выделены адреса начиная с 0х4002 0000. Порт ввода-вывода общего назначения GPIOA занимает диапазон адресов от 0х4002 000 до 0х4002 03FF. Порт GPIOB занимает диапазон адресов 0х4002 400 - 0х4002 07FF. И так далее.

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

Здесь также находиться таблица, но уже с картой памяти для диапазона адресов GPIO. Согласно этой карте памяти первые 4 байта принадлежат регистру MODER, следующие 4 байта принадлежат регистру OTYPER и так далее. Адреса регистров считаются от начала диапазона, принадлежащему конкретному порту GPIO. То есть каждый регистр GPIO имеет конкретный адрес, который можно использовать при разработке программ для микроконтроллера.

Но использование адресов регистров для человека неудобно и чревато большим количеством ошибок. Поэтому производители микроконтроллеров создают стандартные библиотеки, которые облегчают работу с микроконтроллерами. В этих библиотеках физическим адресам ставиться в соответствие их буквенное обозначение. Для STM32F4xx эти соответствия заданы в файле stm32f4xx.h . Файл stm32f4xx.h принадлежит библиотеке CMSIS и лежит в папке Libraries\CMSIS\ST\STM32F4xx\Include\.

Посмотрим как определяется в библиотеках порт GPIOA. Аналогично определяется и всё остальное. Достаточно понять принцип. Файл stm32f4xx.h довольно большой и поэтому лучше использовать поиск или возможности, которые предоставляет ваш toolchain.

Для порта GPIOA находим строку в которой упоминается GPIOA_BASE

GPIOA_BASE определяется через AHB1PERIPH_BASE

AHB1PERIPH_BASE в свою очередь определяется через PERIPH_BASE

А в свою очередь PERIPH_BASE определяется как 0х4000 0000. Если посмотреть карту рапределения памяти периферийных устройств (в разделе 2.3 Memory map на странице 64), то увидим этот адрес в самом низу таблицы. С этого адреса начинаются регистры всей периферии микроконтроллера STM32F4. То есть PERIPH_BASE - это начальный адрес всей периферии микроконтроллеров STM32F4xx вообще, и микроконтроллера STM32F407VG в частности..

AHB1PERIPH_BASE определяется как сумма (PERIPH_BASE + 0x00020000). (см.картинки обратно). Это будет адрес 0х4002 0000. В карте памяти с этого адреса начинаются порты ввода-вывода общего назначения GPIO.

GPIOA_BASE определяется как (AHB1PERIPH_BASE + 0x0000), то есть это начальный адрес группы регистров порта GPIOA.

Ну а сам порт GPIOA определяется как структура из регистров, размещение которых в памяти начинается с адреса GPIOA_BASE (см строку #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE).

Структура каждого порта GPIO определяется как тип GPIO_TypeDef.

Таким образом, стандартные библиотеки, в данном случае файл stm32f4xx.h , просто очеловечивают машинную адресацию. Если вы увидите запись GPIOA->ODR = 1234, то это означает, что по адресу 0х40020014 будет записано число 1234. GPIOA имеет начальный адрес 0х40020000 и регистр ODR имеет адрес 0х14 от начала диапазона, поэтому GPIOA->ODR имеет адрес 0х40020014.

Или например, вам не нравиться запись GPIOA->ODR, то можно определить #define GPIOA_ODR ((uint32_t *) 0x40020014) и получить тот же самый результат, записав GPIOA_ODR = 1234;. Только вот насколько это целесообразно? Если действительно хочется ввести свои обозначения, то лучше просто переназначить стандартные. Как это делается, можно посмотреть в файле stm32f4_discovery.h Например, вот так там определяется один из светодиодов:

#define LED4_PIN GPIO_Pin_12
#define LED4_GPIO_PORT GPIOD
#define LED4_GPIO_CLK RCC_AHB1Periph_GPIOD

Более детальное описание периферии портов находиться в stm32f4xx_gpio.h

Лучшие статьи по теме