Примеры программ для stm32

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

В качестве среды разработки в статье будет использоваться Eclipse CDT. Поскольку основное внимание будет сосредоточено вокруг программного кода, то вы можете спокойно проделать все манипуляции в Code::Blocks.

Общая структура проекта для ARM микроконтроллеров описана в моей статье «Программирование AVR и ARM микроконтроллеров в Eclipse. Часть 2».

Здесь я коротко напомню, что для сборки проекта для ARM — микроконтроллеров (STM32F10x в частности) понадобится скрипт компоновщика и C-Startup файл.

Скрипт компоновщика представляет собой файл с инструкциями по размещению кода программы и данных в памяти микроконтроллера. Он может скомандовать загрузить код вашей программы в Flash -память программ или SRAM -память данных.

Для микроконтроллеров с различным объемом памяти программ и данных необходимы разные скрипты компоновки. Их можно достать у производителя микроконтроллеров — компании STMicroelectronics.

Flowcode 9 первая программа для микроконтроллера STM32


Распакуйте из архива ARM_Toolchain/Lib/stm32f10x_stdperiph_lib.zip библиотеку STM32F10x standard peripheral library.
В ней имеются примеры проектов для различных сред разработки ( IAR EWB, Keil uVision, Atollic True Studio и т.д). Наиболее близким для нас является Atollic True Studio, поскольку представляет собой модификацию Eclipse.
Зайдите в каталог Project/StdPeriph_Template/TrueSTUDIO, там есть несколько подкаталогов, названия которых соответствуют названиям отладочных плат STM3210x-EVAL.

Узнайте, в какой из этих плат используется микроконтроллер той же линейки, что и ваш. Скопируйте файл stm32_flash.ld из соответствующего каталога в свой проект.

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

Стартовый код (C-Startup) для микроконтроллеров STM32 может быть написан на С или Assembler.
Хотя библиотеку STM32F10x Standard Peripheral Library (далее по тексту используется сокращение STM32F10x SPL) часто критикуют за наличие ошибок, все же для начала программирования под STM32 использование этой библиотеки предоставляет самый простой способ быстро приступить к работе.
Но всегда хочется, чтобы была какая-то альтернатива. На самом деле их множество, например, программировать на языке ассемблера .

Это самый тяжелый и бессмысленный путь. Второй способ — использовать библиотеку CMSIS, которая предоставляет синтаксис обращения к структурам языка С для доступа к различной периферии микроконтроллера. Самым простым и логичным способом (на мой взгляд) является использование библиотек.

Если вы категорически настроены против STM32F10x SPL, то специально для вас имеется еще одна альтернатива — библиотека libopencm3. В ней основное количество примеров сосредоточено вокруг основной серии микроконтроллеров STM32F10x , но появление примеров для других серий ( STM32F2xx/4xx) является только вопросом времени. Вы всегда можете присоединиться к проекту libopencm3 и ускорить этот процесс.

STM32 — программирование для начинающих. Пошагово. CubeMX CubeIDE

Стандарт CMSIS также является не обязательным для применения в ваших программах.
Можно обойтись и без него, потратив некоторые усилия и время для реализации HAL ( Hardware Abstraction Layer ) уровня на языке программирования С.

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

Или вам необходимо реализовать программное обеспечение на языке С для микроконтроллеров с ядром ARM9, для которого производители ориентируются на использование готовых операционных систем (Linux, QNX, Windows CE), поэтому библиотек для программирования на языке С в чистом виде или в сочетании с более легковесной RTOS производители могут не предоставлять.

К счастью производители микроконтроллеров на основе ядра Cortex-M3 предоставляют в распоряжение разработчиков большое количество библиотек кода. Это касается и микроконтроллеров STM32.
Продолжим рассмотрение библиотеки STM32F10x SPL. Рассматривать ее будем на примере stm32f10xQuickstart.
Вы можете открыть этот пример или же создать свой проект «с нуля», чтобы лучше осознать весь процесс происходящего.

