Notice: Memcache::get(): Server localhost (tcp 11211) failed with: Connection refused (111) in /home/a146/www/we.easyelectronics.ru/engine/lib/external/DklabCache/Zend/Cache/Backend/Memcached.php on line 134
Месяц HAL продолжается: HAL для LPC / LPC1xxx / Сообщество EasyElectronics.ru

Месяц HAL продолжается: HAL для LPC

В продолжение месяца постов о HAL решил написать и о своей библиотеке, правда, в отличие от предыдущих она в основном специализируется на контроллерах от NXP. Библиотека не использует дополнительных прослоек и работает напрямую с регистрами. Она состоит из двух обязательных частей, которые собираются с помощью GCC ARM, make и kconfig, и затем статически линкуются с основным проектом.

Предпосылки

К моменту начала написания библиотеки я уже использовал в своих радиолюбительских устройствах два семейства контроллеров от NXP: LPC134x и LPC176x. Предоставленные производителем библиотеки не всегда соответствовали ожиданиям (чего же еще от них ждать), в связи с чем возникло желание их обобщить и систематизировать. В основу первых попыток были положены заголовочные файле от производителя, но быстро выяснилось, что это была не самая удачная идея — отдельные моменты в них были неоптимальны, либо неконсистенты между семействами. Многие семейства используют одинаковые IP-ядра периферии: большая часть LPC имеет полностью одинаковые или сильно похожие между собой блоки, например UART, SSP, GPDMA (последние два к тому же лицензированы у ARM, в связи с чем встречаются и у других производителей). Описания же у блоков часто различались, что вызывало трудности с написанием обобщенных драйверов, которые были бы переносимых между различными семействами. В какой-то момент мне надоело точечно исправлять заголовочные файлы, и я привел их к единому виду, полностью переписав.

Внутренняя структура

Первая версия была монолитной, а по способу сборки напоминала классические библиотеки от производителей, т.е. собиралась внутри целевого проекта. В дальнейшем сборка была переведена на make и kconfig, а библиотека была разделена на более общую часть «xcore», содержащую простые платформонезависимые модули, собирающиеся под указанное процессорное ядро (могут собираться в том числе и под x86), а не серию контроллеров, и на «halm», содержащую непосредственно драйверы, собирающуюся уже под конкретную серию контроллеров и обеспечивающую гибкую и удобную настройку сборки с помощью kconfig-frontends. Сборка не требует использования графических интерфейсов и может производиться удаленно или автоматически, например, по commit hook.
Библиотека предоставляет высокоуровневые интерфейсы, реализующие наиболее востребованную функциональность, перегруженность интерфейсов, традиционная для библиотек от производителя, не приветствуется. Единичные случаи, в которых требуются дополнительные функции, предполагается реализовывать наследованием. Следует отметить абстракцию над периферийными интерфейсами: в отличие от новомодных тенденций в CMSIS (впрочем, первая версия halm появилась, если не ошибаюсь, раньше CMSIS-Driver), было решено пойти дальше, объединив их все единым абстрактным классом и не подразделять на подтипы навроде Serial, SPI и т.д., впрочем, и это не дотягивает до файлов в Linux или в Nuttx.

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

Перед публикацией я подготовил несколько примеров в отдельном репозитории. Примеры доступны по следующей ссылке: github.com/stxent/halm-examples. Сборка осуществляется под Linux; теоретически, примеры можно собрать и на Windows с использованием cygwin. Для сборки примеров необходимо иметь в системе установленные make и LPCXpresso, поскольку примеры при линковке используют урезанную версию libc — Redlib, тем не менее, жесткой привязки нет и, изменив скрипты линковки, можно использовать любой GCC ARM с newlib. Следует отметить, что у рассматриваемых в посте библиотек никакой привязки к Redlib нет, и они свободно собираются любым GCC ARM, поддерживающим C11.

