CraftDuino v2.0
  • - это CraftDuino - наш вариант полностью Arduino-совместимой платы.
  • CraftDuino - настоящий конструктор, для очень быстрого прототипирования и реализации идей.
  • Любая возможность автоматизировать что-то с лёгкостью реализуется с CraftDuino!
Просто добавьте CraftDuino!

STM32: Урок 2 - Quickstart

В прошлой статье мы настроили 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 вдоль и поперёк.
  • +7
  • 15 декабря 2011, 12:34
  • burjui

Комментарии (28)

RSS свернуть / развернуть
+
+2
Чудесная статья! Большое спасибо автору!
avatar

router32

  • 15 декабря 2011, 13:00
+
0
На мой взгляд если эту платформу «обрастить» нормальными библиотеками, автоматизирующими процесс заполнения этой тучи полей с названиями данными какими-то злыми Си-шниками, то может быть её можно будет пользоваться, а так на фоне ардуины смотрится очень паршиво всякий бред типа:
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);
А вообще я где-то видел платки ARM с ардуино-загрузщиком, поэтому поклонникам этого типа контроллера можно писать с Arduino IDE. Касаемо статьи в целом, очень информативно — респект автору.
avatar

execom

  • 15 декабря 2011, 13:55
+
0
libmaple — ребята пытаются прикрутить ардуино среду и wiring (или wirish ???) фрэймворк, к своей плате на stm32 leaflabs.com
avatar

hexanaft

  • 15 декабря 2011, 14:15
+
0
ну в общем-то кто хотел тот уже прикрутил ардуино и к PIC-контроллерам и официальный разработчик ардуины выпустил на ARM свою плату DUE Таким образом я не вижу смысла тратить силы на изучение менее удобных платформ, в то время когда лучше изучать и развивать более перспективную ардуину))
avatar

execom

  • 15 декабря 2011, 16:32
+
0
Ну если быть точным, то всё же не к PIC контроллерам, а к MIPS32. ;)
avatar

hexanaft

  • 15 декабря 2011, 16:38
+
+2
Практика показывает что всё упирается в привычку ;)
avatar

SinauRus

  • 15 декабря 2011, 17:35
+
0
Wirish — это что-то среднее между проводом (wire) и ирландцем (Irish). Ирландский электрик (:
avatar

burjui

  • 15 декабря 2011, 19:41
+
0
Дабы не быть голословным leaflabs.com/docs/libmaple.html
и github.com/leaflabs/libmaple/tree/master/wirish
Так уж назвали =)
avatar

hexanaft

  • 15 декабря 2011, 20:28
+
0
Я понял, в чем причина отсутствия у меня желания изучать программирование ARM.
А также под андроид. Все дело в названиях функций и переменных, в стиле
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

Помимо андроида и ARM, подобный подход кажется применяется на айфоне, при программировании интерфейсов в Windows.
Префикс_ИмяКИЛОМетровойФункцииСПостояннымиСменамиРЕГИСТРАиИспользованиюВсяческих_итп();

Дополняя разговоры выше о плате Mapple от LeafLabs, прикупил ее на днях, будем изучать :)
avatar

oleamm

  • 17 декабря 2011, 16:24
комментарий был удален

комментарий был удален

комментарий был удален

комментарий был удален

+
0
А почему линия А0 (к которой подключена кнопка) настроена как плавающий вход(GPIO_Mode_IN_FLOATING)??? Всегда использовал внутреннюю подтяжку к питанию. Соответственно когда кнопка не нажата, то на выводе уровень логической единицы, если нажали кнопку — на выводе контроллера логический ноль. Так же можно здесь сделать???
avatar

rikzi_em

  • 18 января 2012, 20:45
+
0
Можно, но если заглянуть в STM32VLDiscovery User manual, то там вы найдёте принципиальную схему платы, на которой видно, что пользовательская кнопка уже прижата к земле через резистор R21 (10 K). R22 (0) — это просто перемычка, если что. Пожалуй, не лишним будет упомянуть это в статье.

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

burjui

  • 19 января 2012, 12:19
+
0
Спасибо за ответ. За знаки вопроса извините — дурная привычка :).
В одной из статей вы написали
будем всё делать через библиотеку SPL — с регистрами возится на данном этапе не резон
Чем обосновывается ваш выбор работы с библиотекой периферии, а не напрямую через регистры?
avatar

rikzi_em

  • 19 января 2012, 16:18
+
0
Тем, что названия функций, констант и структур из SPL куда понятнее, чем имена регистров вроде BSRR и т.п. — код становится проще читать. Буду кодить через регистры только когда упрусь в производительность или если через них окажется проще (бывает и такое).
avatar