Для второго случая я перечислю список необходимых шагов :

  • Создать в Eclipse новый пустой проект
  • Скопировать в проект скрипт компоновки и стартовый файл
  • Создать новый или скопировать шаблонный Makefile
  • При использовании в качестве шаблона Makefile из моего примера необходимо создать внутри проекта каталоги src, inc, bin, obj , внутри каталогов bin и obj создать подкаталоги Debug и Release.
  • Скопировать необходимые исходные и заголовочные файлы из библиотек CMSIS и STM32F10x SPL.
  • Внести необходимые изменения в секции настроек пользователя шаблонного Makefile, если он используется.
  • Создать в окне Eclipse “make target ” новые цели “Debug”, “cleanDebug”, “Release”, “cleanRelease”, “Program”.
  • Запустить на выполнение цель «Debug» и проследить за ее выполнением в окне «Console».

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

Конфигурирование STM32F10x SPL с помощью макроопределений

Для конфигурирования библиотеки используются предопределенные значения макросов, которые мы сейчас и рассмотрим.
Их можно задать внутри заголовочных файлов с помощью директивы препроцессора #define или же передать значения макроопределений через ключ -D компилятора GCC.
В своем примере я использую второй способ.
В Makefile переменная DEFINE содержит макросы, необходимые для компиляции библиотеки STM32F10x SPL.
Макроопределение STM32F10X_MD задает принадлежность используемого микроконтроллера к линейке Medium-density.
Сюда входят микроконтроллеры с объемом Flash-памяти от 64 до 128кБ .
В следующей таблице перечислены названия макросов для разных серий микроконтроллеров :

Наименование серии Макрос Описание
Low density Value line STM32F10X_LD_VL микроконтроллеры серии STM32F100xx с объемом Flash-памяти 16 — 32кБ
Low density STM32F10X_LD микроконтроллеры серии STM32F101xx, STM32F102xx, STM32F103xx
с объемом Flash-памяти 16 — 32кБ
Medium density Value line STM32F10X_MD_VL микроконтроллеры серии STM32F100xx с объемом Flash — памяти
64 — 128кБ
Medium-density STM32F10X_MD микроконтроллеры серии STM32F101xx, STM32F102xx, STM32F103xx с объемом Flash- памяти 64 — 128кБ
High density Value line STM32F10X_HD_VL микроконтроллеры серии STM32F100xx с объемом
Flash — памяти 256 — 512кБ
High density STM32F10X_HD микроконтроллеры серии STM32F101xx, STM32F103xx с объемом
Flash- памяти 256 — 512кБ
XL-density STM32F10X_XL микроконтроллеры серии STM32F101xx, STM32F103xx с объемом
Flash- памяти 512 — 1024кБ
Connectivity line STM32F10X_CL микроконтроллеры серии STM32F105xx и STM32F107xx

Для задания тактовой частоты микроконтроллера необходимо раскомментировать в файле system_stm32f10x.c макрос с необходимым значение тактовой частоты.

Источник: cxemotexnika.org

Примеры программ для stm32

Основная задача, стоящая перед программой при обмене информацией по SPI — своевременно считывать принимаемые данные (во избежание потерь приходящих фреймов) и своевременно записывать в SPI передаваемые данные (для ведущего устройства это важно с точки зрения достижения высокой скорости обмена, для ведомого несвоевременная запись грозит передачей неверного фрейма). Но прежде чем выполнять запись в SPI, обязательно следует убедиться в том, что буферный регистр для передаваемых данных свободен (установлен флаг TXE), иначе будут перезаписаны ещё не отправленные данные. Прежде чем выполнять чтение из SPI, следует дождаться установки флага RXNE, сигнализирующего о том, что данные уже получены, находятся в буферном регистре для входящих данных и готовы для считывания.

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

Оглавление
SPI в микроконтроллерах STM32. Основы
SPI в STM32. Работа в ведомом и ведущем режимах
SPI в STM32. Управление передачей данных
Регистры SPI
Использование SPI при работе с микроконтроллерами STM32F100xx
Использование SPI в STM32F100xx. Примеры

Читайте также:
Программа чтоб не видели в ВК

Примеры использования SPI

