Приветствую всех читателей моей первой статьи. Меня зовут Назаров Александр, я программист и резидент Ресурсного центра робототехники - структурного подразделения Донского государственного технического университета. Наши проекты направлены, в основном на мобильную робототехнику и его составляющие: изготовление механических узлов и их сборка, проектирование электрических схем и программирование микроконтроллеров.

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

Рендер платформы EduBot.
Рендер платформы EduBot.

Первый шаг - стандартизировать обучающие материалы - так появился "EduBots" - двухколесный робот и периферийная библиотека для обучения резидентов центра. Робот представляет собой двухколесную платформу 150х150х70 мм массой 550 грамм с двумя опорными роликами, реализован по дифференциальной схеме. В движение робот приводится двумя электромоторами с датчиками углового перемещения (энкодерами). Напряжение питания системы составляет 12,6 В и обеспечивается встроенными аккумуляторами.

Для управления платформой используется микроконтроллер STM32F401 на базе ядра Cortex-M4. Рабочая частота микроконтроллера – 84 МГц. Периферия позволяет подключать различные внешние устройства, работающие по интерфейсам USART, I2C, SPI. На платформе установлено 5 аналоговых датчиков линии, помимо этого у пользователей есть возможность подключения до 8 дополнительных аналоговых сенсоров и прочего оборудования (одноплатных компьютеров, лидаров и камер). Разработка проходила в несколько этапов, а испытания в рамках обучения новых резидентов позволяло оперативно получать информацию о недоработках и слабых местах платформы.

Важным компонентом проекта "EduBot" стала разработанная периферийная библиотека FIL (Fast Initializarion Library) для микроконтроллеров STM32. Основная задача библиотеки - облегчить процесс написания кода инициализации и отладки роботов за счет повышения уровня абстракции программного кода минимально влияющего на время обработки инструкций. Отличие библиотеки FIL от других наиболее популярных библиотек(HAL, SPL) - наличие функций и макросов, оптимизированных для выполнения различных задач робототехники: управление двигателем, обработка цифровых и аналоговых датчиков, подключение устройств по шине I2C и много других. Другой причиной создания - предотвратить "стихийную разработку" резидентов. Обучающиеся находясь на начальном этапе, изучая принципы функционирования и работы периферии микроконтроллера с базисных основополагающих шагов (работа с регистрами микроконтроллера), зачастую полагаются лишь на библиотеку CMSIS, и, как следствие, тратят много временных ресурсов. Таким образом, библиотека FIL преподносится как удобный и оптимизированный инструмент для разработчика. Но обо всем по порядку.

Архитектура проекта с библиотекой FIL можно представлена на диаграмме ниже:

Архитектура проекта с библиотекой FIL.
Архитектура проекта с библиотекой FIL.

Структура проекта содержит несколько функциональных блоков:

  • Блок проекта - к нему относятся всеми известные файлы main.c, main.h и другие файлы проекта. Непосредственно в них ведется написание логики работы робота и сценарии его поведения;

  • Блок CMSIS определений - необходим для облегчения доступа к периферии через стандартизированные идентификаторы. Данные файлы и их содержимое задействуются при работе библиотеки FIL;

  • Блок ядра библиотеки FIL - включает в себя файл-линкер для подключения файлов с API библиотеки согласно требованиям конфигурации;

  • Блок конфигурации робота - содержит, как правило, от одного до двух файлов. Первый - карта портов, она содержит определения необходимых портов микроконтроллера с указанием пользовательского идентификатора (Label). Второй файл конфигурирования необходим для указания требований библиотеке, проще говоря, какие API необходимы в данном проекте.

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

Создание проекта и добавление библиотеки

В настоящее время библиотека поддерживается средой программирования EmBitz версии от 2.30. Подробная инструкция приведена в официальном репозитории библиотеки FIL. Для этого потребуется обзавестись на вашем персональном компьютере следующим программным обеспечением:

  • Система контроля версий Git - потребуется для скачивания библиотеки с репозиториев (ссылка на установщик);

  • Среда разработки EmBitz 2.30 - наша основная программа, где будет демонстрироваться работоспособность библиотеки. К сожалению, доступ к основному сайту разработка был закрыт для жителей КНР и Российской Федерации, поэтому я прикрепил ссылку на свою копию установщика. Также, вместе со средой будет автоматически установлен драйвер ST-Link.

  • USB драйвер для программатора ST-Link - необходим только в том случае, если у вас выскакивает ошибка при прошивке контроллера, установка этого драйвера обычно помогает. Способов его установки существует множество, я, в рамках обзора, воспользуюсь удобной для меня программой Zadig (ссылка на установщик).

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

