Главная » Хабрахабр » Отладочная плата STM32F4 в форм-факторе Raspberry Pi

Отладочная плата STM32F4 в форм-факторе Raspberry Pi

Хочу представить общественности мой проект — небольшая отладочная плата на базе STM32, но в форм-факторе Raspberry Pi. image Добрый день, уважаемые хабровчане! А также приятные дополнения в виде разъёма для micro-SD карты и стерео-усилителя. От других отладочных плат она отличается тем, что имеет совместимую с корпусами от Raspberry Pi геометрию и наличие ESP8266-модуля в качестве беспроводного модема. В статье я хочу подробно описать как аппаратную, так и программную части этого проекта. Для использования всего этого богатства я разработал высокоуровневую библиотеку и демонстрационную программу (на C++11).

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

Основные мотиваторы этого проекта выглядят так: Для начала, попытаюсь ответить на вопрос, зачем это все.

  • Выбор платформы STM32 обусловлен чисто эстетическими соображениями — нравится соотношение цена/производительность, плюс широкий спектр периферии, плюс большая и удобная экосистема разработки от производителя контроллеров (sw4stm, cubeMX, HAL library).
  • Конечно, есть много отладочных плат как от самого производителя контроллеров (Discovery, Nucleo), так и от сторонних производителей (например, Olimex). Но повторить многие из них в домашних условиях в своём форм-факторе является проблематичным, для меня, по крайней мере. В моём же варианте имеем несложную двухслойную топологию и удобные для ручной пайки компоненты.
  • Для своих устройств хочется иметь достойные корпуса, дабы замаскировать низкое качество электроники внутри. Есть по крайней мере две популярные платформы, для которых имеется огромное количество самых разнообразных корпусов: Ардуино и Raspberry Pi. Вторая из них мне показалась более удобной с точки зрения расположения вырезок под разъёмы. Поэтому в качестве донора для геометрии платы я выбрал именно её.
  • Выбранный мной контроллер на борту имеет USB, SDIO, I2S, сеть. С другой стороны, эти же интерфейсы полезны и для домашней хобби-платформы. Именно поэтому, помимо самого контроллера со стандартной обвязкой, я добавил USB разъём, SD карточку, звуковой тракт (цифро-аналоговый конвертор и усилитель), а также беспроводной модуль на базе ESP8266.

Схема и компоненты

Как мне кажется, получилась достаточно симпатичная плата со следующими характеристиками и компонентами:

  • Контроллер STM32F405RG: ARM 32-bit Cortex -M4 с математическим сопроцессором, частота до 168 MHz, 1 Mb флаш-памяти, 196 Kb оперативной памяти.
    Использованные пины контроллера
    Обвязка контроллера
  • Разъём SWD для программирования контроллера (6 контактов).
  • Кнопка Reset для перезагрузки.
  • Трёхцветный светодиод. С одной стороны, три вывода контроллера потеряны. С другой стороны, они бы все равно потерялись в силу ограниченности контактов на разъёмах GPIO, а для отладки такой светодиод вещь очень полезная.
  • Высокочастотный HSE (16 MHz для тактирования ядра) и низкочастотный LSE (32.7680 kHz для часов реального времени) кварцы.
  • Контакты GPIO с шагом 2.54 мм совместимы с макетными платами.
  • На месте 3.5 мм звукового разъёма Raspberry Pi я расположил разъём питания 5 вольт. На первый взгляд, решение спорное. Но есть доводы "за". Питание с разъёма USB опционально присутствует (подробности ниже), но для отладки схемы это плохой вариант, так как время до сжигания USB порта компьютера в этом случае может быть довольно коротким.
    Контур питания
  • Разъём mini-USB. С одной стороны, он подключён через микросхему защиты STF203-22.TCT к порту USB-OTG контроллера. С другой стороны, контакт питания VBUS выведен на разъём GPIO. Если соединить его с контактом +5V, то плата будет запитана от USB порта.
    Контур USB
  • Разъём карты памяти micro-SD с обвязкой: подтягивающие резисторы на 47 kΩ, транзистор управления питанием (P-channel MOSFET BSH205) и маленький зелёненький светодиод на линии питания.
    Контур карты micro-SD
    Затвор транзистора подключён к контакту PA15 контроллера. Это системный контакт JTDI контроллера, который интересен тем, что в исходном положении он сконфигурирован как выход с высоким уровнем (pull-up) напряжения. Так как вместо JTAG для программирования задействован SWD, данный контакт остаётся свободным, и его можно использовать для других целей, например, управления транзистором. Это удобно — при подаче питания на плату карта памяти обесточена, для её включения нужно подать низкий уровень на контакт PA15.
  • Цифро-аналоговый преобразователь на основе UDA1334. Этой микросхеме не нужен внешний тактовый сигнал, что облегчает её использование. Данные передаются по шине I2S. С другой стороны, Datasheet рекомендует использовать аж 5 полярных конденсаторов на 47 μF. Размер в данном случае важен. Самые маленькие, которые получилось купить, это танталовые с размером 1411, которые очень даже не дешёвые. Впрочем, про цену я напишу подробней чуть ниже. Для аналогового питания используется свой линейный стабилизатор, питание цифровой части включается/выключается сдвоенным транзистором.
    Контур DAC
  • Двухканальный усилитель на основе двух микросхем 31AP2005. Их основное преимущество — малое количество компонент обвязки (только фильтры питания и входной фильтр). Аудиовыход — 4 площадки с шагом 2.54 мм. Для себя я так пока не определился, что лучше — такой кустарный вариант или, как на малинке, 3.5 мм штекер. Как правило, 3.5 мм ассоциируется с наушниками, в нашем же случае речь идёт о подключении динамиков.
    Контур усилителя
  • Последний модуль — платка ESP11 с обвязкой (питание, разъём для программирования) в качестве WiFi модема. Выводы UART платы подключены к контроллеру и одновременно выведены на внешний разъём (для работы с платой напрямую с терминала и программирования). Есть переключатель питания (постоянное внешнее или управление с микроконтроллера). Есть дополнительный светодиод для индикации питания и разъём «FLASH» для перевода платы в режим программирования.
    Контур ESP
    Конечно, ESP8266 сам по себе неплохой контроллер, но он всё-таки уступает STM32F4 как по производительности, так и по периферии. Да и размер с ценой этого модуля так и намекают, что это прямо-таки вылитый модемный блок для своего более старшего собрата. Модуль управляется по USRT с использованием текстового AT протокола.