Итак, порядок действий для сборки следующий:
  git clone https://github.com/stxent/halm-examples.git
  cd halm-examples
  git submodule update --init --recursive
  make CROSS_COMPILE=arm-none-eabi-
Первая команда вытягивает репозиторий с примерами, с помощью второй осуществляется переход в соответствующую директорию, а третья вытягивает субрепозитории — библиотеки xcore и halm. Четвертая запускает сборку, после чего собранные исполняемые файлы станут доступны в директориях build/lpc13xx_default и build/lpc43xx_default, по одному ELF-файлу на каждый из примеров. Дополнительный аргумент для make можно не указывать, если компилятор и линкер будут в $PATH. Исполняемые файлы затем можно прошить любым удобным способом, например, с помощью той же LPCXpresso.
Структура репозитория с примерами создана с целью минимизации действий по сборке. Примеры подразделены на группы, в которых конфигурация библиотек одинакова, для каждой из групп индивидуально собираются xcore и halm, далее собираются и линкуются примеры. Сборка библиотек по отдельности чуть сложней, для этого обязательным требованием становится наличие конфигурационного приложения kconfig, ранее поставлявшегося с ядром Linux, но ныне часто входящего в репозитории ОС.

Пример 1: мигаем светодиодом

Первый рассматриваемый пример находится в директории examples/lpc13xx_default/blinking_led. Пример показывает использование GPIO и одного из четырех стандартных таймеров контроллера.

#define LED_PIN PIN(3, 0)
Сначала с помощью макроса PIN(port, offset) задается идентификатор для пина, к которому подключен светодиод. Идентификатор имеет тип pinNumber (на данный момент это переопределенный uint16_t) и формируется таким образом, чтобы нулевое значение соответствовало недопустимому пину.

  struct Pin led;

  led = pinInit(LED_PIN);
  pinOutput(led, 0);
Далее в функции main производится инициализация структуры доступа для этого пина с помощью функции pinInit, в этот момент никаких действий с периферией еще не происходит, функция является потокобезопасной и может выполняться произвольное число раз и из различных частей программы, дополнительная память под структуру доступа не выделяется. После инициализации структуры доступа происходит непосредственное конфигурирование и установка начального значения с помощью функции pinInit. Следует отметить, что функции для деинициализации пина не предусмотрено, если возникает такая необходимость, следует переконфигурировать пин в другой режим. После этого структура доступа может использоваться для настройки альтернативных функций, подтяжек, выходного тока, типа выхода, а также для установки и чтения логического уровня на этом пине.

static const struct GpTimerConfig timerConfig = {
    .frequency = 1000,
    .channel = GPTIMER_CT32B0
};

  struct Timer *timer;
  bool event = false;

  timer = init(GpTimer, &timerConfig);
  assert(timer);

  timerSetOverflow(timer, 500);
  timerCallback(timer, ledToggle, &event);
  timerSetEnabled(timer, true);
Далее производится инициализация и настройка таймера. Небольшая ремарка о подходе к построению драйверов: все драйвера выполнены с применением ООП на C, похожего на описанный Шрайнером, но без RTTI, множественного наследования и исключений; память выделяется однократно на куче с помощью стандартных функций языка. Итак, вернемся к таймеру: для таймера с помощью функции init из библиотеки xcore вызывается конструктор, создающий минимальную нижнюю (для конкретного семейства) и более сложную верхнюю (для всех семейств с этим IP-ядром) части драйвера, и производящий начальную инициализацию блока в соответствии со структурой инициализации. Отдельные поля в структурах опциональны и спроектированы таким образом, что в случае инициализации их нулем будут использоваться адекватные значения по умолчанию (упрощенный аналог значений по умолчанию в C++). В нашем случае, для настройки таймера указываются только частота и идентификатор блока периферии.

static void ledToggle(void *argument)
{
  bool * const event = argument;

  *event = true;
}

  while (1)
  {
    while (!event)
      barrier();

    event = false;

    pinWrite(led, ledValue);
    ledValue ^= 0x01;
  }