Укажем характерные особенности каждого из вариантов управления передачей данных при работе с SPI и приведём простые примеры, иллюстрирующие основные идеи по реализации каждого подхода.

  1. Программное управление передачей: данные передаются/принимаются по одному байту (или фрейму). Моменты, когда в SPI можно записывать очередной элемент данных или когда есть готовые для считывания (полученные) данные, определяются по флагам SPI, которые непрерывно контролируются в программном цикле. Способ имеет крайне простую программную реализацию, но процесс передачи сопровождается высокой бесполезной вычислительной нагрузкой на микроконтроллер. Причём, передача на низких скоростях не уменьшает эту нагрузку, а даже увеличивает (на пересылку данных требуется больше времени, большую часть которого процессор проводит в циклах контроля за флагами). Способ годится для эпизодических обменов небольшими порциями данных, но плохо подходит для интенсивного обмена информацией.
  2. Управление по прерываниям. Необязательно в процессе пересылки фрейма постоянно проверять флаги SPI; в это время можно заняться более полезной работой или уйти в состояние с пониженным потреблением энергии. А чтобы своевременно узнать об изменении состояния SPI, нужно просто разрешить прерывания при установке интересующих нас флагов. Действия по чтению полученных данных и записи очередных данных для пересылки, выполняются обработчиком прерывания. Способ более сложен с точки зрения программной реализации, но значительно более эффективен. Однако, с ростом скорости передачи, вычислительная нагрузка на микроконтроллер растёт (так как передача и приём каждого фрейма сопровождается обслуживанием этого события обработчиком прерывания, на вызов и выполнение которого затрачиваются определённые вычислительные ресурсы).
  3. Передача с использованием DMA. Процессор только лишь осуществляет конфигурирование SPI и DMA, после чего передача данных из буфера в памяти и приём данных в буфер происходят под управлением системы DMA. Вычислительная нагрузка на микроконтроллер минимальна (намного меньше, чем при передаче по прерываниям), особенно если передаются большие объёмы данных. В отличие от предыдущего варианта, передача с высокими скоростями не приводит к существенным накладным расходам. С точки зрения программной реализации, способ достаточно прост. Правда, количество каналов DMA в микроконтроллерах STM32F100xx весьма ограничено, что делает этот ресурс очень ценным. Кроме того, передача каждого блока данных требует выполнения ряда действий по подготовке DMA, что снижает общую эффективность метода при передаче данных небольшими порциями (не исключено, что в таких случаях более выгодным окажется второй или даже первый метод). Но если передаются большие объёмы данных на больших скоростях, то вариант с DMA оптимален.

Соответственно, рассмотрим следующие три примера, демонстрирующие программную реализацию каждого из описанных подходов. Во всех примерах происходит передача данных между двумя устройствами SPI одного микроконтроллера (SPI1, SPI2). Это существенно упрощает отладку. Для определённости, в каждом примере будем настраивать SPI1 как ведущее, а SPI2 — как ведомое устройство.

Пример 1. Простейший пример, SPI1 и SPI2 обмениваются данными объёмом 1 байт под программным управлением передачей.

Пример 2. Более сложный пример, решаемая задача уже ближе к задачам из реальной жизни. SPI1 и SPI2 обмениваются сообщениями произвольной длины. Устройство SPI1 находится под программным управлением передачей. SPI2 управляется по прерываниям.

Пример 3. SPI1 передаёт данные с помощью DMA. SPI2 управляется по прерываниям.

Общие замечания

Рассмотрим некоторые моменты, касающиеся всех предложенных выше примеров.

Используемое оборудование. Все приведённые примеры предполагают использование микроконтроллера STM32F100RB (запускались и проверялись на этом устройстве), однако, не должно возникнуть проблем, если заменить его на другой микроконтроллер из семейства STM32F100xx, имеющий 2 устройства SPI. Без труда примеры можно адаптировать под любой микроконтроллер STM32 благодаря высокой программной совместимости периферийных устройств в микроконтроллерах STM разных моделей.