Пара фотографий:

Подготовка модуля ESP11

Я уверен, что многие с ней уже знакомы, поэтому подробное руководство будет здесь лишним. ESP8266 — вещь известная. В силу схематических особенностей подключения модуля ESP11 к плате, приведу только краткое руководство для тех, кто желает поменять его прошивку:

  • Для работы с ESP буду использовать утилиту esptool. В отличие от стандартной утилиты от производителя, esptool является платформенно-независимой.
  • Для начала, включаем режим внешнего питания перемычкой ESP-PWR (замыкаем контакты 1 и 2), и подключаем модуль к компьютеру через любой USART-USB адаптер. Адаптер подключается к контактам GRD/RX/TD. Подаём питание на плату:
  • Убеждаемся, что адаптер опознан операционной системой. В моём примере, я использую адаптер на базе FT232, поэтому с списке устройств он должен быть виден как FT232 Serial (UART) IC:

    > lsusb
    ...
    Bus 001 Device 010: ID 0483:3748 STMicroelectronics ST-LINK/V2
    Bus 001 Device 009: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC ...

  • Сами ESP8266 различаются объёмом флэш-памяти. На практике, в одном и том же модуле ESP11, я встречал как 512 KB (4 Mbit), так и 1 MB (8 Mbit). Так что первое, что нужно проверить — сколько памяти в используемом экземпляре модуля. Отключаем питание с платы, и переводим модуль в режим программирования, замыкая перемычку "FLASH":
  • Включаем питание, запускаем esptool со следующими параметрами

    > esptool.py --port /dev/ttyUSB0 flash_id
    Connecting....
    Detecting chip type... ESP8266
    Chip is ESP8266EX
    Uploading stub...
    Running stub...
    Stub running...
    Manufacturer: e0
    Device: 4014
    Detected flash size: 1MB
    Hard resetting...

  • esptool сообщает, что, в данном случае, мы имеем дело с модулем с 1 MB памяти.
  • Для версии с 1 MB можно использовать актуальную прошивку, например, ESP8266 AT Bin V1.6.1. Но она не подходит для версии с 4 Mbit, для которой нужно использовать что-нибудь постарее, например, эту. Прошивка состоит из нескольких файлов, стартовые адреса каждого файла указаны в официальном документе ESP8266 AT Instruction Set. Эти стартовые адреса используются как параметры утилиты esptool. Например, для модуля с 1 MB параметры esptool будут выглядеть так (все необходимые файлы нужно предварительно извлечь из архива прошивки и собрать в рабочем каталоге)

    > esptool.py --port /dev/ttyUSB0 write_flash 0x00000 boot.bin 0x01000 user1.1024.new.2.bin 0x7E000 blank.bin 0xFB000 blank.bin 0xFC000 esp_init_data_default.bin 0xFE000 blank.bin

  • Подаём питание на плату, запускаем esptool с указанными параметрами.
  • После завершения работы скрипта отключаем питание от платы, размыкаем перемычку "FLASH", включаем управление питанием с микроконтроллера. Модуль к работе готов.

