image Добрый день, уважаемые хабровчане! Хочу представить общественности мой проект — небольшая отладочная плата на базе STM32, но в форм-факторе Raspberry Pi. От других отладочных плат она отличается тем, что имеет совместимую с корпусами от Raspberry Pi геометрию и наличие ESP8266-модуля в качестве беспроводного модема. А также приятные дополнения в виде разъёма для micro-SD карты и стерео-усилителя. Для использования всего этого богатства я разработал высокоуровневую библиотеку и демонстрационную программу (на 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
    {
    public:

        virtual void onButtonPressed (const Button *, uint32_t numOccured) =0;
    };

    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;
}

По этой же схеме реализованы процедуры enableClock и disableClock, что плохо расширяемо и плохо переносимо на другие контроллеры. В данном случае лучше использовать шаблоны, где параметром шаблона являются HAL имя интерфейса (SPI1, SPI2, SPI3), параметры пинов (GPIO_AF5_SPI1), и что-то, что управляет включением/выключением тактирования. Здесь есть интересная статья по этой теме, хотя в ней рессматриваются контроллеры 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-контроллера. I2S (Inter-IC Sound) — это программно-аппаратная надстройка над SPI, которая сама, например, осуществляет подбор тактовой частоты и управление каналами в зависимости от аудио-протокола и битрейта.


В данном случае, класс I2S наследуется от класса «порт», то есть I2S — это порт со специальными свойствами. Некоторые данные хранятся в структурах HAL (плюс к удобству, минус к объёму данных). Некоторые данные передаются из основного кода по ссылкам (например, структура 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. Однако, играя параметрами тактирования, мне удавалось запускать его на 172 MHz и на 180 MHz, то есть инициализация тактирования с такой частотой выполняется без ошибок, и эта частота видна на осциллографе, если его подключить к пину выходного тактового сигнала MCO. Но при этом контроллер зависает, если использовать USART или I2S, что, быть может, просто программная проблема на уровне HAL.


Цена


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


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


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


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

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


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



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

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


  1. IronHead
    04.06.2018 00:36
    +1

    Да, но это не форм-фактор Raspberry Pi. Разъемы GPIO расположены не так (опять же порядок пинов не соответствует таковому в Raspbery), нет порта дисплея, нет Ethernet и USB A Female разъемов на своих местах, хотя STM32F4 это все умеет.
    PS Фото форм фактора Raspbery PI 3 для примера
    image


    1. Shamrel
      04.06.2018 04:35
      +1

      Автор ставил задачу уместиться в корпус малинки, а не сделать совместимую по выводам.


      1. Mike_soft
        04.06.2018 10:12

        «форм-фактор» — это не только «уместиться в корпус».


        1. mkulesh Автор
          04.06.2018 11:07

          Я бы не стал так категорично обобщать. Если мы говорим, к примеру, о форм-факторе mini-ITX,
          то сравните, пожалуйста, две платы: эту
          и эту.
          Другое дело, что Raspberry Pi — это не только форма, но и много интересных плюшек. На данном этапе, я просто не стал реализовывать их все.


      1. devprodest
        04.06.2018 22:13

        Учитывая большую свободную площадь на плате — слишком мало переферии и действительно ничего не мешало сделать…


    1. mkulesh Автор
      04.06.2018 10:49

      Я в самом начале работы думал над этим и с Вами согласен, но только частично. Например, RPi3 имеет на борту аж 4 USB разъёма. Лично я не уверен, что 4 разъёма действительно нужны на подобной плате. Проводная сеть нужна, наверное. Но тут нужно иметь ввиду, что придётся тянуть в код весь TCP/IP стек, чтобы его использовать. Но, как бонус, можно сделать PoE. Сдвоенная гребёнка GPIO на 40 контактов тоже не совсем практична, так как у контроллера в корпусе LQFP64 40 свободных пинов для неё наберётся с трудом. В будущем я думаю сделать вариант с LQFP100, тогда можно будет сделать GPIO, как на RPi3.


      1. IronHead
        04.06.2018 11:05

        USB можно было бы и 1 вывести (USB Host) но на то же место, где он у Raspberry. Ethernet поднимается легко, если использовать LwIP. Но желательно к Eth добавить еще и внешнюю RAM. А вот гребенка GPIO на 40 контактов — это то, что сделало бы плату совместимой с уже готовыми модулями для Raspberry (но для этого необходимо сделать вывод аппаратных интерфейсов SPI, I2C и пр на те же пины, что у малинки). Я бы взял для этого всего STM32F407IGT6 у нее и ног хватит и интерфейсы все есть.


        1. mkulesh Автор
          04.06.2018 14:10

          Вы предлагаете уже высший пилотаж — LQFP-176. Мне хотя бы на LQFP-100 перейти, уже будет большим достижением для меня.


          1. IronHead
            04.06.2018 14:13

            А в чем существенная разница? Шаг выводов у них одинаковый, платы у вас с маской, то есть при пайке разницы никакой не будет.


            1. mkulesh Автор
              04.06.2018 15:25

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


              1. proton17
                04.06.2018 21:35

                Так в чем проблема? На jlcpcb.com 10шт. таких плат в 32$ встанут, еще за 7 сверху и трафарет для пасты сделают)


              1. devprodest
                05.06.2018 07:20

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


        1. avf1906
          04.06.2018 15:15
          +1

          Тогда уж STM32F746 или STM32L4Rxx — существенно больше возможностей, кроме того последние содержат 640к RAM, что практически полностью снимает потребность во внешней RAM


          1. mkulesh Автор
            04.06.2018 15:29

            Я тоже на эту серию (F476) уже поглядываю. Если переходить на 100-ножечный вариант, то этот контроллер, действительно, неплохой выбор.


            1. Elmot
              04.06.2018 22:25

              Уже H7 и L4+ есть.


              1. mkulesh Автор
                04.06.2018 23:33

                Я буквально неделю назад документацию на H7 изучал. Весьма достойный контроллер, только вот цена кусается — в среднем в два раза дороже, чем F405.


                1. Sub_Dia
                  05.06.2018 08:31
                  +1

                  Да, H7 весьма современное и достойное решение (если говорить о cortex-m ядре). Богатая периферия (уже и qspi умеет, и прочее, и прочее). Мы на нем чуть проект не запилили, правда, выяснилось, что производительности нам все же не хватает, и в итоге выбрали SoC на Cortex-A9, впрочем, это совсем другая история… =))


      1. Elmot
        04.06.2018 22:25

        .


  1. da-nie
    04.06.2018 06:58

    inline void setHandler (EventHandler * _handler)


    Насколько я помню, inline при реализации метода внутри объявления класса не нужно писать — эти методы и так inline.


  1. Misaka10032
    04.06.2018 07:59

    Моё ИМХО — в принципе, можно было взять 407-ой в 100-ногом корпусе и тогда бы можно было добавить Ethernet. Ну и внешнюю оперативку заодно. Хотя в форм-фактор влезть было бы труднее.


    1. mkulesh Автор
      04.06.2018 11:15

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


  1. vin2809
    04.06.2018 08:52

    Спасибо, отличный материал.

    А опыт конкретного применения будет?


    1. mkulesh Автор
      04.06.2018 11:23
      +1

      Надеюсь, что да. У меня в доме установлены Zwave датчики на окнах, красивые, но не работают совершенно в силу большого расстояния до базы. Промежеточные усилители тоже не помогают. Хочу протянуть провода, корпуса датчиков оставить, начинку поменять, и на каждом этаже сделать свой блок на базе этой платы. Уже отлажено всё: сама плата, датчики, прошивка, серверная часть, клиент для Андроида. Осталось только провода по всему дому протянуть. То есть как всю систему запущу и настрою, так и напишу.


      1. vin2809
        04.06.2018 11:25

        Отлично, будем ждать результата.


  1. pfr46
    04.06.2018 09:02
    -1

    Что же вы везде куда не нужно пихаете этот недоязык? Тут место Форту.


    1. vin2809
      04.06.2018 09:15

      Любой объект(язык, контроллер и т.п.) будет «недо», когда нет устойчивых знаний по нему.

      Может быть вы и ОС на Форт переведете?


      1. pfr46
        04.06.2018 09:29

        Тем, кто знает больше чем С++ очевидно. что таки «недо».

        Умная мысль
        C++ — кошмарный язык. Его делает ещё более кошмарным тот факт, что множество недостаточно грамотных программистов используют его, до такой степени, что оказывается намного проще выкинуть его как мусор. Откровенно говоря, даже если нет *никаких* причин для выбора Си, кроме того чтобы держать C++-программистов подальше — то одно это уже будет достаточно веским основанием для использования Си.
        …Я пришёл к выводу, что *действительно* предпочту выгнать любого, кто предпочтёт вести разработку проекта на C++, нежели на Си, чтобы этот человек не загубил проект, в который я вовлечён.
        C++ приводит к очень, очень плохим проектным решениям. Неизбежно начинают применяться «замечательные» библиотечные возможности вроде STL, и Boost, и прочего мусора, которые могут «помочь» программированию, но порождают:
        — невыносимую боль, когда они не работают (и всякий, кто утверждает, что STL и особенно Boost стабильны и портируемы, настолько погряз во лжи, что это даже не смешно)
        — неэффективно абстрагированные программные модели, когда спустя два года обнаруживается, что какая-то абстракция была недостаточно эффективна, но теперь весь код зависит ото всех окружающих её замечательных объектных моделей, и её нельзя исправить, не переписав всё приложение.
        Другими словами, единственный способ иметь хороший, эффективный, низкоуровневый и портируемый C++ сводится к тому, чтобы ограничиться всеми теми вещами, которые элементарно доступны в Си. А ограничение проекта рамками Си будет означать, что люди его не выкинут, и что будет доступно множество программистов, действительно хорошо понимающих низкоуровневые особенности и не отказывающихся от них из-за идиотской ерунды про «объектные модели».
        … когда эффективность является первостепенным требованием, «преимущества» C++ будут огромной ошибкой.(с) Линус Торвальдс,


      1. Jef239
        04.06.2018 14:15

        Вообще-то стандартная реализация форта включает в себя ОС и IDE. Занимает это все килобайт 12 ОЗУ. Такой уж язык, создан для работы на голом железе.


    1. IronHead
      04.06.2018 09:15
      +1

      Будем благодарны, если напишите статейку: «STM32 на форте — это просто!»


    1. Sub_Dia
      04.06.2018 09:18

      Ну, почти C++. А насчет Форта — аргументируйте. =)


    1. mkulesh Автор
      04.06.2018 13:30
      +1

      Программирование — это же не только язык. Это и среда разработки, библиотеки, отладчики, документация, сообщество. В какой IDE лучше всего программировать на Форте? Под какие конкретно модели контроллеров уже есть библиотеки, где реалирована поддержка SD карт, файловая система, USB и прочие высокоуровневые протоколы и интерфейсы? Может, действительно напишите туториал, как начать программировать STM32 на Форте?


      1. pfr46
        04.06.2018 14:27

        Я бы с удовольствие написал, но увы — хронически страдаю корявостью слога. А сред разработок навалом. Для контролеров как раз больше всего на Форте написано языков. Особенно хорош шитый Форт. Он маленький и шустрый. И запрограммировать на нём можно то, что на плюсах физичеси не возможно реализовать. А для STM32 есть среда m3Forth.


        1. Armleo
          04.06.2018 14:49

          Например, что физически невозможно сделать в Сдваплюса?


          Кстати не спроста он в кассовых аппаратах, на борту Аэробуса, Хаббле и много где ещё в спутниках.

          Не аргумент.


          1. pfr46
            04.06.2018 14:52

            Нельзя расширять язык средствами самого языка. Так же у Форта есть возможность самомодификации кода.


            1. ser-mk
              06.06.2018 01:41

              Желательно примерчик, причем какой-нибудь полезный.


  1. Shtucer
    04.06.2018 09:26

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

    Какой в этом смысл? Получается, что у вас два счетчика аптайма, ваш и системный? Хорошо когда памяти много!


    1. mkulesh Автор
      04.06.2018 11:31

      Если устройство работает гарантированно меньше 49 дней, то смысла нет. Если это что-то, что должно работать в режиме 365/7/24, то Вам точно нужно периодическое переполнение системного таймера и целый спектр проблем, с этим переполением связанный?


      1. Shtucer
        04.06.2018 11:33

        О каком "спектре проблем" идёт речь?


        1. mkulesh Автор
          04.06.2018 13:22

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


          1. Shtucer
            04.06.2018 13:48
            +1

            1. mkulesh Автор
              04.06.2018 14:04

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


              1. Shtucer
                04.06.2018 14:13

                В этой "ключевой" фразе как раз и кроется причина моего вопроса: почему вместо корректной работы с системным таймером нужно городить огород с оверхедом по памяти. И, кстати, за сколько тактов инкрементится двухбайтное число? Я всё ещё не вижу никаких плюсов. Если дешифровка у вас там происходит давольно часто, а переполнение происходит раз в 49 дней… чорт, я всё ещё не вижу проблемы, которую вы решаете.


                1. mkulesh Автор
                  04.06.2018 15:20

                  Наверное, Вы правы. Похоже, я решил надуманную проблему. Ресурсы этого контроллера (частота и память) позволяют не задумываться о таких мелочах, вот я и сделал, так как мне казалось это удобным.


                  1. Shtucer
                    04.06.2018 16:04

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


                    Кстати, почему у вас везде интерфейс I2C называется I2S?


                    1. mkulesh Автор
                      04.06.2018 16:14

                      Ну, это разные интерфейсы. I2S (Inter-IC Sound) — это программно-аппаратная надстройка над SPI, которая сама, например, осуществляет подбор тактовой частоты и управление каналами в зависимости от аудио-протокола и битрейта. I2C — это более универсальная штука.


                      1. Shtucer
                        04.06.2018 16:17

                        А, понятно.


                        1. mkulesh Автор
                          04.06.2018 16:54

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


  1. AnotherReality
    04.06.2018 09:58

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


    1. mkulesh Автор
      04.06.2018 13:09

      Ваш вопрос, к сожалению, выходит за рамки моей квалификации. И по образованию, и по сфере деятельности я математик, а данный проект — это хобби. Я примерно понимаю, о чем речь в этом вопросе, но ответить на него не могу. Вот здесь лежит файл правил для EagleCad, может быть, он поможет это сказать. Если есть желание, милости прошу переработать разводку платы и закинуть merge-request. Буду только рад такому сотрудничеству!


      1. Armleo
        04.06.2018 14:52

        По другому: какова минимальная толщина линии? 0.3мм?


        1. mkulesh Автор
          04.06.2018 22:09

          Сигнальные линии 0.25мм, линии питания толше. Переходные отверстия 0.5мм.


      1. AnotherReality
        04.06.2018 15:33

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


        1. mkulesh Автор
          04.06.2018 16:18

          Ну, я сам из Германии. С учётом бесплатной доставки Mouser мне, я думаю, обойдётся дешевле, чем диджикей. А какие замены компонент на более дешёвые Вы бы могли порекомендовать?


  1. Punk_Joker
    04.06.2018 12:43

    У С++11 есть неприятная собенность, при описании структуры нельзя присваивать ее полям значение по умолчанию. В предыдущем и следующем стандартах можно. Поэтому я использую в работе С++14 (к тому же там расширенно применение constexpr)


    1. mkulesh Автор
      04.06.2018 15:17

      Я тоже уже подумываю перейти на С++14, благо sw4stm, которую я в данный момент использую, это позволяет.
      Sarcasm On. С другой стороны, тут в соседней ветке обсуждают использование Форта вместо С++, так что я и не знаю даже, что сегодня более адекватно. Sarcasm off.


      1. Punk_Joker
        04.06.2018 23:53

        Лучше использовать Attolic True Studio for STM32, намного более проработанная IDE.


        1. mkulesh Автор
          05.06.2018 11:44

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


  1. NordicEnergy
    04.06.2018 12:49

    Забавно читать про проблему с кварцем. Зачем было ставить 16 МГц? Достаточно поставить 4 или 8 МГц типа 3225 и радоваться жизни. Нужную частоту все равно получаются после PLL, а меньшая частота кварца позволила бы вообще забить на качество его разводки.
    Это просто добровольный выстрел себе в ногу…


    1. mkulesh Автор
      04.06.2018 12:56

      Кстати, Вы совершенно правы! Я, в силу отсутствия опыта, об этом просто не подумал. Спасибо за подсказку, исправлю в следующих версиях.


  1. Hoshi
    04.06.2018 12:51

    Расположение трёхцветного светодиода также неудачное. Хочу заменить его на SMD светодиод, вынести на край платы.

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


    1. mkulesh Автор
      04.06.2018 13:14

      Согласен, я здесь совершенно напрасно решил съэкономить. Это не было бы проблемой, если бы светоиод был подключен в выводам контроллера, где есть ШИМ. Но на использованных выводах ШИМа нет.


  1. gopotyr
    04.06.2018 13:34

    Хороша только как тренировка по изготовлению ПП. И совершенно бессмысленна… Разумнее всего как старт работы с STM было бы сделать что-то вроде «голубой таблетки».
    Для расширения функционала «голубой таблетки» была разработана плата
    Gerber файлы прилагаются, можете повторить.
    Исправлены ошибки: слабый стабилизатор, 1.5К на USB,…
    Для изготовления прототипов, макетирования придумано это
    Добавляем в проект то, что интересно, например модули ардуины, коих хватает.


    1. Mike_soft
      04.06.2018 13:48

      я правильно понял, что «зеленая» — это исправленная «голубая»?


      1. gopotyr
        04.06.2018 13:59

        Отчасти да. Делалось в том числе для расширения линейки МК.
        Можно впаять STM32F100, STM32F103 ( Cortex M3 ), STM32F303 ( Cortex M4 )
        В том же кубике можно подобрать совместимые по ногам 48-ми ножечные.


        1. Mike_soft
          04.06.2018 14:04

          понял, спасибо.


        1. mkulesh Автор
          04.06.2018 14:08

          Я и сам такое делал. В github репозитории, о котором я пишу в статье, есть набор и таких плат.


          1. gopotyr
            04.06.2018 14:43
            +1

            Да, я смотрел. Это по сути — переходники. Нет стабилизатора 5V — 3.3V
            Я предлагаю несколько иной взгляд. Уж коль скоро максимально распространенная это «голубая таблетка», то обеспечиваем максимальную совместимость по ногам именно с ней.
            Используя макетные паячно-беспаячные платы ( ссылку дал ), можно собирать прототипы из набора «микрокубиков». Вставляя / припаивая всё на макетные платы. Не нужно напрягаться рассудком, как и куда потом всё это «затолкать», в какой корпус.
            Нужно поменять/попробовать как будет работать с другим МК — просто поменяли «таблетку», оставив периферию на месте.
            После разработки ПО и тестирования прототипа можно «нарисовать» свою плату законченного устройства. Детали прототипа используем далее для разработки чего-то следующего. Если лень, или не целесообразно — используем как есть. Внешний вид промышленного устройства уже есть. Да и провода, в случае беспаячного монтажа хорошо прижимаются крышками корпуса.


            1. mkulesh Автор
              04.06.2018 16:44

              Лично мне такой подход тоже нравится. Я даже писал о чём-то похожем на Хабре, с той только разницей, что использовал беспаячную макетную плату. Как-то та статья не зашла. Наверное, не смог хорошо объяснить. Отчасти поэтому, отчасти из-за желания нового я пошёл по пути наращивания и усложнения функционала контроллерной платы, вместо декомпозиции на макетной.


              1. Costic
                04.06.2018 18:43

                1) В вашем микроконтроллере есть 2 ЦАП, зачем использовать внешние?
                (2?12-bit D/A converters)
                2) Усилители и схемы можно подсмотреть у STM, но мне нравится LM4863. Дело вкуса :-)
                3) Если у вас линия питания зашумлена, то я бы советовал последовательно после регулятора 3.3В поставить индуктивность (дроссель подавления ЭМ помех). Получится LC-фильтр и всё будет ровно. Схемы и компоненты можно подсмотреть в любых радиомодулях. Там качественное питание критично.
                4) У вас линия SD_DET идёт сразу в контроллер. Я бы её к питанию подтянул 100к. Тогда она не будет плавать (z-state).
                5) Линия NRST разве не должна быть к питанию подтянута через резистор?
                Удачи вам. Буду ждать от вас LQFP100 с памятью DRAM.


                1. mkulesh Автор
                  04.06.2018 21:03

                  Хотелось качественного звука. Внешний ЦАП UDA1334 — это специализированный аудио-ЦАП с разрешением до 24 бит и частотой дискретизации до 100 кГц. Кстати, на STM-Discovery также используется внешний ЦАП, но более навороченный. Спасибо за наводку на LM4863 — интересная штука, поизучаю. Хотя он выходит почти в 3 раза дороже того, что использовал я. За совет с индуктивностью спасибо — обязательно посмотрю. SD_DET я не подтянул внешним резистором, так как при инициализации контроллера я гарантированно успеваю подтянуть его внутренним резистором перед первым использованием. То есть при старте системы напряжение конечно плавает, но это не критично. По поводу NRST: задумался и проверил документ Getting started with STM32F4xxxx MCU hardware development. Вроде нет, резистора там нет, см. Figure 26. JTAG connector implementation на странице 37.


                  1. Costic
                    04.06.2018 21:20

                    В этом документе на 13 и 14 стр. нарисован резистор встроенный на 40к и подтянутый к VDD. Так что я не прав, у вас всё корректно.


    1. mkulesh Автор
      04.06.2018 13:58

      Ну, зачем же Вы так категорично? И "голубая таблетка", и Discovery, и пара Nucleo, и пара плат от Olimex у меня есть. Все они достаточно хороши. В каждой есть изюминка. Мне, например, очень нравятся Nucleo. Как старт работы с STM32 разумно использовать любую из них. Только ведь я не совсем об этом писал. Смысл проекта: нечто, что имеет WiFi, звук, SD карточку и подходит к очень распространённым корпусам от RPi. Ну и на уровне софта несколько больше, чем просто светодиодом помигать.


  1. REPISOT
    04.06.2018 13:41

    Del


  1. gopotyr
    04.06.2018 14:50
    +1

    Ну и хочу добавить, что для «железа» лицензия GPL не подходит. Это для ПО.
    Для «железа»:
    — TAPR Open Hardware License
    — CERN Open Hardware Licence
    — …


    1. mkulesh Автор
      04.06.2018 15:10

      Упс, этого я просто не знал! Спасибо за информацию, буду курить мануалы по лицензиям.


  1. barbos6
    04.06.2018 15:04

    Печально, что и на этой плате нет USB PHY, и, соответсвенно, HS не светит.
    Это сужает спектр возможных применений.


    1. mkulesh Автор
      04.06.2018 15:08

      Выше о USB тоже уже писали. Я тоже за то, чтобы его добавить. Предлагаю объединить усилия и поработать над этим совместно. Если есть желание скооперироваться, пишите в личку.


      1. IronHead
        04.06.2018 20:31
        +1

        Напишите мне, как созреете на новую версию. Готов проконсультировать по подключению внешней RAM, Ethernet и разводке платы.


        1. Costic
          04.06.2018 21:21

          А вы тут порекомендуйте внешние RAM. Всем будет интересно.


          1. Misaka10032
            05.06.2018 10:07
            +1

            А это смотря, какую хотите. Для начала — SRAM или DRAM?


            1. da-nie
              05.06.2018 10:51

              Берите сразу FRAM. Типа 1666РЕ014. :) ОЗУ и ПЗУ в одном флаконе.


            1. mkulesh Автор
              05.06.2018 11:37

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


              1. Misaka10032
                05.06.2018 15:27

                1. Сценариев очень много. Ethernet, например.
                2. Решается очень просто. В скрипте линкера заводится область, с началом в нужном банке и нужного размера. Потом всем переменным, которые во внешней памяти лежать должны, прописываете __attribute__ ((section(«секциянейм»))). В скрипте линкера дописывается что-то вроде:
                .секциянейм:
                {
                } > областьнейм
                Это в случае с GCC, но для IAR с Keil очень похоже должно быть, думаю.
                Обращаться, соответственно, можете как и к обычным переменным.
                3. У самого контроллера смотрите на FSMC, если надо только статику. За динамикой — только к FMC.
                4. Конкретные модели по памяти и не назову, увы. Это надо гуглить.


              1. Sub_Dia
                06.06.2018 09:14

                В дополнение к вышесказанному, обращу Ваше внимание на документ от SGS Thomson — это касательно LL и HAL. Там подробно описано применение и FMC (SDRAM), и FSMC (SRAM).