Как уже отмечалось, для удобства работы, в примерах происходит обмен данными между двумя устройствами SPI, находящимися в одном микроконтроллере (SPI1, SPI2). Соответствующие выводы этих двух устройств должны быть соединены между собой:
SPI1_MOSI — SPI2_MOSI;
SPI1_MISO — SPI2_MISO;
SPI1_SCK — SPI2_SCK;
SPI1_NSS — SPI2_NSS.

SPI1 настраивается как ведущее устройство, а SPI2 — как ведомое.

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

Тактовый сигнал включаем для SPI1, SPI2, GPIOA (чтобы использовать выводы SPI1), GPIOB (для использования выводов SPI2). Если требуются дополнительные устройства (допустим, DMA) — то и для них.

Затем следует сконфигурировать выводы, используемые как выводы SPI. Так как договорились, что SPI1 — ведущее, SPI2 — ведомое, то во всех примерах выводы конфигурируются одинаково:
выходы (MOSI, SCK, NSS для SPI1; MISO для SPI2) — как двухтактные выходы для выполнения альтернативной функции;
входы (MISO для SPI1; MOSI, SCK, NSS для SPI2) — как входы с подтяжкой.

Поскольку SPI — интерфейс, способный работать на достаточно высоких скоростях, то и скорость переключения для выходов SPI в примерах задаётся высокой. Если высокая скорость передачи для SPI не нужна, то стоит снизить скорость переключения выходов. Это полезно для снижения энергопотребления.

Кроме того, при работе в качестве ведомого устройства возникают проблемы в некоторых режимах (а именно, когда CPOL = 0, CPHA = 0 или CPOL = 1, CPHA = 1), если ведомое устройство подключено к ведущему устройству, для выходов которого задана высокая скорость переключения. В процессе тестирования при выполнении указанных условий, наблюдались сбои в передаче данных и сбой синхронизации ведомого устройства. Поэтому, следует подумать о снижении скорости в проблемных режимах, либо использовать менее проблемные режимы (CPOL = 0, CPHA = 1 или CPOL = 1, CPHA = 0). В процессе тестирования, SPI в качестве ведущего устройства работало без проблем во всех режимах.

Входы сконфигурированы как входы с подтяжкой. Это совершенно необходимо для входа NSS ведомого устройства — подтягивая вход к высокому уровню, мы гарантируем, что устройство не будет случайно активировано наведённой на входе помехой, если вход NSS ведомого устройства остался физически неподключённым, либо подключён к выходу другого SPI устройства, которое пока не включено и его NSS выход находится в Z-состоянии.

Подтяжка также полезна для SCK-входа ведомого устройства. Сделав подтяжку к начальному уровню тактового сигнала (который задаётся битом CPOL), мы предотвращаем возможные ложные переключения в процессе конфигурирования устройств, пока SCK линия находится в Z-состоянии.

Подтяжка для входов данных необязательна, но и не помешает.

После того, как настроены выводы, следует выполнить настройку SPI. Устройства SPI в предлагаемых примерах конфигурируются сходным образом, отличия касаются лишь регистра CR2, инициализация которого зависит от того, используются ли прерывания и запросы DMA. Регистр CR1 инициализируется везде одинаково, с его помощью: для SPI1 устанавливаем бит режима ведущего устройства; задаётся скорость (в целях тестирования здесь устанавливается максимальный делитель для получения тактового сигнала SPI, соответственно скорость будет минимальной); задаём полярность и фазу тактового сигнала SPI. Для SPI2 только задаём полярность и фазу тактового сигнала SPI, все остальные биты регистра CR1 устанавливаем нулевыми.

Полярность и фаза тактового сигнала SPI могут быть заданы любыми, но для «общающихся» между собой SPI, они должны быть заданы одинаково. Чтобы было удобнее экспериментировать с разными режимами тактирования, параметры тактового сигнала определены в начале текста программы в виде констант CPOL и CPHA, которые могут иметь значения 0 или SPI_CR1_CPOL и 0 или SPI_CR1_CPHA соответственно.

На этом конфигурирование завершается, теперь можно включить устройства SPI1, SPI2 и приступить к передаче данных:

Пример 1. Программное управление передачей.

Пример 2. Управление по прерываниям.

Пример 3. Передача с помощью DMA.

Простейший пример