Программное обеспечение

Она делает следующее: На github находится тестовая программа.

  • выводит контроллер на максимальную частоту (168 MHz)
  • активирует часы реального времени
  • активирует SD карту и читает с неё сетевую конфигурацию. Для работы с файловой системой используется библиотека FatFS
  • устанавливает соединение с заданной сетью WLAN
  • соединяется с заданным NTP сервером и запрашивает с него текущее время. Подводит часы.
  • контролирует состояние нескольких заданных портов. Если их состояние изменилось, посылает текстовое сообщение на заданный TCP сервер.
  • при нажатии на внешнюю кнопку читает заданный *.wav файл с SD карты и воспроизводит его в асинхронном режиме (I2S с использованием DMA контроллера).
  • работа с ESP11 также реализована в асинхронном режиме (пока без DMA, просто на прерываниях)
  • осуществляет логирование через USART1 (контакты PB6/PB7)
  • ну и, конечно же, мигает светодиодом.

Например, из относительно последних: раз, два, три. На Хабре было много статей, посвящённых программированию STM32 на достаточно низком уровне (только управлением регистров или CMSIS). Но вот для длительного хобби-проекта, когда хочется, чтобы всё было красиво и расширяемо, этот подход уж слишком низкоуровневый. Статьи, безусловно, очень качественные, но моё субъективное мнение — для разовой разработки какого-либо продукта этот подход, быть может, себя и оправдывает. Поэтому я решил пойти в этом же направлении и надстроить над библиотекой HAL достаточно высокоуровневую объектно-ориентированную прослойку. Одна из причин популярности Ардуино именно как программной платформы, на мой взгляд, заключается в том, что авторы Ардуино ушли с такого низкого уровня на объектно-ориентированную архитектуру.

Таким образом, получается три уровня программы:

  • Библиотеки производителя (HAL, FatFS, в будущем USB-OTG) образуют фундамент
  • На этом фундаменте базируется моя библиотека StmPlusPlus. Она включает в себя набор базовых классов (типа System, IOPort, IOPin, Timer, RealTimeClock, Usart, Spi, I2S), набор классов-драйверов внешних устройств (типа SdCard, Esp11, DcfReceiver, Dac_MCP49x1, AudioDac_UDA1334 и тому подобное), а также сервисные классы типа асинхронного проигрывателя WAV.
  • На базе библиотеки StmPlusPlus строится уже само приложение.

Пока я несколько старомоден — остаюсь на C++11. Что касается диалекта языка. Кстати, на Хабре есть замечательная статья на эту тему. Этот стандарт имеет несколько фишек, особенно полезных для разработки встроенного ПО: классы-перечисления (enum class), вызов конструкторов при помощи фигурных скобок для контроля типов передаваемых параметров, статические контейнеры типа std::array.

Библиотека StmPlusPlus

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

Первый пример — класс для периодического опроса состояния пина (например, кнопки) и вызова обработчика при изменении этого состояния:

class Button : IOPin
{
public: class EventHandler ; Button (PortName name, uint32_t pin, uint32_t pull, const RealTimeClock & _rtc, duration_ms _pressDelay = 50, duration_ms _pressDuration = 300); inline void setHandler (EventHandler * _handler) { handler = _handler; } void periodic (); private: const RealTimeClock & rtc; duration_ms pressDelay, pressDuration; time_ms pressTime; bool currentState; uint32_t numOccured; EventHandler * handler;
};

Конструктор определяет все параметры кнопки:

Button::Button (PortName name, uint32_t pin, uint32_t pull, const RealTimeClock & _rtc, duration_ms _pressDelay, duration_ms _pressDuration): IOPin{name, pin, GPIO_MODE_INPUT, pull, GPIO_SPEED_LOW}, rtc{_rtc}, pressDelay{_pressDelay}, pressDuration{_pressDuration}, pressTime{INFINITY_TIME}, currentState{false}, numOccured{0}, handler{NULL}
{ // empty
}

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

