В прошлой статье мы настроили 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-битный и очень лёгкий в использовании. А для пущей крутизны нагрузим его миганием светодиода:
#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 BUTTON = GPIO_Pin_0; // PA0 void init_button(); void init_leds(); int main() { init_button(); init_leds(); GPIO_ResetBits(GPIOC, LED1 | LED2); /* Конфигурируем таймер SysTick на срабатывание 100 раз в секунду */ SysTick_Config(SystemCoreClock / 100); do ; while (1); } /* Обработчик прерывания по переполнению таймера SysTick */ void SysTick_Handler() { /* Обработка кнопки */ static uint8_t btn_old_state = 0; uint8_t btn_state = GPIO_ReadInputDataBit(GPIOA, BUTTON); if (btn_old_state == 0 && btn_state == 1) GPIO_WriteBit(GPIOC, LED1, !GPIO_ReadOutputDataBit(GPIOC, LED1)); btn_old_state = btn_state; /* Мигание светодиодом */ static uint8_t counter = 0; if (counter == 0) { GPIO_WriteBit(GPIOC, LED2, !GPIO_ReadOutputDataBit(GPIOC, LED2)); counter = 10; } else --counter; } void init_button() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 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 = LED1 | LED2; gpio.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOC, &gpio); }
Здесь мы настраиваем таймер так, чтобы он срабатывал каждые SystemCoreClock/100 тиков. Костанта SystemCoreClock в случае с STM32100RBT6B равна 24000000 (24 МГц) — это максимальная его частота при работе от кварца. Тут нужно учитывать, что таймер SysTick 24-битный, и значения больше 16777216 он использовать не может, но это легко обойти, используя свою переменную-счётчик в обработчике прерывания. В этом примере я использовал счётчик для переключения светодиода 10 раз в секунду. По-хорошему, обработку прерывания лучше делать в главном цикле, а в прерывании выставлять какой-нибудь флаг. Прерывания и правильную работу с ними мы тоже, со временем, рассмотрим под всеми углами.
Теперь вы получили представление о том, как работать с SPL, и готовы к более серьёзным вещам — в следующей статье мы изучим GPIO вдоль и поперёк.
30 комментариев на «“STM32: Урок 2 — Quickstart”»
Чудесная статья! Большое спасибо автору!
А почему линия А0 (к которой подключена кнопка) настроена как плавающий вход(GPIO_Mode_IN_FLOATING)??? Всегда использовал внутреннюю подтяжку к питанию. Соответственно когда кнопка не нажата, то на выводе уровень логической единицы, если нажали кнопку — на выводе контроллера логический ноль. Так же можно здесь сделать???
Можно, но если заглянуть вSTM32VLDiscovery User manual , то там вы найдёте принципиальную схему платы, на которой видно, что пользовательская кнопка уже прижата к земле через резистор R21 (10 K). R22 (0) — это просто перемычка, если что. Пожалуй, не лишним будет упомянуть это в статье.
Кстати, одного вопросительного знака мне вполне достаточно, чтобы заметить вопросительную интонацию предложения (:
Спасибо за ответ. За знаки вопроса извините — дурная привычка :).
В одной из статей вы написали
Чем обосновывается ваш выбор работы с библиотекой периферии, а не напрямую через регистры?
Тем, что названия функций, констант и структур из SPL куда понятнее, чем имена регистров вроде BSRR и т.п. — код становится проще читать. Буду кодить через регистры только когда упрусь в производительность или если через них окажется проще (бывает и такое).
А вот если я пойду на работу и буду кодить с использованием библиотеки, какое ко мне будет отношение со стороны начальства и коллег? Мне могут сказать: «Эх студент! И чего там тебя в универе учили? Так не пойдет, переделывай!»?..
Просто я еще студент и нигде не работал вот и интересуюсь.
Зависит от того, к кому попадёте. В нормальных конторах велосипедостроительство — моветон, так как использование библиотек — залог более быстрого написания кода.
А вообще, использование библиотеки не освобождает от знания регистров, а всего-навсего упрощает популярные способы работы с периферией и пр. Ковырять биты в регистрах лучше тогда, когда освоишься с кристаллом, я так считаю.
Прикрутил примеры к HY-MINI. Эта плата очень похожа на дискавери, но пришлось разобраться с раскладкой входов-выходов 🙂
Плата хороша тем, что в комплекте идет цветной дисплей, CD c кучей примеров, Keil, ULINK2.
Не могу понять в чём проблема.
Собираю для stm32f4
Собирается всё нормально. но не работает.
Помогите разобраться)
ifolder.ru/30015580
это мой проект
Проблема в том, что вы невнимательно смотрели заголовочный файл и автодополнение в Eclipse, а также оставили часть полей неинициализированным. Вот так делать нельзя:
Вы объявили экземпляр структуры GPIO_InitTypeDef, но заполнили всего 3 поля из 5, забыв про GPIO_PuPd и GPIO_OType, так что в них может быть что угодно, что там было на стеке до выделения памяти под структуру — от длины ноги Обамы до веса какашки динозавра. Поэтому инженеры ST написали функции XXX_StructInit(), которые заполняют соответствующую структуру для периферии XXX некоторыми безопасными значениями по умолчанию. Приучайтесь писать после каждого
также
И далее в том же духе при работе с другой периферией: USART_StructInit(), SPI_StructInit() и т.п. Полезно заглядывать в код этих функций, чтобы знать, какие значения присваиваются полям структуры по умолчанию — прочитать всплывающую подсказку по функции, например (в ней показывается код функции).
Ну и универсальное правило любого сишника: всегда инициализируй переменные нулём или другим безопасным значением. Если нет готовой функции XXX_StructInit(), всегда можно забить нулями через memset():
Исправил:
Никакой разници. Диоды не загорелись
Видимо, в код функций вы таки не заглядывали (чукча не читатель, чукча — писатель), и зачем-то убрали код, который конфигурирует пины. Надо так:
И всё-таки, читайте код функций:
и опять никаких результатов.
код смотрел))) проглядел что он как вход метиться.
если можно взгляните на настройки проекта. Видимо в них дело.
Я когда-то выкладывал свой рабочий проект для STM32F4DISCOVERY —вот он . Сравнивайте сами (:
спасибо. разница в ld скприпте. с вашим всё работает.
Здравствуйте! Столкнулся со следующей проблемой при работе с 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.
Проблема заключается в следующем. Как понять что значит каждый режим (как модуль работает в данном режиме)? Не всегда понятно из названия(. Насколько я понимаю, объявленную здесь константу нужно связать с регистрами и посмотреть референс мануал. Но непонятно к какому регистру относится константа.
Заранее благодарен.
На мой взгляд если эту платформу «обрастить» нормальными библиотеками, автоматизирующими процесс заполнения этой тучи полей с названиями данными какими-то злыми Си-шниками, то может быть её можно будет пользоваться, а так на фоне ардуины смотрится очень паршиво всякий бред типа:
А вообще я где-то видел платки ARM с ардуино-загрузщиком, поэтому поклонникам этого типа контроллера можно писать с Arduino IDE. Касаемо статьи в целом, очень информативно — респект автору.
libmaple — ребята пытаются прикрутить ардуино среду и wiring (или wirish ???) фрэймворк, к своей плате на stm32leaflabs.com
ну в общем-то кто хотел тот уже прикрутил ардуино и кPIC-контроллерам и официальный разработчик ардуины выпустил на ARM свою плату DUE Таким образом я не вижу смысла тратить силы на изучение менее удобных платформ, в то время когда лучше изучать и развивать более перспективную ардуину))
Ну если быть точным, то всё же не кPIC контроллерам, а к MIPS32 . 😉
Практика показывает что всё упирается в привычку 😉
Wirish — это что-то среднее между проводом (wire) и ирландцем (Irish). Ирландский электрик (:
Дабы не быть голословнымleaflabs.com/docs/libmaple.html github.com/leaflabs/libmaple/tree/master/wirish
и
Так уж назвали =)
Я понял, в чем причина отсутствия у меня желания изучать программирование ARM.
А также под андроид. Все дело в названиях функций и переменных, в стиле
Помимо андроида и ARM, подобный подход кажется применяется на айфоне, при программировании интерфейсов в Windows.
Дополняя разговоры выше о плате Mapple от LeafLabs, прикупил ее на днях, будем изучать 🙂
Вот же развелось лентяев, я фигею. Вы с execom — прямо два сапога пара: лень поставить IDE или просто редактор с автодополнением кода, лень настраивать периферию через короткие имена регистров, лень писать больше двух строк кода для настройки GPIO. Как вы двое программируете вообще? У вас же не хватит терпения что-то отлаживать, да и вообще разрабатывать мало-мальски серьёзный софт, где кода легко могут быть сотни и тысячи строк.
Прежде чем жаловаться на длинные имена, сначала ознакомьтесь с архитектурой STM32, а потом попробуйте придумать короткое имя для функции для управления тактированием периферии на шине APB2, да так, чтобы с первого взгляда на это имя было понятно, зачем она нужна.
Для всех тентяев привожу квинтессенцию моего негодования:
Лень читать парший код… — да… лень писать паршивый код -да…
ни чего лучше делфи пока не придумали по этому вам так же советую не выпендриаться)))
касаемо объемов… в последней моей публичной программе исходник имеет 45тысяч строк и я его знаю от и до и проблем в его понимании у тех кто ковырял не было… просто есть кривые люди любящие имена типо
а есть задроты которые им потакают… вот и все…
1 Используя Delphi можно легко создать и 100 тысяч строк, не заметишь.
2 Нравиться Delphi, пишите с его помощью ТОЛЬКО ДЕСКТОПНЫЕ программы под Шиндоус.
3 Если у вас нет способности понимать непривычный код, не понимайте, видимо вам не нужно.
4 Есть профессионалы, а есть низко-квалифицированные специалисты — в чём различие и так понятно. Задроты тоже есть.
5 На Delphi прогресс не остановился. Повышайте квалификацию.
Да ну. Это девайс для самых маленьких.
Добрый день!
Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны!
Хотел узнать зачем использовать переменную типа uint8_t для хранения одного бита состояния кнопки:
Я воспользовался переменной bool для хранения состояния кнопки:
Есть ли преимущества в экономии памяти и времени обработки такого кода?
Спасибо!
Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях сгенерирует одинаковый код. Можете проверить дизассемблером, входящим в тулчейн:
Кстати, ваш сниппет можно переписать компактнее, без if:
Что касается курса, спасибо, рад, что он оказался полезен. Жаль, не было возможности его доделать, как планировал (а то и вовсе переделать, на свежую голову). Впрочем, на дворе конец 2018 года, и STM32 уже стали обыденностью, так информации сейчас куча, и вы можете найти что-нибудь посвежее 🙂