И ещё один пример. Во всех рассмотренных ранее программах происходила полноценная передача данных между двумя устройствами SPI. Но существует ещё более простой вариант: передача устройством SPI данных самому себе. Это возможно, если SPI настроено как ведущее устройство. Тогда, если соединить выводы MOSI и MISO между собой, то отправляя байт через SPI, должны его же и получить.

Читайте также:
Какое из устройств обеспечивает выполнение post программы

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

Пусть для работы выбран микроконтроллер STM32F100RB (подойдут и другие STM32F100xx без необходимости внесения изменений в программу и практически любые STM32, но, возможно, с внесением небольших изменений).

Будем экспериментировать с SPI1, SPI использует 4 вывода микроконтроллера:
PA4 — NSS;
PA5 — SCK;
PA6 — MISO;
PA7 — MOSI.

Из них нам, на самом деле, здесь потребуется только MOSI и MISO (которые в данном случае мы соединяем вместе), но чтобы этот простейший пример был пригоден в качестве основы для более сложных, настроим правильным образом все выводы.

Несколько пояснений к тексту программы. В начале программы можно увидеть определение двух констант: CPOL (по желанию можно определить её как 0 или SPI_CR1_CPOL) и CPHA (0 или SPI_CR1_CPHA). Задав желаемое значение, можно выбрать полярность и фазу тактового сигнала, используемые в тесте.

Далее определяется функция main, в которой и находится основной код. Функция main в процессе выполнения: включает тактовый сигнал для SPI1 и для GPIOA (GPIOA требуется, поскольку к этому порту относятся выводы SPI1); выполняет конфигурирование выводов.

Так как SPI1 будет ведущим устройством, то PA4 (SPI1_NSS), PA5 (SPI1_SCK) и PA7 (SPI1_MOSI) настраиваем как выходы (двухтактные выходы для выполнения альтернативной функции; так как SPI — высокоскоростной интерфейс, выбираем высокую скорость переключения выходов). Вывод PA6 (SPI1_MISO) — вход. Здесь выбран режим с подтяжкой, на тот случай, если вывод окажется неподключённым. Используется подтяжка к высокому уровню, тогда, если выход останется неподключённым, в процессе обмена все принимаемые биты будут единичными.

После конфигурирования выводов приступаем к конфигурированию SPI (регистры CR1, CR2). Для обычного режима работы большинство битов оставляются нулевыми. Устанавливаются в 1 биты SPI_CR1_MSTR (так как SPI1 будет ведущим), все биты в битовом поле SPI_CR1_BR управления скоростью передачи (используем максимальный делитель тактовой частоты, 256 и, соответственно — минимальную скорость — для целей отладки здесь не нужна высокая скорость). В регистре CR2 устанавливаем только бит SPI_CR2_SSOE, что означает, что вывод NSS будет выходом (обычный вариант использования вывода NSS в случае ведущего устройства).

Теперь включаем SPI1. До включения SPI, выходы находились в Z-состоянии, после включения, на них выводится соответствующий начальному состоянию уровень. Например, на NSS устанавливается низкий уровень, на SCK — уровень, определяемый значением CPOL.

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

В данном примере мы передаём только один байт данных. Передав его, переходим в цикл ожидания получения данных — выполняем пустой цикл, пока сброшен флаг RXNE. После установки флага, считываем полученные данные из регистра DR. Если всё прошло успешно, полученный байт в нашем примере должен совпасть с отправленным.

Если мы не собираемся больше работать с SPI, устройство может быть отключено. Для используемого здесь режима полнодуплексной связи, руководство требует перед отключением SPI дождаться сначала установки флага TXE, а затем — сброса флага BSY.

Далее приведён полный текст программы:

