В прошлой статье мы настроили IDE, и теперь просто обязаны испытать STM32 в деле. Этот урок будет служить этаким трамплином для программерского прыжка в STM32: помигаем светодиодами, поиграемся с таймером — легко и непринуждённо, без копошения в несущественных сейчас деталях. Цель урока — дать общее представление о том, как программируются эти МК.
На всякий случай, проясню ситуацию с курсом: он не для чайников. Этим словом я не хочу никого обидеть, а лишь хочу указать на нижнюю планку необходимых знаний. Я предполагаю, что для освоения курса падаван должен:
Сносно писать на языке программирования C. То есть, уметь кодить на C консольные утилитки для ПК, хотя бы поверхностно представлять себе, что получается из кода при компиляции, уверенно пользоваться указателями (в т.ч. указателями на указатели).
Уметь работать в командной строке и знать про переменные окружения вроде PATH.
Уметь читать. Это не шутка, часто люди пробегают глазами сообщения об ошибках, даже не понимая написанного, а то и вовсе их не читают, а потом задают вопросы: «А что значит ошибка gcc: fatal error: no input files?», хотя ответ очевиден.
Естественно, знать азы электроники. Курс — не про подключение светодиодов к плате, а про микроконтроллеры STM32 и их особенности.
И, конечно же, приветствуется умение работать в Linux (:
Итак, приступим к практическому использованию STM32. Для всех микроконтроллеров Cortex-M3 есть одна общая для семейства библиотека CMSIS, которая содержит в себе описание констант, функций, адресов регистров и т.п. для работы с системным таймером SysTick и контроллером прерываний. Доступ к остальной периферии, которая уже зависит от производителя, осуществляется через библиотеки, предоставляемые производителем конкретного МК. У ST Microelectronics такая библиотека для каждого семейства МК называется Standard Peripheral Library. Название длинноватое, так что далее я её буду звать просто SPL.
SPL предоставляет не только стандартный способ работы с периферией — запись в регистры, но и множество полезных функций, сильно облегчающих жизнь: например, чтобы инициализировать таймер, не обязательно помнить имена регистров — достаточно заполнить специальную структуру нужными константами и вызывать функцию инициализации для периферии. Код писать становится в разы проще, так что будем использовать SPL.
Сделайте копию скелетного проекта для своей платы и настройте его, если ещё не сделали этого (описание настройки тут). Я использую плату STM32VLDiscovery, и буду писать код для неё, так что назвал копию stm32vld_quickstart. Всё, что нужно сделать после копирования, чтобы можно было полноценно работать с проектом — это заменить название ELF-бинарника в файлах gdb_commands_debug и gdb_commands_release с stm32vld_template.elf на stm32vld_quickstart.elf. К эльфам, кстати, эти файлы никакого отношения не имеют: ELF = Executable and Linkable Format.
Для начала, зажгём два пользовательских светодиода, которые находятся на краю платы, противоположном разъёму mini-USB. Они подключены к порту C, к пинам 8 и 9. У STM32 порты 16-битные, так что не удивляйтесь такой нумерации пинов.
Как вы, наверное, заметили, плата STM32VLDiscovery поставляется с прошивкой, которая при включении платы мигает светодиодом PC9, а при нажатии пользовательской кнопки (синяя) на секунду зажигает светодиод PC8 и увеличивает скорость мигания светодиода PC9. А мы возьмём, и всё сломаем!
Откройте в проекте исходник main.c и скопируйте в него следующий код:
#include <stm32f10x.h>
/* Подключаем функции управления генератором частоты и GPIO */
#include <stm32f10x_rcc.h>
#include <stm32f10x_gpio.h>
int main()
{
/* Включаем тактирование порта C */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
/* Заполняем структуру gpio данными для инициализации:
* - Режим: вывод, Push-Pull
* - Пины: 8 и 9
* - Частота обновления: 2 МГц
*/
GPIO_InitTypeDef gpio;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
gpio.GPIO_Speed = GPIO_Speed_2MHz;
/* Инициализируем GPIO на порту C */
GPIO_Init(GPIOC, &gpio);
/* Устанавливаем единички на выводах 8 и 9 */
GPIO_SetBits(GPIOC, GPIO_Pin_8 | GPIO_Pin_9);
do __NOP(); while (1); // зависаем
}
Жмите Ctrl+B, чтобы собрать проект, запускайте утилиту stlink (командой «st-util -1») и отладочную конфигурацию в Eclipse через меню Debug→Debug configurations..., чтобы программа залилась в МК. Когда запустится отладка и курсор встанет на первом выражении в main(), жмите F8 (continue). Оба светодиода должны зажечься. Кстати, если вас напрягает долгая прошивка через stlink, в Windows вы можете использовать улитилу CoFlash от создателей CoIDE — тогда из файлов gdb_commands* нужно будет убрать строки «load <конфигурация>/stm32vld_quickstart.elf».
Наверняка у вас возникли некоторые вопросы — например, про тактирование порта C и частоту GPIO. Это особенности STM32: при включении МК тактирование на периферию не подаётся — это сделано для снижения энергопотребления, так что прежде чем что-то пытаться делать с периферией, нужно подать на неё тактовый сигнал. Это касается всей периферии, не только GPIO. Что до частоты — это частота обновления состояния вывода GPIO. Сам контроллер STM32F100RBT6B может работать на частоте 24 МГц, но скорость работы пинов может быть другой. Для STM32F10x доступны 3 частоты — 2, 10 и 50 МГц. В случае с нашим МК работать будут только 2 и 10 МГц, а сейчас нам и 2 МГц хватит за глаза.
Ну, одними светодиодами сыт не будешь, так что давайте-ка задействуем пользовательскую кнопку на PA0. Пусть при её нажатии зажигается один светодиод и гаснет другой:
#include <stm32f10x.h>
/* Подключаем функции управления генератором частоты и GPIO */
#include <stm32f10x_rcc.h>
#include <stm32f10x_gpio.h>
const uint16_t
LED1 = GPIO_Pin_8, // PC8
LED2 = GPIO_Pin_9, // PC9
LEDS = GPIO_Pin_8 | GPIO_Pin_9,
BUTTON = GPIO_Pin_0; // PA0
void init_button();
void init_leds();
int main()
{
init_button();
init_leds();
/* Один светодиод зажигаем, другой - гасим */
GPIO_ResetBits(GPIOC, LED1);
GPIO_SetBits(GPIOC, LED2);
while (1)
{
static uint8_t btn_old_state = 0;
/* Читаем бит состояния кнопки */
uint8_t btn_state = GPIO_ReadInputDataBit(GPIOA, BUTTON);
/* По нажатию инвертируем биты в порту C, сответствующие светодиодам */
if (btn_old_state == 0 && btn_state == 1)
GPIO_Write(GPIOC, ~GPIO_ReadOutputData(GPIOC) & LEDS);
btn_old_state = btn_state;
}
}
void init_button()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* Всё то же, что и со светодиодами, только конфигурируем
* на вход без подтяжки (GPIO_Mode_IN_FLOATING).
*/
GPIO_InitTypeDef gpio;
GPIO_StructInit(&gpio);
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
gpio.GPIO_Pin = BUTTON;
gpio.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &gpio);
}
void init_leds()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef gpio;
GPIO_StructInit(&gpio);
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Pin = LEDS;
gpio.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &gpio);
}
Как видите, сконфигурировать пин на вход ничуть не сложнее, чем на выход. Функции SPL позволяют читать/писать как порты целиком, так и отдельные комбинации битов, что очень удобно. Код получается не сложнее ардуинистого. Кстати, в общей сложности режимов GPIO имеется 8 — входы/выходы со всякими подтяжками и Open Drain. Но, если заглянуть в STM32VLDiscovery User manual (скачать у нас), то там вы найдёте принципиальную схему платы, на которой видно, что пользовательская кнопка уже прижата к земле через резистор R21 (10 K). О режимах GPIO я расскажу в следующей статье.
Антидребезг я здесь никакой не применял, так что реакция на нажатие кнопки не всегда будет адекватной. Но мы можем это исправить примитивным способом — опрашивать кнопку не постоянно, а 100 раз в секунду, к примеру. Тут-то нам и пригодится таймер SysTick, который не умеет ШИМ и прочих вкусностей, но зато 24-битный и очень лёгкий в использовании. А для пущей крутизны нагрузим его миганием светодиода:
Здесь мы настраиваем таймер так, чтобы он срабатывал каждые SystemCoreClock/100 тиков. Костанта SystemCoreClock в случае с STM32100RBT6B равна 24000000 (24 МГц) — это максимальная его частота при работе от кварца. Тут нужно учитывать, что таймер SysTick 24-битный, и значения больше 16777216 он использовать не может, но это легко обойти, используя свою переменную-счётчик в обработчике прерывания. В этом примере я использовал счётчик для переключения светодиода 10 раз в секунду. По-хорошему, обработку прерывания лучше делать в главном цикле, а в прерывании выставлять какой-нибудь флаг. Прерывания и правильную работу с ними мы тоже, со временем, рассмотрим под всеми углами.
Теперь вы получили представление о том, как работать с SPL, и готовы к более серьёзным вещам — в следующей статье мы изучим GPIO вдоль и поперёк.
На мой взгляд если эту платформу «обрастить» нормальными библиотеками, автоматизирующими процесс заполнения этой тучи полей с названиями данными какими-то злыми Си-шниками, то может быть её можно будет пользоваться, а так на фоне ардуины смотрится очень паршиво всякий бред типа:
А вообще я где-то видел платки ARM с ардуино-загрузщиком, поэтому поклонникам этого типа контроллера можно писать с Arduino IDE. Касаемо статьи в целом, очень информативно — респект автору.
ну в общем-то кто хотел тот уже прикрутил ардуино и к PIC-контроллерам и официальный разработчик ардуины выпустил на ARM свою плату DUE Таким образом я не вижу смысла тратить силы на изучение менее удобных платформ, в то время когда лучше изучать и развивать более перспективную ардуину))
А почему линия А0 (к которой подключена кнопка) настроена как плавающий вход(GPIO_Mode_IN_FLOATING)??? Всегда использовал внутреннюю подтяжку к питанию. Соответственно когда кнопка не нажата, то на выводе уровень логической единицы, если нажали кнопку — на выводе контроллера логический ноль. Так же можно здесь сделать???
Можно, но если заглянуть в STM32VLDiscovery User manual, то там вы найдёте принципиальную схему платы, на которой видно, что пользовательская кнопка уже прижата к земле через резистор R21 (10 K). R22 (0) — это просто перемычка, если что. Пожалуй, не лишним будет упомянуть это в статье.
Кстати, одного вопросительного знака мне вполне достаточно, чтобы заметить вопросительную интонацию предложения (:
Тем, что названия функций, констант и структур из SPL куда понятнее, чем имена регистров вроде BSRR и т.п. — код становится проще читать. Буду кодить через регистры только когда упрусь в производительность или если через них окажется проще (бывает и такое).
А вот если я пойду на работу и буду кодить с использованием библиотеки, какое ко мне будет отношение со стороны начальства и коллег? Мне могут сказать: «Эх студент! И чего там тебя в универе учили? Так не пойдет, переделывай!»?..
Просто я еще студент и нигде не работал вот и интересуюсь.
Зависит от того, к кому попадёте. В нормальных конторах велосипедостроительство — моветон, так как использование библиотек — залог более быстрого написания кода.
А вообще, использование библиотеки не освобождает от знания регистров, а всего-навсего упрощает популярные способы работы с периферией и пр. Ковырять биты в регистрах лучше тогда, когда освоишься с кристаллом, я так считаю.
Прикрутил примеры к HY-MINI. Эта плата очень похожа на дискавери, но пришлось разобраться с раскладкой входов-выходов :)
Плата хороша тем, что в комплекте идет цветной дисплей, CD c кучей примеров, Keil, ULINK2.
Проблема в том, что вы невнимательно смотрели заголовочный файл и автодополнение в Eclipse, а также оставили часть полей неинициализированным. Вот так делать нельзя:
Вы объявили экземпляр структуры GPIO_InitTypeDef, но заполнили всего 3 поля из 5, забыв про GPIO_PuPd и GPIO_OType, так что в них может быть что угодно, что там было на стеке до выделения памяти под структуру — от длины ноги Обамы до веса какашки динозавра. Поэтому инженеры ST написали функции XXX_StructInit(), которые заполняют соответствующую структуру для периферии XXX некоторыми безопасными значениями по умолчанию. Приучайтесь писать после каждого
GPIO_InitTypeDef gpio;
также
GPIO_StructInit(&gpio);
И далее в том же духе при работе с другой периферией: USART_StructInit(), SPI_StructInit() и т.п. Полезно заглядывать в код этих функций, чтобы знать, какие значения присваиваются полям структуры по умолчанию — прочитать всплывающую подсказку по функции, например (в ней показывается код функции).
Ну и универсальное правило любого сишника: всегда инициализируй переменные нулём или другим безопасным значением. Если нет готовой функции XXX_StructInit(), всегда можно забить нулями через memset():
Здравствуйте! Столкнулся со следующей проблемой при работе с STM32L1xx_StdPeriph_Lib_V1.2.0. Не могу разобраться с функциями, где передача параметров происходит через структуры. Например функция: void TIM_OC1Init ( TIM_TypeDef * TIMx, TIM_OCInitTypeDef * TIM_OCInitStruct ). В ней есть структура TIM_OCInitStruct, которая имеет тип TIM_OCInitTypeDef. Рассмотрим одно из полей структуры, например: uint16_t TIM_OCMode.
Это поле может принимать следующее значения:
00184 #define TIM_OCMode_Timing ((uint16_t)0×0000)
00185 #define TIM_OCMode_Active ((uint16_t)0×0010)
00186 #define TIM_OCMode_Inactive ((uint16_t)0×0020)
00187 #define TIM_OCMode_Toggle ((uint16_t)0×0030)
00188 #define TIM_OCMode_PWM1 ((uint16_t)0×0060)
00189 #define TIM_OCMode_PWM2 ((uint16_t)0×0070)
Скопировал из файла stm32l1xx_tim.h.
Проблема заключается в следующем. Как понять что значит каждый режим (как модуль работает в данном режиме)? Не всегда понятно из названия(. Насколько я понимаю, объявленную здесь константу нужно связать с регистрами и посмотреть референс мануал. Но непонятно к какому регистру относится константа.
Заранее благодарен.
Добрый день!
Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны!
Хотел узнать зачем использовать переменную типа uint8_t для хранения одного бита состояния кнопки:
Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях сгенерирует одинаковый код. Можете проверить дизассемблером, входящим в тулчейн:
$ arm-none-eabi-objdump -d <бинарник проекта>
Кстати, ваш сниппет можно переписать компактнее, без if:
btn_state = (GPIOA->IDR & 1<<0) != 0;
Что касается курса, спасибо, рад, что он оказался полезен. Жаль, не было возможности его доделать, как планировал (а то и вовсе переделать, на свежую голову). Впрочем, на дворе конец 2018 года, и STM32 уже стали обыденностью, так информации сейчас куча, и вы можете найти что-нибудь посвежее :)
Комментарии (30)
RSS свернуть / развернутьrouter32
А вообще я где-то видел платки ARM с ардуино-загрузщиком, поэтому поклонникам этого типа контроллера можно писать с Arduino IDE. Касаемо статьи в целом, очень информативно — респект автору.
execom
hexanaft
execom
hexanaft
SinauRus
burjui
и
Так уж назвали =)
hexanaft
А также под андроид. Все дело в названиях функций и переменных, в стиле
Помимо андроида и ARM, подобный подход кажется применяется на айфоне, при программировании интерфейсов в Windows.
Дополняя разговоры выше о плате Mapple от LeafLabs, прикупил ее на днях, будем изучать :)
oleamm
rikzi_em
Кстати, одного вопросительного знака мне вполне достаточно, чтобы заметить вопросительную интонацию предложения (:
burjui
В одной из статей вы написали Чем обосновывается ваш выбор работы с библиотекой периферии, а не напрямую через регистры?
rikzi_em
burjui
Просто я еще студент и нигде не работал вот и интересуюсь.
rikzi_em
А вообще, использование библиотеки не освобождает от знания регистров, а всего-навсего упрощает популярные способы работы с периферией и пр. Ковырять биты в регистрах лучше тогда, когда освоишься с кристаллом, я так считаю.
burjui
Плата хороша тем, что в комплекте идет цветной дисплей, CD c кучей примеров, Keil, ULINK2.
dongrigorio
Собираю для stm32f4
Собирается всё нормально. но не работает.
Помогите разобраться)
это мой проект
lamazavr
Вы объявили экземпляр структуры GPIO_InitTypeDef, но заполнили всего 3 поля из 5, забыв про GPIO_PuPd и GPIO_OType, так что в них может быть что угодно, что там было на стеке до выделения памяти под структуру — от длины ноги Обамы до веса какашки динозавра. Поэтому инженеры ST написали функции XXX_StructInit(), которые заполняют соответствующую структуру для периферии XXX некоторыми безопасными значениями по умолчанию. Приучайтесь писать после каждого
также
И далее в том же духе при работе с другой периферией: USART_StructInit(), SPI_StructInit() и т.п. Полезно заглядывать в код этих функций, чтобы знать, какие значения присваиваются полям структуры по умолчанию — прочитать всплывающую подсказку по функции, например (в ней показывается код функции).
Ну и универсальное правило любого сишника: всегда инициализируй переменные нулём или другим безопасным значением. Если нет готовой функции XXX_StructInit(), всегда можно забить нулями через memset():
burjui
Никакой разници. Диоды не загорелись
lamazavr
И всё-таки, читайте код функций:
burjui
код смотрел))) проглядел что он как вход метиться.
если можно взгляните на настройки проекта. Видимо в них дело.
lamazavr
burjui
lamazavr
Это поле может принимать следующее значения:
00184 #define TIM_OCMode_Timing ((uint16_t)0×0000)
00185 #define TIM_OCMode_Active ((uint16_t)0×0010)
00186 #define TIM_OCMode_Inactive ((uint16_t)0×0020)
00187 #define TIM_OCMode_Toggle ((uint16_t)0×0030)
00188 #define TIM_OCMode_PWM1 ((uint16_t)0×0060)
00189 #define TIM_OCMode_PWM2 ((uint16_t)0×0070)
Скопировал из файла stm32l1xx_tim.h.
Проблема заключается в следующем. Как понять что значит каждый режим (как модуль работает в данном режиме)? Не всегда понятно из названия(. Насколько я понимаю, объявленную здесь константу нужно связать с регистрами и посмотреть референс мануал. Но непонятно к какому регистру относится константа.
Заранее благодарен.
Dmitriy986
Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны!
Хотел узнать зачем использовать переменную типа uint8_t для хранения одного бита состояния кнопки:
Я воспользовался переменной bool для хранения состояния кнопки:
Есть ли преимущества в экономии памяти и времени обработки такого кода?
Спасибо!
Pavel
Кстати, ваш сниппет можно переписать компактнее, без if:
Что касается курса, спасибо, рад, что он оказался полезен. Жаль, не было возможности его доделать, как планировал (а то и вовсе переделать, на свежую голову). Впрочем, на дворе конец 2018 года, и STM32 уже стали обыденностью, так информации сейчас куча, и вы можете найти что-нибудь посвежее :)
burjui
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.