Переход к установке для среды EmBitz.
Переход к установке для среды EmBitz.

В начале предлагают создать новый проект. Я буду следовать всем шагам установки, в том числе с отключением других периферийных библиотек, оставив только CMSIS. Для этого в меню выбора модели контроллера измените опцию Peripherals Library на not used.

Убираем добавление в проект периферийных библиотек.
Убираем добавление в проект периферийных библиотек.

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

git clone https://github.com/Casonka/FIL-EmbitzDeploy.git & cd FIL-EmbitzDeploy & rmdir /q /s images & del /q README.md

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

Недостаточно просто загрузить файлы, их нужно добавить в текущий проект. Для этого нажмём ПКМ по проекту и выберем опцию Add files recursively. Через всплывающее окно проводника выбрал папку FIL-EmBitzDeploy (была загружена через Git).

Открыв проект в нашей IDE, остается добавить подключение библиотеки в заголовочном файле main.h.

Добавление директивы включения главного файла ядра библиотеки.
Добавление директивы включения главного файла ядра библиотеки.

В файле main.c добавим стандартное макро определение Board_Config, про которое было упомянуто в конце инструкции. Теперь, при нажатии кнопки build или сочетания клавиш ctrl+b будет собран проект. Результат порадовал, проект собрался без ошибок.

Окно успешного завершения сборки проекта.
Окно успешного завершения сборки проекта.

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

Демонстрация работы библиотеки на примерах

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

Для демонстрации работы некоторых примеров было использовано окно для отслеживания переменных, которое можно вывести себе через контекстное меню выше - Debug > Debugging Window > Watches.

Зная структуру библиотеки, о которой было оговорено ранее, заглянем в содержимое нашей конфигурации. Согласно инструкции, по умолчанию, работает демо конфигурация, поэтому заглянем в файлы по пути FIL-EmBitzDeploy/conf/demo/.

Здесь мы видим два файла: файл карты портов и файл настроек демо-конфигурации. В карте портов содержится макрос порта светодиода - LED_PIN(расположен на верхней плате робота). Запомним название, оно нам пригодятся далее. Структура файла приведена ниже, содержит всего одну строку.

#define LED_PIN                 GPIOPinID(PORTC, 13)

В файле настроек видим общий список параметров.

/*! General settings */ 
#define __configUSE_RCC                   1 
#define __configUSE_GPIO                  1 
#define __configUSE_TIM                   1 
#define __configUSE_USART                 0 
#define __configUSE_DMA                   0 
#define __configUSE_I2C                   0 
#define __configUSE_SPI                   0 
#define __configUSE_ADC                   0 
#define __configUSE_EXTI                  0 
#define __configUSE_RTC                   0

/*! Optional settings */ 
#define __configCALC_RCC                  1 
#define __configCALC_TIM                  1 
#define __configCALC_USART                0 
#define __configCALC_I2C                  0

Каждый из параметров отвечает за получения доступа в пользовательское пространство какого-либо участка периферии. Ключевая приставка USE относит группу параметров к главным, поскольку их изменение влияет на включение/исключение периферийных файлов. Например, параметр __configUSE_GPIO выставленный в значение 1 приведет к подключению файла GPIO.h библиотеки и откроет доступ к командам для работы непосредственно с портами ввода/вывода.

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

Ниже, в том же файле, находим макрос Board_Config - основная команда применения настроек.

#define Board_Config { \
    SetGPIOC;\
    InitPeriph;\
    SysTick_Config(84000);\
    }

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

  • SetGPIOC - подает тактирование на группу портов С. Акцентирую ваше внимание на то, что данная команда не будет работать, если параметр сектора тактирования __configUSE_RCC (см. выше) не включен (значение не 1);

  • SysTick_Config(84000) - команда инициализации системного таймера. Числом задается делитель тактовой частоты контроллера. В данном случае системный таймер будет настроен на частоту 1кГц. Не относится к API библиотеки, её определение можно найти в файлах CMSIS библиотеки. Вызов данной команды необходим для работы команд, отсчитывающих интервалы времени при выполнении, например, функция задержки delay_ms();

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