/* File: main.cpp Простейший пример с использованием SPI: передача байта самому себе. MCU: STM32F100RB SPI1: PA4 — NSS; PA5 — SCK; PA6 — MISO; PA7 — MOSI.

Для теста MISO и MOSI соединяем вместе. */ /** * IMPORTANT NOTE! * The symbol VECT_TAB_SRAM needs to be defined when building the project * if code has been located to RAM and interrupts are used. */ #include «stm32f10x.h» // Указываем полярность и фазу тактового сигнала SPI. const uint32_t CPOL=SPI_CR1_CPOL*0; const uint32_t CPHA=SPI_CR1_CPHA*0; int main(void) < // Включаем тактирование используемых устройств: // SPI1 и порт ввода-вывода GPIOA (SPI1 использует выводы PA4..PA7). RCC->APB2ENR|= RCC_APB2ENR_SPI1EN| RCC_APB2ENR_IOPAEN; // Конфигурирование выводов SPI1. // PA4, SPI1_NSS: alt. out, push-pull, high speed // PA5, SPI1_SCK: alt. out, push-pull, high speed // PA6, SPI1_MISO: input, pull up/down // PA7, SPI1_MOSI: alt. out, push-pull, high speed GPIOA->CRL= GPIOA->CRL // Настраиваем подтяжку входа PA6 (SPI1_MISO) к высокому уровню // (использование подтяжки необязательно).

GPIOA->BSRR=GPIO_BSRR_BS6; // Конфигурируем SPI1 (обычный ведущий режим в данном случае). // BIDIMODE: 0 (выбор режима с одной линией данных — отключено); // BIDIOE: 0 (направление передачи, бит используется при BIDIMODE=1); // CRCEN: 0 (аппаратный подсчёт CRC отключён); // CRCNEXT:0 (отправка CRC, используется при CRCEN=1; // DFF: 0 (длина фрейма данных, здесь — 8-битовый фрейм); // RXONLY: 0 (включение режима «только приём», здесь — полнодуплекс); // SSM: 0 (включение режима программного управления сигналом NSS); // SSI: 0 (значение бита используется вместо сигнала NSS при SSM=1); // LSBFIRST: 0 (порядок передачи битов, здесь — первым передаётся старший); // SPE: 0 (бит включения SPI, здесь разделяем конфигурирование и включение); // BR[2:0] (управление скоростью передачи, только для ведущего; // здесь задаём 0x7, что соотв. макс. делителю /256 и мин. скорости); // MSTR: 1 (бит переключения в ведущий режим). SPI1->CR1= // Большинство битов задаём нулевыми.

SPI_CR1_MSTR| // Ведущий режим. SPI_CR1_BR| // BR[2:0]=0x7 — минимальная скорость для теста. CPOL| // Полярность тактового сигнала. CPHA; // Фаза тактового сигнала. // С помощью регистра CR2 настраиваем генерацию запросов на // прерывание и DMA (если нужно); с помощью бита SSOE запрещаем или // разрешаем использовать ведущему устройству вывод NSS как выход. SPI1->CR2 // Сбрасываем все значимые биты регистра.

SPI1->CR2|=SPI_CR2_SSOE;// NSS будет выходом. // Включаем SPI1. // Передача не начнётся, пока не запишем что-то в его регистр, // данных, но установится состояние выходов. SPI1->CR1|=SPI_CR1_SPE; char d_out=0x5A; // Передаваемый байт. char d_in=0; // Сюда запишем полученный байт. // Ждём готовности буфера для передаваемых данных (собственно, // он готов сразу после включения SPI) и пишем передаваемые данные. while(!(SPI1->SR> SPI1->DR=d_out; // Ждём получения данных, читаем их. while(!(SPI1->SR> d_in=SPI1->DR; // Можем проверить правильность полученных данных, здесь должно быть // d_in==d_out; // Если больше не планируется передавать данные по SPI, // он может быть отключён. while(!(SPI1->SR> while(SPI1->SR> SPI1->CR1 /* Infinite loop */ while (true) <> >

Источник: www.rotr.info

STM32 с нуля. FreeRTOS. Типы многозадачности, пример программы.

Как и обещал, сейчас попробуем реализовать что-нибудь посложнее мигания диодами на базе FreeRTOS. Но сначала немного теории, которая нам понадобится для понимания сути работы ОСРВ.

Помните, мы говорили о многозадачности операционных систем реального времени? Так вот, существуют три разных типа многозадачности. Первый из них мы использовали в предыдущей статье – это вытесняющая многозадачность. Что же это такое и кого она вытесняет?