void Button::periodic ()
{ if (handler == NULL) { return; } bool newState = (gpioParameters.Pull == GPIO_PULLUP)? !getBit() : getBit(); if (currentState == newState) { // state is not changed: check for periodical press event if (currentState && pressTime != INFINITY_TIME) { duration_ms d = rtc.getUpTimeMillisec() - pressTime; if (d >= pressDuration) { handler->onButtonPressed(this, numOccured); pressTime = rtc.getUpTimeMillisec(); ++numOccured; } } } else if (!currentState && newState) { pressTime = rtc.getUpTimeMillisec(); numOccured = 0; } else { duration_ms d = rtc.getUpTimeMillisec() - pressTime; if (d < pressDelay) { // nothing to do } else if (numOccured == 0) { handler->onButtonPressed(this, numOccured); } pressTime = INFINITY_TIME; } currentState = newState;
}

Для отсчёта времени здесь используется не HAL_GetTick, который в силу своего типа (uint32_t) сбрасывается по переполнению каждые 2^32 миллисекунд (каждые 49 дней). Основной плюс такого подхода — разнесение логики и кода детектирования события от его обработки. Я реализовал собственный класс RealTimeClock, который отсчитывает миллисекунды со старта программы, или включения контроллера, как uint64_t, что даёт примерно 5^8 лет.

Например, SPI. Второй пример — работа с аппаратным интерфейсом, которых в контроллере несколько. С точки зрения основной программы, очень удобно выбрать только нужный интерфейс (SPI1/SPI2/SPI3), а всё остальные параметры, которые зависят от этого интерфейса, сконфигурирует конструктор класса.

class Spi
{
public: const uint32_t TIMEOUT = 5000; enum class DeviceName { SPI_1 = 0, SPI_2 = 1, SPI_3 = 2, }; Spi (DeviceName _device, IOPort::PortName sckPort, uint32_t sckPin, IOPort::PortName misoPort, uint32_t misoPin, IOPort::PortName mosiPort, uint32_t mosiPin, uint32_t pull = GPIO_NOPULL); HAL_StatusTypeDef start (uint32_t direction, uint32_t prescaler, uint32_t dataSize = SPI_DATASIZE_8BIT, uint32_t CLKPhase = SPI_PHASE_1EDGE); HAL_StatusTypeDef stop (); inline HAL_StatusTypeDef writeBuffer (uint8_t *pData, uint16_t pSize) { return HAL_SPI_Transmit(hspi, pData, pSize, TIMEOUT); } private: DeviceName device; IOPin sck, miso, mosi; SPI_HandleTypeDef *hspi; SPI_HandleTypeDef spiParams; void enableClock(); void disableClock();
};

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

Spi::Spi (DeviceName _device, IOPort::PortName sckPort, uint32_t sckPin, IOPort::PortName misoPort, uint32_t misoPin, IOPort::PortName mosiPort, uint32_t mosiPin, uint32_t pull): device(_device), sck(sckPort, sckPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), miso(misoPort, misoPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), mosi(mosiPort, mosiPin, GPIO_MODE_AF_PP, pull, GPIO_SPEED_HIGH, false), hspi(NULL)
{ switch (device) { case DeviceName::SPI_1: #ifdef SPI1 sck.setAlternate(GPIO_AF5_SPI1); miso.setAlternate(GPIO_AF5_SPI1); mosi.setAlternate(GPIO_AF5_SPI1); spiParams.Instance = SPI1; #endif break; ... case DeviceName::SPI_3: #ifdef SPI3 sck.setAlternate(GPIO_AF6_SPI3); miso.setAlternate(GPIO_AF6_SPI3); mosi.setAlternate(GPIO_AF6_SPI3); spiParams.Instance = SPI3; #endif break; } spiParams.Init.Mode = SPI_MODE_MASTER; spiParams.Init.DataSize = SPI_DATASIZE_8BIT; spiParams.Init.CLKPolarity = SPI_POLARITY_HIGH; spiParams.Init.CLKPhase = SPI_PHASE_1EDGE; spiParams.Init.FirstBit = SPI_FIRSTBIT_MSB; spiParams.Init.TIMode = SPI_TIMODE_DISABLE; spiParams.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spiParams.Init.CRCPolynomial = 7; spiParams.Init.NSS = SPI_NSS_SOFT;
}

В данном случае лучше использовать шаблоны, где параметром шаблона являются HAL имя интерфейса (SPI1, SPI2, SPI3), параметры пинов (GPIO_AF5_SPI1), и что-то, что управляет включением/выключением тактирования. По этой же схеме реализованы процедуры enableClock и disableClock, что плохо расширяемо и плохо переносимо на другие контроллеры. Здесь есть интересная статья по этой теме, хотя в ней рессматриваются контроллеры AVR, что, впрочем, принципиальной разницы не имеет.

Начало и окончание передачи контролируются двумя методами start/stop:

HAL_StatusTypeDef Spi::start (uint32_t direction, uint32_t prescaler, uint32_t dataSize, uint32_t CLKPhase)
{ hspi = &spiParams; enableClock(); spiParams.Init.Direction = direction; spiParams.Init.BaudRatePrescaler = prescaler; spiParams.Init.DataSize = dataSize; spiParams.Init.CLKPhase = CLKPhase; HAL_StatusTypeDef status = HAL_SPI_Init(hspi); if (status != HAL_OK) { USART_DEBUG("Can not initialize SPI " << (size_t)device << ": " << status); return status; } /* Configure communication direction : 1Line */ if (spiParams.Init.Direction == SPI_DIRECTION_1LINE) { SPI_1LINE_TX(hspi); } /* Check if the SPI is already enabled */ if ((spiParams.Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE) { /* Enable SPI peripheral */ __HAL_SPI_ENABLE(hspi); } USART_DEBUG("Started SPI " << (size_t)device << ": BaudRatePrescaler = " << spiParams.Init.BaudRatePrescaler << ", DataSize = " << spiParams.Init.DataSize << ", CLKPhase = " << spiParams.Init.CLKPhase << ", Status = " << status); return status;
} HAL_StatusTypeDef Spi::stop ()
{ USART_DEBUG("Stopping SPI " << (size_t)device); HAL_StatusTypeDef retValue = HAL_SPI_DeInit(&spiParams); disableClock(); hspi = NULL; return retValue;
}

Класс реализует I2S интерфейс с использованием DMA-контроллера. Работа с аппаратным интерфейсом с использованием прерываний. Некоторые данные хранятся в структурах HAL (плюс к удобству, минус к объёму данных). В данном случае, он наследуется от класса «порт», то есть I2S — это порт со специальными свойствами. Некоторые данные передаются из основного кода по ссылкам (например, структура irqPrio).

class I2S : public IOPort
{
public: const IRQn_Type I2S_IRQ = SPI2_IRQn; const IRQn_Type DMA_TX_IRQ = DMA1_Stream4_IRQn; I2S (PortName name, uint32_t pin, const InterruptPriority & prio); HAL_StatusTypeDef start (uint32_t standard, uint32_t audioFreq, uint32_t dataFormat); void stop (); inline HAL_StatusTypeDef transmit (uint16_t * pData, uint16_t size) { return HAL_I2S_Transmit_DMA(&i2s, pData, size); } inline void processI2SInterrupt () { HAL_I2S_IRQHandler(&i2s); } inline void processDmaTxInterrupt () { HAL_DMA_IRQHandler(&i2sDmaTx); } private: I2S_HandleTypeDef i2s; DMA_HandleTypeDef i2sDmaTx; const InterruptPriority & irqPrio;
};

Его конструктор задаёт все статические параметры:

I2S::I2S (PortName name, uint32_t pin, const InterruptPriority & prio): IOPort{name, GPIO_MODE_INPUT, GPIO_NOPULL, GPIO_SPEED_FREQ_LOW, pin, false}, irqPrio{prio}
{ i2s.Instance = SPI2; i2s.Init.Mode = I2S_MODE_MASTER_TX; i2s.Init.Standard = I2S_STANDARD_PHILIPS; // will be re-defined at communication start i2s.Init.DataFormat = I2S_DATAFORMAT_16B; // will be re-defined at communication start i2s.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE; i2s.Init.AudioFreq = I2S_AUDIOFREQ_44K; // will be re-defined at communication start i2s.Init.CPOL = I2S_CPOL_LOW; i2s.Init.ClockSource = I2S_CLOCK_PLL; i2s.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE; i2sDmaTx.Instance = DMA1_Stream4; i2sDmaTx.Init.Channel = DMA_CHANNEL_0; i2sDmaTx.Init.Direction = DMA_MEMORY_TO_PERIPH; i2sDmaTx.Init.PeriphInc = DMA_PINC_DISABLE; i2sDmaTx.Init.MemInc = DMA_MINC_ENABLE; i2sDmaTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; i2sDmaTx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; i2sDmaTx.Init.Mode = DMA_NORMAL; i2sDmaTx.Init.Priority = DMA_PRIORITY_LOW; i2sDmaTx.Init.FIFOMode = DMA_FIFOMODE_ENABLE; i2sDmaTx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; i2sDmaTx.Init.MemBurst = DMA_PBURST_SINGLE; i2sDmaTx.Init.PeriphBurst = DMA_PBURST_SINGLE;
}

Начало передачи данных контролируются методам start, который отвечают за настройку параметров порта, тактирование интерфейса, настройку прерываний, старт DMA, старт самого интерфейса с заданными параметрами передачи.

HAL_StatusTypeDef I2S::start (uint32_t standard, uint32_t audioFreq, uint32_t dataFormat)
{ i2s.Init.Standard = standard; i2s.Init.AudioFreq = audioFreq; i2s.Init.DataFormat = dataFormat; setMode(GPIO_MODE_AF_PP); setAlternate(GPIO_AF5_SPI2); __HAL_RCC_SPI2_CLK_ENABLE(); HAL_StatusTypeDef status = HAL_I2S_Init(&i2s); if (status != HAL_OK) { USART_DEBUG("Can not start I2S: " << status); return HAL_ERROR; } __HAL_RCC_DMA1_CLK_ENABLE(); __HAL_LINKDMA(&i2s, hdmatx, i2sDmaTx); status = HAL_DMA_Init(&i2sDmaTx); if (status != HAL_OK) { USART_DEBUG("Can not initialize I2S DMA/TX channel: " << status); return HAL_ERROR; } HAL_NVIC_SetPriority(I2S_IRQ, irqPrio.first, irqPrio.second); HAL_NVIC_EnableIRQ(I2S_IRQ); HAL_NVIC_SetPriority(DMA_TX_IRQ, irqPrio.first + 1, irqPrio.second); HAL_NVIC_EnableIRQ(DMA_TX_IRQ); return HAL_OK;
}

Процедура stop делает всё наоборот:

void I2S::stop ()
{ HAL_NVIC_DisableIRQ(I2S_IRQ); HAL_NVIC_DisableIRQ(DMA_TX_IRQ); HAL_DMA_DeInit(&i2sDmaTx); __HAL_RCC_DMA1_CLK_DISABLE(); HAL_I2S_DeInit(&i2s); __HAL_RCC_SPI2_CLK_DISABLE(); setMode(GPIO_MODE_INPUT);
}

Здесь есть несколько интересных особенностей:

  • Используемые прерывания в данном случае определены как статические константы. Это минус к переносимости на другие контроллеры.
  • Подобная организация кода позволяет гарантировать, что пины порта всегда, когда нет передачи, находятся в состоянии GPIO_MODE_INPUT. Это плюс.
  • Приоритетность прерываний передаётся извне, то есть имеется хорошая возможность задать в одном месте основного кода карту приоритетов прерываний. Это тоже плюс.
  • Процедура stop отключает тактирование DMA1. В данном случае это упрощение может иметь очень негативные последствия, если кто-то другой продолжает использовать DMA1. Проблема решается созданием централизованного регистра потребителей подобных устройств, который и будет отвечать за тактирование.
  • Ещё одно упрощение — процедура start не приводит интерфейс в изначальное состояние в случае ошибки (это минус, но легко поправимый). В то же время, ошибки логируются более подробно, что является плюсом.
  • Основной код должен при использовании этого класса перехватывать прерывания SPI2_IRQn и DMA1_Stream4_IRQn и обеспечить вызов соответствующих обработчиков processI2SInterrupt и processDmaTxInterrupt.

Основная программа

Основная программа пишется с использованием вышеописанной библиотеки достаточно просто:

int main (void)
{ HAL_Init(); IOPort defaultPortA(IOPort::PortName::A, GPIO_MODE_INPUT, GPIO_PULLDOWN); IOPort defaultPortB(IOPort::PortName::B, GPIO_MODE_INPUT, GPIO_PULLDOWN); IOPort defaultPortC(IOPort::PortName::C, GPIO_MODE_INPUT, GPIO_PULLDOWN); // System frequency 168MHz System::ClockDiv clkDiv; clkDiv.PLLM = 16; clkDiv.PLLN = 336; clkDiv.PLLP = 2; clkDiv.PLLQ = 7; clkDiv.AHBCLKDivider = RCC_SYSCLK_DIV1; clkDiv.APB1CLKDivider = RCC_HCLK_DIV8; clkDiv.APB2CLKDivider = RCC_HCLK_DIV8; clkDiv.PLLI2SN = 192; clkDiv.PLLI2SR = 2; do { System::setClock(clkDiv, FLASH_LATENCY_3, System::RtcType::RTC_EXT); } while (System::getMcuFreq() != 168000000L); MyApplication app; appPtr = &app; app.run();
}

Устанавливаем частоту контроллера, запускаем тактирование (включая часы реального времени от внешнего кварца). Здесь мы инициализируем библиотеку HAL, все пины контроллера конфигурируем по умолчанию на вход (GPIO_MODE_INPUT/PULLDOWN). После этого, немного в стиле Java, создаём экземпляр нашего приложения и вызываем его метод run, который реализует всю логику приложения.

Так как мы пишем на C++, а прерывания — это вещи из мира C, то их нужно соответственно маскировать: Отдельной секцией мы должны определить все используемые прерывания.

extern "C"
{
void SysTick_Handler (void)
{ HAL_IncTick(); if (appPtr != NULL) { appPtr->getRtc().onMilliSecondInterrupt(); }
} void DMA2_Stream3_IRQHandler (void)
{ Devices::SdCard::getInstance()->processDmaRxInterrupt();
} void DMA2_Stream6_IRQHandler (void)
{ Devices::SdCard::getInstance()->processDmaTxInterrupt();
} void SDIO_IRQHandler (void)
{ Devices::SdCard::getInstance()->processSdIOInterrupt();
} void SPI2_IRQHandler(void)
{ appPtr->getI2S().processI2SInterrupt();
} void DMA1_Stream4_IRQHandler(void)
{ appPtr->getI2S().processDmaTxInterrupt();
} void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *channel)
{ appPtr->processDmaTxCpltCallback(channel);
} ... }

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

class MyApplication : public RealTimeClock::EventHandler, class MyApplication : public RealTimeClock::EventHandler, WavStreamer::EventHandler, Devices::Button::EventHandler
{
public: static const size_t INPUT_PINS = 8; // Number of monitored input pins private: UsartLogger log; RealTimeClock rtc; IOPin ledGreen, ledBlue, ledRed; PeriodicalEvent heartbeatEvent; IOPin mco; // Interrupt priorities InterruptPriority irqPrioI2S; InterruptPriority irqPrioEsp; InterruptPriority irqPrioSd; InterruptPriority irqPrioRtc; // SD card IOPin pinSdPower, pinSdDetect; IOPort portSd1, portSd2; SdCard sdCard; bool sdCardInserted; // Configuration Config config; // ESP Esp11 esp; EspSender espSender; // Input pins std::array<IOPin, INPUT_PINS> pins; std::array<bool, INPUT_PINS> pinsState; // I2S2 Audio I2S i2s; AudioDac_UDA1334 audioDac; WavStreamer streamer; Devices::Button playButton; ...

В конструкторе класса MyApplication необходимо вызвать конструкторы всех устройств, после чего, к моменту запуска процедуры run, все используемые устройства микроконтроллера будут инициализированы: То есть, по сути, все используемы устройства декларируются статически, что потенциально ведёт к увеличению используемой памяти, но сильно упрощает доступ к данным.

MyApplication::MyApplication () : // logging log(Usart::USART_1, IOPort::B, GPIO_PIN_6, GPIO_PIN_7, 115200), // RTC rtc(), ledGreen(IOPort::C, GPIO_PIN_1, GPIO_MODE_OUTPUT_PP), ledBlue(IOPort::C, GPIO_PIN_2, GPIO_MODE_OUTPUT_PP), ledRed(IOPort::C, GPIO_PIN_3, GPIO_MODE_OUTPUT_PP), heartbeatEvent(rtc, 10, 2), mco(IOPort::A, GPIO_PIN_8, GPIO_MODE_AF_PP), // Interrupt priorities irqPrioI2S(6, 0), // I2S DMA interrupt priority: 7 will be also used irqPrioEsp(5, 0), irqPrioSd(3, 0), // SD DMA interrupt priority: 4 will be also used irqPrioRtc(2, 0), // SD card pinSdPower(IOPort::A, GPIO_PIN_15, GPIO_MODE_OUTPUT_PP, GPIO_PULLDOWN, GPIO_SPEED_HIGH, true, false), pinSdDetect(IOPort::B, GPIO_PIN_3, GPIO_MODE_INPUT, GPIO_PULLUP), portSd1(IOPort::C, /* mode = */GPIO_MODE_OUTPUT_PP, /* pull = */GPIO_PULLUP, /* speed = */GPIO_SPEED_FREQ_VERY_HIGH, /* pin = */GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12, /* callInit = */false), portSd2(IOPort::D, /* mode = */GPIO_MODE_OUTPUT_PP, /* pull = */GPIO_PULLUP, /* speed = */GPIO_SPEED_FREQ_VERY_HIGH, /* pin = */GPIO_PIN_2, /* callInit = */false), sdCard(pinSdDetect, portSd1, portSd2), sdCardInserted(false), // Configuration config(pinSdPower, sdCard, "conf.txt"), //ESP esp(rtc, Usart::USART_2, IOPort::A, GPIO_PIN_2, GPIO_PIN_3, irqPrioEsp, IOPort::A, GPIO_PIN_1), espSender(rtc, esp, ledRed), // Input pins pins { { IOPin(IOPort::A, GPIO_PIN_4, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_5, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_6, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::A, GPIO_PIN_7, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::C, GPIO_PIN_4, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::C, GPIO_PIN_5, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::B, GPIO_PIN_0, GPIO_MODE_INPUT, GPIO_PULLUP), IOPin(IOPort::B, GPIO_PIN_1, GPIO_MODE_INPUT, GPIO_PULLUP) } }, // I2S2 Audio Configuration // PB10 --> I2S2_CK // PB12 --> I2S2_WS // PB15 --> I2S2_SD i2s(IOPort::B, GPIO_PIN_10 | GPIO_PIN_12 | GPIO_PIN_15, irqPrioI2S), audioDac(i2s, /* power = */ IOPort::B, GPIO_PIN_11, /* mute = */ IOPort::B, GPIO_PIN_13, /* smplFreq = */ IOPort::B, GPIO_PIN_14), streamer(sdCard, audioDac), playButton(IOPort::B, GPIO_PIN_2, GPIO_PULLUP, rtc) { mco.activateClockOutput(RCC_MCO1SOURCE_PLLCLK, RCC_MCODIV_5); }

В качестве примера обработчик события нажатия кнопки, по которой запускается/останавливается воспроизведение WAV файла:

virtual void MyApplication::onButtonPressed (const Devices::Button * b, uint32_t numOccured) { if (b == &playButton) { USART_DEBUG("play button pressed: " << numOccured); if (streamer.isActive()) { USART_DEBUG(" Stopping WAV"); streamer.stop(); } else { USART_DEBUG(" Starting WAV"); streamer.start(AudioDac_UDA1334::SourceType:: STREAM, config.getWavFile()); } } }

Ну и, наконец, основной метод run завершает настройку устройств (например, устанавливает MyApplication в качестве обработчика событий), и запускает бесконечный цикл, где периодически обращается к тем устройствам, которые требуют периодического внимания:

void MyApplication::run () { log.initInstance(); USART_DEBUG("Oscillator frequency: " << System::getExternalOscillatorFreq() << ", MCU frequency: " << System::getMcuFreq()); HAL_StatusTypeDef status = HAL_TIMEOUT; do { status = rtc.start(8 * 2047 + 7, RTC_WAKEUPCLOCK_RTCCLK_DIV2, irqPrioRtc, this); USART_DEBUG("RTC start status: " << status); } while (status != HAL_OK); sdCard.setIrqPrio(irqPrioSd); sdCard.initInstance(); if (sdCard.isCardInserted()) { updateSdCardState(); } USART_DEBUG("Input pins: " << pins.size()); pinsState.fill(true); USART_DEBUG("Pin state: " << fillMessage()); esp.assignSendLed(&ledGreen); streamer.stop(); streamer.setHandler(this); streamer.setVolume(1.0); playButton.setHandler(this); bool reportState = false; while (true) { updateSdCardState(); playButton.periodic(); streamer.periodic(); if (isInputPinsChanged()) { USART_DEBUG("Input pins change detected"); ledBlue.putBit(true); reportState = true; } espSender.periodic(); if (espSender.isOutputMessageSent()) { if (reportState) { espSender.sendMessage(config, "TCP", config.getServerIp(), config.getServerPort(), fillMessage()); reportState = false; } if (!reportState) { ledBlue.putBit(false); } } if (heartbeatEvent.isOccured()) { ledGreen.putBit(heartbeatEvent.occurance() == 1); } } }

Немного экспериментов

Его максимальная частота — 168 MHz. Интересный факт — микроконтроллер поддаётся частичному оверклокингу. Но при этом контроллер зависает, если использовать USART или I2S, что, быть может, просто программная проблема на уровне HAL. Однако, играя параметрами тактирования, мне удавалось запускать его на 172 MHz и на 180 MHz, то есть инициализация тактирования с такой частотой выполняется без ошибок, и эта частота видна на осциллографе, если его подключить к пину выходного тактового сигнала MCO.

Цена

На github есть список всех компонент платы. Это самый больной вопрос во всей этой работе. На получившуюся цифру в 37 Евро без слёз смотреть не получается. Чтобы получить хоть какую-то цифру, я скрупулёзно собрал цены всех компонент из этого списка с сайта Mouser (не сочтите за рекламу). То есть, по сравнению с массовыми демонстрационными платами от STM или Olimex, моя плата получилась очень уж дорогой. К ней нужно ещё прибавить стоимость изготовления платы и время на пайку.

Проблемы и перспективы

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

  • Полигон земли высокочастотного кварца разведён с ошибкой (выведен на основную землю). Вместо этого, его бы соединить с земляной ногой микроконтроллера, но вот только подобраться к ней на двухслойной плате не пока получилось.
  • Эксперименты с осциллографом показали, что линия питания контроллера очень сильно зашумлена. Дополнительный входной фильтрующий конденсатор на 47 μF ситуацию улучшает. Наверное, нужно будет его добавить.
  • Расположение разъёма SWD для программирования контроллера не очень удобное. Если сверху примостить какую-нибудь плату расширения, то до него не добраться. Нужно переносить в другое место.
  • Расположение трёхцветного светодиода также неудачное. Хочу заменить его на SMD светодиод, вынести на край платы.

Документация

Проект опубликован на github под лицензией GPL v3:

Спасибо за внимание!


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

«ПК», но не «персональный компьютер»: интервью с программным комитетом Joker

Кнопочки на сайте видны всем — но при взгляде со стороны можно никогда не задуматься о бэкенде, без которого эти кнопочки не работали бы. Любому Java-разработчику понятно, сколько важного остаётся незаметным для конечного пользователя. «Фронтендом» можно считать спикеров: они видны ...

Мастер осанки: остаться в живых

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