#define InitPeriph {\
  GPIOConfPin(LED_PIN, GENERAL, PUSH_PULL, FAST_S, NO_PULL_UP);\
  }

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

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

  • GENERAL - работа порта в режиме выхода. Согласно документации кроме данного режима можно задать ещё три: INPUT - режим входа, ALTERNATE - альтернативный режим, ANALOG - аналоговый режим;

  • PUSH_PULL - стандартная схемотехника подключение порта. Если необходимо сменить, есть второй режим - OPEN_DRAIN;

  • FAST_S - скорость работы порта 50 МГц. Помимо этой опции возможно задать ещё несколько: LOW_S (2 МГц), MEDIUM_S (25 МГц), HIGH_S (100 МГц);

  • NO_PULL_UP - не использовать никаких подтягивающих резисторов. В ином случае используются опции PULL_UP (подтяжка к питанию) и PULL_DOWN (подтяжка к земле).

Мигание светодиодом

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

int main(void)
{
Board_Config;
  while(1) 
  {
      TooglePin(LED_PIN);
      delay_ms(250);
  }
}

Для применения настроек всегда в первую очередь выполняется макрос Board_Config. Приведенный пример меняет состояние пина каждые 250 миллисекунд. Команда TooglePin() внутри себя сравнивает текущее состояние выхода порта и меняет его логический уровень на противоположный, за счет этого и достигается изменение. Задержка лишь для видимости изменений. Ниже можете наблюдать, что пример успешно загрузился и светодиод мигает на нашем роботе.

Демонстрационный пример работает.
Демонстрационный пример работает.

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

  • SetPin(pin) - устанавливает на указанном идентификаторе пина высокий потенциал (зажечь светодиод или просто высокий потенциал для проверки);

  • ResetPin(pin) - устанавливает на указанном идентификаторе пина низкий потенциал (потушить светодиод или установить низкий потенциал для отладки и других целей).

Использование таймеров

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

В этом разделе будут продемонстрированы примеры работы таймеров с помощью библиотеки FIL. Перед тем как производить манипуляции и использовать команды с таймерами необходимо убедиться, что значения параметров __configUSE_TIM и __configCALC_TIM равны 1.

Широтно-импульсная модуляция (ШИМ) с использованием FIL

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

#include "main.h"

int main(void)
{
    Board_Config;
    while(1)
    {
        SetPWM(TIM1, 1, 0.5);
    }
}

Встречаем новую командуSetPWM(). Исходя из названия, можно догадаться что её задача - генерация импульса ШИМ. На вход она требует 3 аргумента: указатель таймера, на выходе которого нужно установить сигнал (в примере указан первый таймер), номер канала таймера (в примере первый канал), скважность импульса. Принцип записи значения скважности такой - диапазон значений от -1.0 до 1.0. По факту это значение заполнения в процентах поделенной на 100. Минусовой диапазон включен для того, чтобы можно было генерировать шим обратной полярности (хотя для этого можно было использовать регистр полярности, но что имеем то имеем).

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

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

В файле с конфигурацией нужно прежде всего добавить в Board_Config некоторые команды:

  • SetGPIOA - подаем тактирование на группу портов А.

  • SetTIM1 - подаем тактирование на таймер №1. Просто и быстро;

  • TimPWMConfigure(TIM1, 8400, 10000, 1, 0, 0, 0) - однострочная команда для быстрой инициализации таймера в режиме генерации ШИМ. На вход подается уже ранее упомянутый TIM1 (идентификатор таймера №1), далее идут аргументы предделителя и периода. В документации на эту модель контроллера можно найти частоту тактирования первого таймера (84 МГц) поскольку он находится на высокоскоростной шине APB1. Результирующая частота, при указанных аргументах, получается равной 1 Гц. Формула для расчета частоты таймера будет приведена ниже, однако для новичков рекомендуем обратить своё внимание на команду TimPWMConfigureAutomatic(TIM1, 100, 1, 0, 0, 0) - облегченная команда, призванная взять на себя ответственность за расчет предделителя и периода. От вас потребуется только указать требуемую частоту в герцах (я указал 100 Гц). Последние 4 аргумента функций - включение каналов (их может быть до 4). Нам для примера достаточно задействовать только первый, поэтому его выставил в 1.

Согласно формуле, частота находится путем деления частоты шины (bus), на которой находится нужный нам таймер, на произведение предделителя (PSC) + 1 и периода (ARR).

Формула нахождения частоты таймера.
Формула нахождения частоты таймера.

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

#define LED_PIN                 GPIOPinID(PORTC, 13)

// Добавим ниже новое определение, привяжем к пину PA15

#define PWM_PIN                 GPIOPinID(PORTA, 15)

Теперь, добавим в макрос InitPeriph несколько команд:

  • GPIOConfPin(PWM_PIN, ALTERNATE, PUSH_PULL, FAST_S, NO_PULL_UP) - Инициализация пина для первого канала таймера №1, настройки почти совпадают с примером для светодиода, за исключением другого названия (мы используем другой пин), режим ALTERNATE для подготовки к подключению альтернативной функции, все остальное предпочел оставить как есть;

  • GPIOConfAF(PWM_PIN, AF1) - Подключение первого канала альтернативной функции. В нашем случае произойдет привязка пина PWM_PIN к первому каналу таймера №1 (смотри рисунок ниже). Всю информацию можно найти в документации контроллера (ссылка), в разделе pinout and pin description.

Поиск нужной альтернативной функции.
Поиск нужной альтернативной функции.

Добавив все необходимое, получаем новую версию конфигурации:

#define Board_Config {\
                    SetGPIOA;\
                    SetGPIOC;\
                    SetTIM1;\
                    InitPeriph;\
                    SysTick_Config(84000);\
                    TimPWMConfigure(TIM1,8400, 10000, 1, 0, 0, 0);\
                    }

#define InitPeriph {\
        GPIOConfPin(LED_PIN, GENERAL, PUSH_PULL, FAST_S, NO_PULL_UP);\
        GPIOConfPin(PWM_PIN, ALTERNATE, PUSH_PULL, FAST_S, NO_PULL_UP);\
        }

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

Подключение инкрементального энкодера

Таймер можно использовать для получения данных о вращении приводных колес робота. В платформе "EduBot" установлены два энкодера на входной вал двигателя, то есть без учета коэффициента передачи редуктора. Следующий пример демонстрирует работу команды инициализации таймера в режиме энкодера, входы которого можно использовать для регистрации вращения. Эти команды необходимо добавлять в Board_Config и InitPeriph, запишем как оно должно выглядеть, в случае использования обучающей платформы.

 /* [PinMap.h] */
// Добавляем идентификаторы для пинов энкодера
#define ENCODER1A_PIN           GPIOPinID(PORTA,0)
#define ENCODER1B_PIN           GPIOPinID(PORTA,1)

 /* [Configuration.h] */

/* Подаем тактирование на таймера №4 и 
   какая-то команда*/

    SetTIM4;\
    TimEncoderConfigure(TIM4);\

/* Используем уже известные нам команды, 
  скорость тактирования пинов намеренно делаем LOW_S,
  режим работы - вход, согласно таблице это вторая альтернативная функция */

    GPIOConfPin(ENCODER1A_PIN, ALTERNATE, PUSH_PULL, LOW_S, PULL_UP);      \
    GPIOConfAF(ENCODER1A_PIN, AF2);                                        \
    GPIOConfPin(ENCODER1B_PIN, ALTERNATE, PUSH_PULL, LOW_S, PULL_UP);      \
    GPIOConfAF(ENCODER1B_PIN, AF2);                                        \

 /* [main.c] */

uint16_t EncoderData;
int main(void)
{
    Board_Config;
    while(1)
    {
        EncoderData = GetTimCNT(TIM4);
    }
}

В примере встречаются почти все знакомые команды и функции, за исключением двух:

  • TimEncoderConfigure(TIM4) - инициализация таймера №4 для работы в режиме энкодера. Просто записав это макроопределение и выполнив его, система будет готова к принятию входных данных с энкодера;

  • GetTimCNT(TIM4) - макрос, который позволяет получить данные из регистра счетчика. Содержит внутри обращение к CNT с приведением к типу.

Демонстрационного примера достаточно, чтобы производить замеры общего пройденного расстояния. Для проверки, задал вращение одного приводного колеса робота (задействован предыдущий пример) и вывел переменную EncoderData в окно Watches.

Результат работы счетчика энкодера спустя некоторое время.
Результат работы счетчика энкодера спустя некоторое время.

Генерация прерываний с помощью таймеров

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

Рассмотрим новый демонстрационный пример, взяв предыдущий за основу. Единственное отличие будет заключаться в том, что считывание данных будет в обработчике прерывания. Рассмотрим пример подробнее. Команда TimPIDConfigure(TIM3, 4200, 1000) позволяет настроить таймер на генерацию прерываний в зависимости от настроенной частоты. Для этой функции есть упрощенная альтернатива TimPIDConfigureAutomatic(TIM3, 10), на вход которой вторым аргументов записывается частота в герцах. Теперь, перейдем к коду примера.

/* [Configuration.h] */

/* Инициализация таймера №3 для генерации
   прерывания */

  TimPIDConfigure(TIM3, 4200, 1000); \
  NVIC_EnableIRQ(TIM3_IRQn); \

 /* [main.c] */

uint16_t EncoderData;
void TIM3_IRQHandler(void)
{
  EncoderData = GetTimCNT(TIM4);

  ResetTIMSR(TIM3);
}

int main(void)
{
    Board_Config;
    while(1)
    {
      
    }
}

Обработчик прерывания TIM3_IRQHandler() необходимо объявить таким же по названию, каким он указан в стартовом файле startup_stm32f401xc.s, генерируемом при создании проекта. В примере демонстрируется инициализация прерывания по переполнению счетчика таймера, в его работоспособности вы можете убедиться при запуске отладки.

Взаимодействие с АЦП

Аналого-цифровой преобразователь (АЦП) используется для получения данных с аналоговых сенсоров. С учетом того, что комплект обучающей платформы включает аналоговые сенсоры Sharp 2Y0A21, то следующий пример будет актуален для начинающих. Конструкция и оснащение платформы позволяет оснастить такими датчиками в количестве до 8 единиц.

Добавим новые фрагменты кода в конфигурацию и рабочее пространство.

 /* [PinMap.h] */
// Добавляем идентификатор для аналогового входа A4

#define ADC_PIN                 GPIOPinID(PORTA,4)

/* [Configuration.h] */

/* Подаем тактирование на единственный и 
   первый АЦП */

    SetADC1; \

/* Добавляем в InitPeriph 
   инициализацию пина  как аналогового входа */
    GPIOConfPin(ADC_PIN, ANALOG, PUSH_PULL, FAST_S, NO_PULL_UP);\

 /* [main.c] */
uint16_t ADC_Data;
int main(void)
{
    Board_Config;
    ADCAddChannel(4, REGULAR, ADC_480_CYCLES);
    ADC_Init();
    while(1)
    {
      ADC_Data = GetADCData;
    }
}

В примере все выглядит понятным до главной точки входа программмы int main(void). Инициализация АЦП требует дополнительных действий от разработчика:

  • ADCAddChannel(4, REGULAR, ADC_480_CYCLES) - указанной командой мы добавляем 4 вход АЦП (пин A4) к очереди на обработку. Тип канала в данном случае - регулярный, для инжектированных каналов, согласно документации необходимо изменить второй параметр на INJECTED. третий параметр регулирует количество циклов преобразования перед выдачей окончательного результата, в примере указано 480 циклов, однако параметр можно уменьшать до 3 циклов, но результат будет соответствовать затраченному времени, нужно смотреть по ситуации;

  • ADC_Init() - общая функция инициализации АЦП и его параметров. Необходимо её выполнить после добавления каналов, во избежания того, что каналы не будут обрабатываться АЦП после старта;

  • GetADCData - получение данных с буферного регистра. Ничем не примечательна, считывает данные из регистра DR с приведением к типу uint16_t.

Данные с АЦП приходят.
Данные с АЦП приходят.

Пример актуален при наличии одного активного регулярного канала и не подходит для получения данных с нескольких сенсоров одновременно, это можно реализовать с помощью контроллера прямого доступа к памяти (DMA). Для его включения потребуется соответствующая команда на подачу тактирования SetDMA1. Однако это не все манипуляции, которые потребуется проделать. Ознакомимся с нюансами.

/* Добавляем новый параметр конфигурации */
#define __configADC_DMARequest          (1)

Данный параметр при общей инициализации АЦП через ADC_Init() подключает АЦП к контроллеру памяти, в результате мы будет спокойно получать данные с нескольких каналов в массив, который предварительно создадим.

ConnectADCTODMA(HIGH_P, ADC1_Data, 0);

С помощью этого макроса инициализируется один из каналов DMA и подключается к буферу АЦП. По своему определению, контроллер просто перебрасывает информацию из одного места в другое. Параметр HIGH_P означает высокий приоритет выполнения операций на данном канале, помимо этого существуют ещё варианты указать низкий (LOW_P), средний (MEDIUM_P) и очень высокий (VeryHigh_P) приоритеты. Второй параметр содержит ссылку на буфер куда нам будут присылаться значения с АЦП. Определения всех возможных буферов приведены в файле DMA_FIFOBuffers.h. Взглянем на фрагмент этого файла для общего понимания:

#if(FIL_DMA == 1)
/*!
*   @note [FIL:DMA] This place configuration sizes of buffers
*
*/
#define ADC1_NUMB   2

#ifdef STM32F40_41xxx
#define ADC2_NUMB   2
#define ADC3_NUMB   3
#endif /*STM32F40_41xxx*/

#define USART1RX_NUMB  8
#define USART1TX_NUMB  8
#define USART2RX_NUMB  8
#define USART2TX_NUMB  8

/*!
*   @brief  ADC1_Data[ADC_NUMB] - buffer ADC1 Conversions
*   @list  ADC1_Data
*/
extern uint16_t ADC1_Data[ADC1_NUMB];
...

Найдем определение для буфера первого АЦП. Он называется ADC1_Data (это было нетрудно). Его необходимо скопировать и добавить в качестве второго аргумента ранее указанного макроса, если вы этого ещё не сделали. Так, в остальном тут зарезервированы буферы для некоторых других интерфейсов, таких как UART и так далее.

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

  • 0 - прерывания по завершению переброса не будут действовать;

  • 0.5 - включение прерывания по переброске половины всех данных;

  • 1 - включение прерывания по завершению передачи данных;

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

/*!
*   @note [FIL:DMA] This place configuration sizes of buffers
*
*/
#define ADC1_NUMB   2

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

Действуем по той же схеме: добавляем второго идентификатор аналогового входа, подаем тактирование на второй DMA, добавляем инициализацию в файл main.c.

/* [PinMap.h] */
/* Добавляем новый идентификатор */
#define ADCnew_PIN              GPIOPinID(PORTA, 4)

/* [Configuration.h] */
/* Подача тактирования на DMA2 */
  SetDMA2;\

/* [main.c] */

uint16_t ADC1_Data[2];
int main(void)
{
  Board_Config;
  // Добавляем каналы АЦП
  ADCAddChannel(3, REGULAR, ADC_480_CYCLES);
  ADCAddChannel(8, REGULAR, ADC_480_CYCLES);
  // Не забываем про инициализацию DMA
  ConnectADCTODMA(HIGH_P, ADC1_Data, 0);
  // Общая инициализация АЦП
  ADC_Init();
  while(1)
  {
    /* Оставляем пустым */
  }
}

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

Готово, данные с нескольких каналов записываются раздельно.
Готово, данные с нескольких каналов записываются раздельно.

Итоги

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

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

Плюсы

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

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

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

Минусы

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

  2. Не вся периферия готова к использованию - рассмотрение демонстрационных примеров это по сравнению со всеми возможностями библиотеки, как капля в море, но даже с учетом этого некоторые участки периферии не были добавлены в библиотеку. К самым известным из них можно отнести SPI, DAC, CAN и другие интерфейсы. В последующих версиях список будет уменьшаться, следите за обновлениями;

  3. Что там по RTOS - несомненно, для реализации сложных и серьезных проектов в области робототехники потребуется подвязать операционную систему реального времени. Проводилось немало тестов при добавлении FreeRTOS, её можно будет скоро добавить через External Manager. Но пока её нет, проект библиотеки считаю не совсем полноценным и жизнеспособным;

  4. Некоторые жалобы на ошибки при выполнении - замечено (и не раз), как при выполнении абсолютно правильного кода происходит уход в ошибки HardFault и Default. Детальный анализ структуры кода навел на мысль, что при выполнении некоторых макросов, либо их множества приводит к ошибкам из-за слишком быстрой скорости выполнения (или может быть я ошибаюсь). Причиной этого стало уделение слишком малого количества времени на добавление всевозможных синхронизирующих барьеров и проверок.

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

Планы по дальнейшему развитию

Выложив сюда эту статью, я закончил вводный этап развития библиотеки. Впереди запланированы добавления и изменения. Следующим шагом станет поиск способов добавления библиотеки micro-ROS (ROS для микроконтроллеров), которая необходима резидентам и разработчикам для создания стандартизированных сообщений ROS msgs, их отправка и получение со стороны ЭВМ верхнего уровня. К сожалению, среда EmBitz не адаптирована к подключению такой библиотеки, поэтому первый подэтап - оптимизация кода библиотеки под работы в среде Eclipse. Задач и планов множество, буду улучшать и модифицировать.

Спасибо, что дочитал статью до конца! Мне очень приятно осознавать, что мой проект добрался до пространства Хабр и с ним ознакомилось намного больше людей чем год назад, когда проект представлял собой лишь нарезки кода и идею. Нашему центру будет очень интересно узнать ваше мнение о разработке, для себя я услышу нужную мне сейчас, как никогда, критику и/или советы по наполнению. Отдельно, буду признателен за проставленный star проекта на гитхаб.

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


  1. gleb_l
    00.00.0000 00:00
    +3

    Извините, если обертка вокруг периферии, как вы пишете, непотокобезопасная - это первый и главный ее недостаток. Использовать ее в реальных real-time проектах нельзя! Только в демо и PoC. То, что она обвязывает не всю периферию SoC - это уже вторично, третично итд. Это как коммерческая эксплуатация автобуса, у которого иногда отказывают тормоза.


    1. Caska Автор
      00.00.0000 00:00

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


  1. longtolik
    00.00.0000 00:00
    -1

    Рабочая частота микроконтроллера – 84 МГц.

    Вот когда приводят это, то что имеют в виду?
    Поясню: на STM32F103 она 72 Мгц. Я делал задержку в микросекундах, получилось, что процессор может вичитать единицк 8 000 000 раз в секунду. То есть, его быстродействие — 8 млн. операций в секунду. Еслм ARM — это RISC, то за один такт процессора должна выполняться одна инструкция.
    Получается, ядро процессора работает на частоте 8 МГц. При кварце 8 МГц.На что они дальше умеожают — это уже другое дело. На 9 умножат и говорят — частота работы 72 МГц.
    Для сравнения, AVR Atmega 328 из Arduino выполняет команды за один такт, а это — 20 МГц, но 8 бит.
    Можете сами поэкспериментировать и определить, сколько и каких команд выполнит ваш процессор в секунду.


    1. GennPen
      00.00.0000 00:00
      +1

      Поясню: на STM32F103 она 72 Мгц. Я делал задержку в микросекундах, получилось, что процессор может вичитать единицк 8 000 000 раз в секунду. То есть, его быстродействие — 8 млн. операций в секунду. Еслм ARM — это RISC, то за один такт процессора должна выполняться одна инструкция.Получается, ядро процессора работает на частоте 8 МГц. 

      Вычитание делалось из определенной переменной? Нужно как-то данные из памяти загрузить в регистр процессора, и уже потом делать вычитание. А еще наверняка это делалось в цикле, поэтому еще одна переменная, ее уменьшение, условный переход. Не все команды делаются за один такт(как и у атмеги). Вот тут и набегает 9 тактов на итерацию.


    1. ColdPhoenix
      00.00.0000 00:00
      +1

      RISC, то за один такт процессора должна выполняться одна инструкция.

      Нет такого требования у RISC процессоров.

      Обычно у них фиксированнное и легко предсказуемое число тактов на инструкцию.


    1. imdragon
      00.00.0000 00:00
      +1

      Для таких задержек надо DWT использовать, а не переменные мучить


  1. igor_suhorukov
    00.00.0000 00:00

    Интересный проект, молодцы! Не проще ли обучать робототехнике на болле высокоуровневых вещах? Как например проект machinekit-hal пытается перенести наработки из экосистемы linuxCNC на ARM платформу для real time управления


    1. Caska Автор
      00.00.0000 00:00
      +1

      Добрый день, спасибо за оценку проекта. Не знаю как остальные коллеги, я лично с таким проектом не сталкивался, его задумка очень интересна. Как говорилось ранее, в нашем центре всё ПО для микроконтроллеров реализовано на С, потому как это просто было "стихийно" и считалось нормальным. Над реализацией на более верхнем уровне задумался, спасибо за совет. За год работы над проектом мне попадались некоторые открытые проеты библиотек, в которых сделано намного большая работа чем у меня. Тут библиотека больше идет как облегчающая для студентов, которые выбрали путь освоения микроконтроллеров. Реализация высокоуровневого управления я планировал реализовать через библиотеки micro-ROS или просто общение с системами верхнего уровня по ModBus. Но для такого шага я буду реализовывать графический генератор конфигураций, чтобы ещё больше облегчить процесс добавления настроек и конфигурации в целом.


  1. Dark_Purple
    00.00.0000 00:00
    +1

    Наплодили новых сущностей, зачем он нужны не ясно.


    1. sami777
      00.00.0000 00:00

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


  1. gatal121
    00.00.0000 00:00
    +2

    при выполнении некоторых макросов, либо их множества приводит к ошибкам из-за слишком быстрой скорости выполнения

    После этой фразы стало интересно глянуть код. На беглый взгляд показалось что там немало мест, где возможны ошибки. Например, в SetPWM:

    if(Channel > 4) return false;
    __attribute__((unused)) uint32_t *address = ((uint32_t *)TIMx);
    address += ((uint32_t)(((uint32_t)(Channel - 1) * 0x4) + 0x34) >> 2);

    не обсуждая целесообразность адресной арифметики в этом месте, что будет при Channel равным нулю? address явно улетит за допустимый диапазон.

    Или в функции CalcTimPulseLength у вас будет деление на ноль при входном параметре Degree < 3.

    По барьерам памяти - насколько я понимаю, есть не так много ситуаций на Cortex-M4 где они нужны, так как нет переопределения порядка операций с памятью или выполнения инструкций и из-за особенностей AHB Lite и APB протоколов. Те примеры что я видел решались скорее dummy read\write, а не dsb https://developer.arm.com/documentation/ka003795/ Поэтому мне кажется, стоит более подробно исследовать причины HardFault и сравнить вашу реализацию с STM32CubeF4. Например, там я видел dummy read после установки APB1ENR, у вас на похожем месте dsb.


    1. Caska Автор
      00.00.0000 00:00

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


  1. Arturius92
    00.00.0000 00:00

    Прикольно, генератором кода(CubeMX) воспользоваться не получится?


    1. Caska Автор
      00.00.0000 00:00

      Добрый день. К сожалению, генератор кода CubeMx не сгенерирует проект на основе данной библиотеки. Единственный вариант автоматической генерации, как мне кажется, это создание своего генератора проектов. Над этим неоднократно задумывался, есть небольшие тестовые наработки, надеюсь в скором времени смогу довести это до стадии релиза.


  1. stslit
    00.00.0000 00:00

    Важное дело делаете! Обязательно продолжайте! На таком уровне, STM32 будет доступен даже школьниками. Тем более милианр выпускает полный аналог этого процессора. Ошибки при разработке всегда будут (сколько лет майкрософт? багов стало меньше?). Есть хороший принцип: ошибайся раньше, исправляй быстрее. Было бы хорошо сделать синтетическую среду типа processing (принцип, чем проще - тем лучше), а может и до скреча доберетесь, тогда ардуино можно будет отпаривать на пенсию! шучу)


    1. Caska Автор
      00.00.0000 00:00

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