Этот тип многозадачности означает, что готовая задача с высоким приоритетом перекрывает, а точнее вытесняет задачу с более низким. Время при вытесняющей многозадачности делится на равные промежутки – кванты, и вызов планировщика происходит по истечению кванта времени. Например, по умолчанию квант времени равен 1 мс, значит, планировщик будет вызываться каждую миллисекунду и передавать управление той или иной задаче (в зависимости от приоритета и готовности задачи). Соответственно, в настройках можно задать другое значение кванта времени. Фиксируем эти основные принципы вытесняющей многозадачности, на очереди следующий тип – многозадачность кооперативная.

Читайте также:
Задачи программы эффективная школа

Здесь уже планировщик не может вклиниться в выполнение задачи. Каждая задача должна сама передавать ему управление. То есть в конце кода задачи мы должны явно вызвать планировщик при помощи функции taskYIELD().

И, наконец, третий тип многозадачности – гибридная. Ну, тут по названию уже понятно, что она объединяет предыдущие два типа. Планировщик вызывается каждый квант времени (привет от вытесняющей многозадачности), но программист также может вызвать его принудительно, как в кооперативной многозадачности.

Вот так кратко и надеюсь понятно получилось ) Типы многозадачности рассмотрели, идем дальше.

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

Пусть есть глобальная переменная glVariable = 100. Задача 1 делает следующее:

glVariable = 0; delay(100); glVariable = 100;

То есть обнуляет переменную, немного ожидает и снова делает ее равной 100. Казалось бы, все хорошо, но что будет, если планировщик отдаст управление Задаче 2 в тот момент, когда Задача 1 еще не восстановила значение глобальной переменной. Получается, что задача 2 вместо значения 100 получила значение 0, а дальше уже последствия могут быть непредсказуемыми.

Чтобы вы оценили масштаб трагедии, вот еще пример ) На столе стоит стакан воды, вдруг бодренько заходит Задача 1 и выпивает его. Прежде чем заполнить его снова она решает посидеть, подождать. А в этот момент заходит Задача 2, берет стакан, а он пуст. Неприятно. Вот поэтому и применяется механизм очередей.

Очередь представляет собой набор элементов определенного размера. В качестве элемента может выступать любая переменная C (char, int и т. д.). При записи элемента в очередь создается его побайтовая копия. Аналогично и при чтении. Очередь в ОСРВ, как и в жизни, базируется на принципе «первым вошел — первым вышел».

То есть последний записанный в очередь элемент последним будет и прочитан. Все справедливо.

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

Создается очередь функцией:

xQueueHandle xQueueCreate (unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxIemSize);

Функция принимает аргументы:

  • uxQueueLength – размер очереди
  • uxItemSize – размер элемента очереди

и возвращает дескриптор очереди:

  • Null – если очередь не создана
  • Не Null – если очередь создана

При удачном создании очереди возвращаемое значение должно быть сохранено в переменной типа xQueueHandle. Создали, а дальше то что?

portBASE_TYPE xQueueSend(xQueueHandle xQueue, const void * pvIiemToQueue, portTickType xTicksToWait);

Функция записи в очередь и ее аргументы:

  • xQueue – дескриптор очереди
  • pvItemToQueue – указатель на элемент, который будет помещен в очередь
  • xTicksToWait — максимальное количество квантов времени, в течение которого задача может пребывать в блокированном состоянии, если очередь полна, и записать новый элемент невозможно

Если что-то непонятно — не страшно — в примере все протестируем наглядно. А теперь функция чтения из очереди:

portBASE_TYPE xQueueReceive(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait);

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

  • pdPASS – успешное завершение
  • errQUEUE_EMPTY – провал

Вот в принципе и все, что касается теории.

Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там — STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также статья на смежную тему из нового курса: STM32CubeMx. Быстрый старт с FreeRTOS для STM32.

Попробуем написать программу, в которой максимально используем то, о чем сейчас узнали. Создадим две задачи, одна будет запускать преобразование АЦП и сравнивать результат с порогом. Если напряжение на входе больше порога, задача кладет в очередь посылку «ОК», а если порог не превышен – посылку «NO». Вторая задача будет читать данные из очереди и отправлять их по USART.

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