burjui

  • 19 января 2012, 16:41
+
0
А вот если я пойду на работу и буду кодить с использованием библиотеки, какое ко мне будет отношение со стороны начальства и коллег? Мне могут сказать: «Эх студент! И чего там тебя в универе учили? Так не пойдет, переделывай!»?..
Просто я еще студент и нигде не работал вот и интересуюсь.
avatar

rikzi_em

  • 19 января 2012, 17:08
+
0
Зависит от того, к кому попадёте. В нормальных конторах велосипедостроительство — моветон, так как использование библиотек — залог более быстрого написания кода.

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

burjui

  • 19 января 2012, 17:32
+
+1
Прикрутил примеры к HY-MINI. Эта плата очень похожа на дискавери, но пришлось разобраться с раскладкой входов-выходов :)
Плата хороша тем, что в комплекте идет цветной дисплей, CD c кучей примеров, Keil, ULINK2.
avatar

dongrigorio

  • 28 января 2012, 18:05
+
0
Не могу понять в чём проблема.
Собираю для stm32f4
Собирается всё нормально. но не работает.

Помогите разобраться)
ifolder.ru/30015580
это мой проект
avatar

lamazavr

  • 19 апреля 2012, 11:09
+
0
Проблема в том, что вы невнимательно смотрели заголовочный файл и автодополнение в Eclipse, а также оставили часть полей неинициализированным. Вот так делать нельзя:

GPIO_InitTypeDef gpio;
//gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Mode = GPIO_Mode_OUT;
gpio.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13;
gpio.GPIO_Speed = GPIO_Speed_2MHz;

/* Инициализируем GPIO на порту C */
GPIO_Init(GPIOD, &gpio);
Вы объявили экземпляр структуры 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():

XXXStruct x;
memset(&x, 0, sizeof(x));
avatar

burjui

  • 19 апреля 2012, 12:11
+
0
Исправил:

#include <stm32f4xx.h>
#include <stm32f4xx_rcc.h>
#include <stm32f4xx_gpio.h>

int main()
{
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
  GPIO_InitTypeDef gpio;
  GPIO_StructInit(&gpio);

  GPIO_Init(GPIOD, &gpio);

  GPIO_SetBits(GPIOD, GPIO_Pin_12 | GPIO_Pin_13);

  do __NOP(); while (1);
}


Никакой разници. Диоды не загорелись

avatar

lamazavr

  • 19 апреля 2012, 12:47
+
0
Видимо, в код функций вы таки не заглядывали (чукча не читатель, чукча — писатель), и зачем-то убрали код, который конфигурирует пины. Надо так:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);

GPIO_InitTypeDef gpio;
GPIO_StructInit(&gpio);
gpio.GPIO_Mode = GPIO_Mode_OUT;
gpio.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13;
GPIO_Init(GPIOD, &gpio);
И всё-таки, читайте код функций:
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct)
{
  /* Reset GPIO init structure parameters values */
  GPIO_InitStruct->GPIO_Pin  = GPIO_Pin_All;
  GPIO_InitStruct->GPIO_Mode = GPIO_Mode_IN;
  GPIO_InitStruct->GPIO_Speed = GPIO_Speed_2MHz;
  GPIO_InitStruct->GPIO_OType = GPIO_OType_PP;
  GPIO_InitStruct->GPIO_PuPd = GPIO_PuPd_NOPULL;
}
avatar

burjui

  • 19 апреля 2012, 12:56
+
0
и опять никаких результатов.
код смотрел))) проглядел что он как вход метиться.

если можно взгляните на настройки проекта. Видимо в них дело.
avatar

lamazavr

  • 19 апреля 2012, 13:08
+
+1
Я когда-то выкладывал свой рабочий проект для STM32F4DISCOVERY — вот он. Сравнивайте сами (:
avatar

burjui

  • 19 апреля 2012, 13:11
+
0
спасибо. разница в ld скприпте. с вашим всё работает.
avatar

lamazavr

  • 19 апреля 2012, 13:28
+
0
Здравствуйте! Столкнулся со следующей проблемой при работе с 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.
Проблема заключается в следующем. Как понять что значит каждый режим (как модуль работает в данном режиме)? Не всегда понятно из названия(. Насколько я понимаю, объявленную здесь константу нужно связать с регистрами и посмотреть референс мануал. Но непонятно к какому регистру относится константа.
Заранее благодарен.
avatar

Dmitriy986

  • 12 октября 2013, 21:24

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.