После инициализации настраивается период таймера, задается callback-функция и таймер включается. Далее раз в 500 мс происходит переполнение таймера, вызывается пользовательская функция, выставляющая флаг, и в основном цикле по этому флагу выполняется переключение состояния пина контроллера.

Пример 2: работа с последовательным портом

Следующий пример находится в директории examples/lpc13xx_default/serial, в нем показан простейший пример работы с последовательным портом — ко всем введенным пользователем символам прибавляется единица и получившиеся символы отправляются обратно.

static const struct SerialConfig serialConfig = {
    .channel = 0,
    .rx = PIN(1, 6),
    .tx = PIN(1, 7),
    .rate = 19200,
    .rxLength = 64,
    .txLength = 64
};

  struct Interface *serial;
  bool event = false;

  serial = init(Serial, &serialConfig);
  assert(serial);
  ifCallback(serial, serialEventCallback, &event);
В начале примера выполняется инициализация драйвера последовательного порта посредством вызова конструктора. В конфигурационной структуре указан номер блока UART, скорость по умолчанию, пины и размер очередей на прием и отправку. В примере используется неблокирующая версия драйвера, использующая два встроенных кольцевых буфера и работающая на прерываниях, кроме этого, реализованы блокирующая версия на опросе и zero-copy версия на DMA.
При инициализации в конструктор передаются идентификаторы пинов, драйвер проверяет корректность пинов и инициализирует их на соответствующую альтернативную функцию. Проверка выполняется с помощью assert, т.е. только для отладочных сборок, после тестирования работоспособности конфигурации предполагается выполнять сборку библиотек с отключенными assert и максимальным набором оптимизаций, в этом случае будут проверяться только ошибки времени выполнения.

  while (1)
  {
    while (!event)
      barrier();
    event = false;

    size_t available;

    if (ifGet(serial, IF_AVAILABLE, &available) == E_OK && available > 0)
    {
      size_t bytesRead;

      pinSet(led);

      while ((bytesRead = ifRead(serial, buffer, sizeof(buffer))))
        processInput(serial, buffer, bytesRead);

      pinReset(led);
    }
  }
После инициализации устанавливается пользовательская функция, по содержимому аналогичная таковой из предыдущего примера, которая вызывается, если очередь заполнилась наполовину или закончился прием по последовательному порту. Пользовательская функция выставляет флаг, который обрабатывается в главном цикле.

Пример 3: заменяем UART на USB CDC

В третьем примере последовательный порт заменен на USB, устройство подключается по USB и представляется как CDC ACM. Пример находится в директории examples/lpc13xx_default/usb_cdc. Замена практически полностью эквивалентна: драйвер виртуального последовательного порта CdcAcm, использующий драйвер UsbDevice, также выполнен в неблокирующем стиле. Тем не менее, есть и отличие — размер буфера на чтение должен быть больше или равным размеру пакета USB, так что при реализации платформонезависимых частей необходимо подбирать такой размер буфера, который подойдет для всех использующихся интерфейсов. Это отличие следует из особенности реализации драйвера порта: первые версии использовали в дополнение к пулу буферов USB промежуточный кольцевой буфер, но впоследствии с целью упрощения кода от него было решено отказаться и использовать только пул.
static const struct UsbDeviceConfig usbConfig = {
    .dm = PIN(PORT_USB, PIN_USB_DM),
    .dp = PIN(PORT_USB, PIN_USB_DP),
    .connect = PIN(PORT_0, 6),
    .vbus = PIN(PORT_0, 3),
    .channel = 0,
    .priority = 0
};

  struct Entity *usb;
  struct Interface *serial;

  setupClock();

  usb = init(UsbDevice, &usbConfig);
  assert(usb);

  const struct CdcAcmConfig config = {
      .device = usb,
      .rxBuffers = 4,
      .txBuffers = 4,

      .endpoint = {
          .interrupt = 0x81,
          .rx = 0x02,
          .tx = 0x82
      }
  };

  serial = init(CdcAcm, &config);
  assert(serial);
  ifCallback(serial, serialEventCallback, &event);
В начале функции main выполняется инициализация, проходящая в три этапа: сначала инициализируется тактирование контроллера, далее создается драйвер USB Device, и в конце с использованием этого драйвера выполняется инициализация CDC ACM. Далее точно так же, как и для драйвера Serial, устанавливается пользовательская функция, которая вызывается после получения данных.

Пример 4: переезд на другую платформу

Четвертый пример находится в директории examples/lpc43xx_default/blinking_led и реализует мигание светодиодом на отладочной плате с LPC4337. К сожалению, на данный момент библиотека поддерживает только основное ядро M4, вспомогательное ядро M0 и межпроцессорное взаимодействие не поддерживаются.
#define LED_PIN PIN(PORT_6, 6)
Из отличий от первого примера можно отметить немного изменившийся стиль задания идентификатора пина и инициализацию системы тактирования. Для более удобного задания порта созданы определения, поскольку пины сгруппированы в 16 портов с именами от 0 до 9 и от A до F. Следует также отметить, что есть пины, относящиеся к специальным группам (в основном это PORT_CLK), для которых может выполняться частичная настройка, например, выбор альтернативных функций, регулировка выходного тока, но работа в режиме GPIO недоступна. Систему тактирования на самом деле настраивать необязательно, но есть особенность: при первом запуске бутлоадер конфигурирует процессор на 96 МГц от PLL, а при программной перезагрузке, например, через отладчик, процессор стартует на 12 МГц от встроенного RC-генератора. Определение текущего режима потребовало бы дополнительных действий, и это было решено обойти просто всегда инициализируя тактирование. Далее все действия аналогичны первому примеру.

Пример 5: перевозим USB

В пятом примере, находящемся в examples/lpc43xx_default/usb_cdc, работа с CDC ACM перенесена на платформу с контроллером LPC4337. Библиотека сконфигурирована на работу с портом USB0 в HS-режиме.
Размер буфера в связи с переходом с Full Speed на High Speed в данном примере увеличен до 512 байт, все остальные отличия касаются только инициализации периферии, остальная работа с интерфейсом для последовательного обмена идентична второму и третьему примерам.

Заключение

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

Ссылки:
github.com/stxent/halm-examples — репозиторий с примерами
github.com/stxent/halm — библиотека драйверов
github.com/stxent/xcore — базовые модули
  • +1
  • 29 мая 2016, 18:19
  • StXt

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

RSS свернуть / развернуть
А что делают функции pinOutput и barrier? Почему у пинов личный набор функций типа pinInit, а не init(GPIO, PinConf)?
0
  • avatar
  • Vga
  • 30 мая 2016, 08:40
1. pinOutput(pin, value) — конфигурирует пин на выход с настройками по умолчанию, которых достаточно в большинстве случаев (без подтяжек, если таковые доступны аппаратно, с максимальным выходным током и в режиме push-pull), и устанавливает начальное значение на пине. Парой к ней идет pinInput(pin), которая конфигурирует на вход с настройками по умолчанию (без подтяжек, без фильтров).
2. barrier() — GCC'шный memory barrier, в данном случае локально отключающий оптимизацию для event.
3. 3. Ближайшие эквиваленты в C++ для пары функций init/deinit — new/delete, т.е. в init выделяется память и вызывается конструктор. Подход с init/deinit в библиотеке используется там, где требуется полиморфизм (хотя отдельные места можно будет упростить), там же, где он не нужен, функции используются напрямую, без дескриптора класса (для пинов, контейнеров из xcore).

Для пинов выбран существующий стиль для того, чтобы не выделять память на куче и для удобства, чтобы не создавать дополнительные конфигурационные структуры. Первое можно было бы обойти, напрямую вызывая конструктор через дескриптор класса для объекта, созданного статически, например: Pin->init(&led, &ledConfig), но это сложней в использовании. По сути pinInit это и есть конструктор, а дескриптор класса при этом отсутствует.

Сделать отдельный абстрактный класс для пина было соблазнительной идеей, тем не менее, от нее было решено отказаться, поскольку в этом случае исчезает возможность оптимизации компилятором операций чтения/записи пинов, которые на данный момент выполнены в виде static inline функций.
0
2. barrier() — GCC'шный memory barrier, в данном случае локально отключающий оптимизацию для event.
А не правильней ли было в таком случае объвить event как volatile?
там же, где он не нужен, функции используются напрямую, без дескриптора класса (для пинов, контейнеров из xcore)
А почему для пинов он не нужен? Пины не обязательно на родном GPIO висят. Они могут быть на 74HC595 или ином GPIO-расширителе. Помнится мне, neiver делал GPIO-обертку, которая обеспечивала и оптимальный доступ к «локальным» пинам, и позволяла вынести их на расширитель.
0
volatile в данном случае не обязателен и уменьшит возможности компилятора по оптимизации, рекомендации по использованию барьеров взяты из этой статьи we.easyelectronics.ru/Soft/skolzkaya-dorozhka-dlya-poklonnikov-volatile.html

Я думал насчет расширителей портов, тем не менее такой вариант был откинут как редко встречающийся, производительность для меня в этом случае была более приоритетна.
Если не ошибаюсь, при использовании подхода neiver необходимо было бы делать шаблонным и класс драйвера, с соответствующими последствиями от их использования. Кроме того, полноценной альтернативы шаблонам в C нет.
0
с соответствующими последствиями от их использования.
Какими именно, если не секрет?
0
Мне видятся следующие последствия:
в случае использования нескольких экземпляров одного драйвера (например, 2 UART или 2 SPI) код будет продублирован для каждой подстановки;
может потребоваться сделать реализацию видимой для пользовательского приложения (перетащить в хедер), чтобы подстановка могла правильно отработать.

Следствием из предыдущего будет усложнение конфигурирования библиотеки: сейчас пользовательскому приложению достаточно указать один символ (например, -DLPC13XX), определяющий, какое семейство используется. При использовании заголовочных файлов в пользовательском приложении этого символа достаточно, при сборке библиотеки же используется больший набор символов, например, предусмотрена опция, включающая или отключающая (в целях уменьшения размера прошивки) возможность поиска устройств в реализации 1-Wire на SSP. В шаблонном варианте отдельные символы или конфиг придется тащить и в пользовательское приложение. Похожий момент немного раздражает при использовании Nuttx.
0
в случае использования нескольких экземпляров одного драйвера (например, 2 UART или 2 SPI) код будет продублирован для каждой подстановки;
Это та часть, с которой вполне справится компилятор и линкер.
может потребоваться сделать реализацию видимой для пользовательского приложения (перетащить в хедер), чтобы подстановка могла правильно отработать.
Это, как бы, очевидно. Не вижу в этом проблемы.
Следствием из предыдущего будет усложнение конфигурирования библиотеки: сейчас пользовательскому приложению достаточно указать один символ (например, -DLPC13XX), определяющий, какое семейство используется.
Это решается чем-нибудь наподобие такого:

#define USE_DLPC13XX
#include <lpc-hal.h>

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

Или вообще директивами командной строки времени компиляции.
Собственно, так и происходит, под -DLPC13XX я имел в виду аргумент командой строки для GCC, символ соответственно будет LPC13XX. 1 символ определить быстрей, удобней и устойчивей к ошибкам, чем определять группу символов.
0
При желании эмулировать некоторые возможности шаблонов можно и на макросах в С, и с этим тоже справятся компилятор с линкером, от разбухания прошивки это не избавит.
В случае макросов — не избавит. В случае щаблонов — избавит.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.