Понадобится переменная для обращения к нашей очереди, ну и еще пара переменных:

GPIO_InitTypeDef port; USART_InitTypeDef usart; ADC_InitTypeDef adc; xQueueHandle xDataQueue; uint8_t usartData[2]; uint8_t sendData[2]; uint8_t usartCounter; uint16_t data;

Далее включаем тактирование всего, что нам понадобится:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

Инициализируем ножки, которые будут нашими Rx и Tx:

GPIO_StructInit( port.GPIO_Mode = GPIO_Mode_AF_PP; port.GPIO_Pin = GPIO_Pin_9; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, port.GPIO_Mode = GPIO_Mode_AF_PP; port.GPIO_Pin = GPIO_Pin_10; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA,

Также нам понадобится первый канал модуля ADC1:

port.GPIO_Mode = GPIO_Mode_AF_PP; port.GPIO_Pin = GPIO_Pin_0; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA,

Настраиваем режим работы АЦП:

ADC_StructInit( // Нам нужен непрерывный режим adc.ADC_ContinuousConvMode = ENABLE; // Режим запуска – установка бита SWSTART adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_Init(ADC1,

Не забываем настроить еще и USART:

USART_StructInit( usart.USART_BaudRate = BAUDRATE; USART_Init(USART1,

BAUDRATE объявлена у меня ранее:

#define BAUDRATE 9600

Пришло время написать код для двух задач – напоминаю, одна работает с АЦП, вторая с USART:

void vADCTask (void *pvParameters) < while(1) < // АЦП у нас фигачит непрерывно и постоянно, так что надо только забирать данные ) data = ADC_GetConversionValue(ADC1); // Если напряжение больше порога – шлем «OK», иначе – «NO» if (data >0x9B2) < sendData[0] = ‘O’; sendData[1] = ‘K’; >else < sendData[0] = ‘N’; sendData[1] = ‘O’; >// Отправляем данные в очередь. // xDataQueue – имя очереди xQueueSend(xDataQueue, > vTaskDelete(NULL); >

Половина дела сделана! Теперь реализация второй функции:

void vUSARTTask(void *pvParameters) < while(1) < // Забираем данные из очереди и сохраняем их // в массив usartData[] xQueueReceive(xDataQueue, // Счетчик отправленных байт – в ноль usartCounter = 0; // Всего передаем два байта while(usartCounter < 2) < // Кладем данные в регистр данных USART, ждем, пока отправится, увеличиваем счетчик и шлем следующий байт USART_SendData(USART1, usartData[usartCounter]); while(!USART_GetFlagStatus(USART1, USART_FLAG_TC)); usartCounter++; >> vTaskDelete(NULL); >

Осталось связать все это:

int main() < vFreeRTOSInitAll(); // Включаем USART USART_Cmd(USART1, ENABLE); ADC_Cmd(ADC1, ENABLE); // Запускаем АЦП ADC_SoftwareStartConvCmd(ADC1, ENABLE); // Создаем очередь с дескриптором xDataQueue xDataQueue = xQueueCreate( 2, sizeof(char *)); xTaskCreate(vADCTask, (signed char*)”ADCTask”, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL); xTaskCreate(vUSARTTask, (signed char*)”USARTTask”, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL); vTaskStartScheduler(); >

Ну вот и все, давайте попробуем скомпилировать и запустить программу в отладчике. Для эмуляции сигнала на входе АЦП в командной строке отладчика напишем (об этом можно почитать тут):

ADC1_IN0 = 2.5

Сейчас на входе будет 2.5 В. Пишем разные команды в командной строке и смотрим, что получается:

В USART идут данные, меняющиеся в зависимости от значения аналогового напряжения на входе АЦП. На самом деле нам повезло, что результат получился таким. И скоро мы разберемся, в чем же проблема этой программы.

Но в традициях лучших сериалов – «об этом в следующей серии» — то есть в следующей статье )

Источник: microtechnics.ru

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru