Использование mbed кода в собственном проекте на STM32 — опыт разгона китайского LCD

Иногда чужой код очень помогает в деле подключения к микроконтроллеру периферийного железа. К сожалению, адаптировать чужой код к своему проекту бывает сложнее, чем переписать его самому, особенно если речь идет о мега фреймворках вроде arduino или mbed. Желая подключить китайский LCD на базе ILI9341 к плате STM32L476G DISCOVERY, автор задался целью воспользоваться в демо-проекте от ST драйвером, написанным для mbed, не изменив ни строчки в его коде. В результате удалось заодно разогнать экран до невиданных скоростей обновления в 27 fps.

e3a44f7c39d643e29471ee044ea6ea38.jpg

Введение в проблему


ST Microelectronics производит весьма интересные, как по возможностям, так и по цене, микроконтроллеры, а также выпускает платы для быстрой разработки. Про одну из них и пойдет речь — STM32L476G DISCOVERY. Вычислительные возможности этой платы весьма радуют — 32 битный ARM с максимальной тактовой частотой 80MHz может выполнять операции с плавающей точкой. При этом он способен снижать энергопотребление до минимума и работать от батарейки, ожидая возможности сделать чего то полезное. К этому устройству я решил подключить дешевый китайский цветной LCD с разрешением 320 на 240, работающий по SPI интерфейсу. Про то, как использовать его с mbed, подробно написано здесь. Mbed — это онлайн среда программирования, где вы можете скомпилировать себе прошивку вообще не имея компилятора на своем компьютере, после чего скачать ее и прошить, просто скопировав на свою mbed-совместимую плату, которая при подключении к USB выглядит как сменный диск. Все это замечательно, но есть несколько проблем. Во-первых, не все платы mbed-совместимы. Во-вторых, есть много уже существующих проектов, которые с mbed не совместимы вообще никак, в том числе софт, поставляемый ST. Ну и наконец, не все разработчики совместимы с mbed, некоторые (например автор этих строк) находят в этом чудесном инструменте больше недостатков, чем преимуществ. Каковы эти недостатки, мы обсудим ниже, пока достаточно упомянуть, что после подключения драйвера дисплея к демо-проекту от ST и нескольких простых оптимизаций он стал работать быстрее примерно в 10 раз.

Изучаем код драйвера


Настало время скачать и изучить исходный код драйвера дисплея. Работа с портами в mbed организована через вызовы методов классов, представляющих порты ввода-вывода. Например, класс DigitalOut реализует доступ к порту вывода. Присвоение экземпляру этого объекта нуля или единицы инициирует запись соответствующего бита в выходной порт. Инициализация класса DigitalOut производится перечислимым типом PinName, единственное назначение которого — идентифицировать ножку процессора. Один из главных недостатков реализации DigitalOut и других классов, реализующих ввод-вывод — то, что инициализация порта происходит в конструкторе экземпляра класса. Это отлично подходит для моргания светодиодом, если экземпляр класса DigitalOut создан на стеке в функции main. Но представим себе, что у нас много разнообразного железа, инициализация которого разбросана по нескольким модулям. Если мы сделаем экземпляры наших классов ввода-вывода статическими переменными, то потеряем всякий контроль над инициализацией, поскольку она будет происходить до функции main и в произвольном порядке. Библиотеки ST (они называются HAL — hardware abstraction level) используют другую, более гибкую, парадигму. Каждый порт ввода-вывода имеет свой контекст и набор функций, которые с ним работают, но их можно вызывать именно тогда, когда это необходимо. Контексты портов обычно создаются как статические переменные, но никакой автоматической неконтролируемой инициализации при этом не происходит (библиотеки ST написаны на C). Стоит также упомянуть чрезвычайно удобную утилиту CubeMX, которая может для нужного вам набора портов сгенерировать весь необходимый код инициализации и даже позволяет вам впоследствии вносить изменения в этот набор портов не затрагивая вашего собственного кода. Ее единственный недостаток — невозможность использования с уже существующими проектами, вы должны начать проект с использования этой утилиты.

Библиотека mbed для инициализации ресурсов микроконтроллера использует те же функции HAL из библиотеки ST, но делает это местами поразительно бездумным образом. Чтобы убедится в этом, достаточно посмотреть код инициализации SPI порта (который нам понадобится для работы с дисплеем) в файле spi_api.c. Функция spi_init сначала ищет подходящий SPI порт по ножкам, которые он будет использовать, а затем вызывает функцию init_spi, которая собственно и инициализирует порт. При этом на все 3 возможных SPI порта используется одна статическая структура контекста

static SPI_HandleTypeDef SpiHandle;


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

Подключаем библиотеку к своему проекту


Итак писать весь код на mbed я не хочу. Мне гораздо больше нравятся примеры от ST, которые поставляются в составе CubeMX. Готового драйвера моего LCD для библиотек ST я не нашел, писать его самому желания не возникло. Остался альтернативный способ как то развлечься — подключить драйвер, написанный для mbed, причем так, чтобы в нем не потребовалось ничего менять. Для этого нужно всего то — реализовать библиотеки mbed альтернативным способом. На самом деле задача проще, чем кажется, поскольку из всех библиотек mbed драйвер LCD использует только порт вывода и SPI. Кроме этого, ему нужны функции формирования задержек и классы файлов и потоков. С последними все просто — они нам не нужны и заменяются заглушками, которые не делают ничего. Функции формирования задержек написать легко — они в файле wait_api.h. Реализация классов ввода-вывода требует чуть более творческого подхода. Мы собираемся исправить недостаток библиотек mbed и не делать инициализацию железа в конструкторе. Конструктор будет получать ссылку на контекст порта, расположенный где то еще, код его инициализации будет совершенно независим от наших интерфейсных классов. Есть единственный способ передать эту информацию в конструктор, не меняя код драйвера, — через PinName, который вместо простого перечисления ножек будет отныне хранить указатель на порт, номер ножки, а также опционально указатель на ресурс (вроде SPI), к которому подключена эта ножка.

class PinName {
public:
        PinName() : m_port(0), m_pin(0), m_obj(0) {}
        PinName(GPIO_TypeDef* port, unsigned pin, void* obj = 0)
                : m_port(port), m_pin(pin), m_obj(obj)
                {
                        assert_param(m_port != 0);
                }

        GPIO_TypeDef* m_port;
        unsigned      m_pin;
        void*         m_obj;

        static PinName not_connected;
};

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

class DigitalOut {
public:
        DigitalOut(GPIO_TypeDef* port, unsigned pin)
                : m_port(port), m_pin(pin)
                {
                        assert_param(m_port != 0);
                }
        DigitalOut(PinName const& N)
                : m_port(N.m_port), m_pin(N.m_pin)
                {
                        assert_param(m_port != 0);
                }
        void operator =(int bit) {
                if (bit) m_port->BSRR = m_pin;
                else     m_port->BRR  = m_pin;
        }
private:
        GPIO_TypeDef* m_port;
        unsigned      m_pin;
};

Код реализации SPI порта ненамного сложнее, его можно посмотреть тут. Поскольку мы отделили инициализацию порта от интерфейсного кода, запросы на изменение конфигурации мы игнорируем. Разрядность слова просто запоминаем. Если пользователь желает передать 16 битное слово, а порт сконфигурирован как 8 битный, то нам достаточно просто переставить местами байты и передать их один за другим — в буфер порта все равно помещается до 4 байт. Все файлы, необходимые для компиляции драйвера, собраны в директории compat. Теперь можно подключить оригинальные файлы драйвера к проекту и скомпилировать их. Нам понадобится также код, который проинициализирует порты, создаст экземпляр драйвера и нарисует на экране что то осмысленное.

Разгон


Если LCD используется для вывода чего то динамического, то возникает естественное желание сделать общение с ним более быстрым. Первое, что приходит в голову — повысить тактовую частоту SPI, которую драйвер выставляет в 10MHz, но мы его пожелания игнорируем и можем выставить какую угодно. Оказалось, что экран прекрасно работает и на частоте 40MHz — это максимальная частота, на которую способен наш процессор с тактовой частотой 80MHz. Для оценки производительности был написан простой код, который в цикле выводит битмапку 100×100 пикселей. Результат потом экстраполировался на весь экран (битмапка, занимающая весь экран, в память просто не помещается). Результат — 11fps весьма далек от теоретического предела в 32fps, который получается, если передавать 16 бит на каждый пиксел без остановок. Причина становится понятна, если заглянуть в исходный код драйвера. Если ему нужно передать последовательность байт, он просто передает их один за другим, в лучшем случае упаковывая в 16 битные слова. Причина такого неэффективного дизайна кроется в mbed API. SPI класс имеет метод для передачи массива данных, но он может использоваться только асинхронно, вызывая функцию нотификации по завершению, причем в контексте обработчика прерывания. Неудивительно, что этим методом мало кто пользуется. Я дополнил свою реализацию класса SPI функцией, которая передает буфер и ожидает окончания передачи. После того, как я добавил вызов этой функции в код передачи битмапки, производительность возросла до 27fps, что уже весьма близко к теоретическому пределу.

Исходный код


Лежит тут. Для компиляции использовался IAR Embedded Workbench for ARM 7.50.2. За основу взят код демонстрационной прошивки от ST. Описание пинов, к которым подключается LCD можно найти в файле lcd.h.

© Geektimes