© 2023 DiyTronic

Пишем драйвер устройства для Zephyr RTOS

Я тут боле-менее разобрался с написанием драйверов для Zephyr-а. Решил поделиться своим опытом. Информации об этом не так, чтобы очень много, поэтому думаю кому-то это может пригодиться. Если вам эта тема интересна, то значит вы попали по адресу.

Как использовать драйвер в коде

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

/* подключаем описание API драйвера */
#include "led_driver_api.h";

void main(void)
{
    /* получаем указатель на конфигурацию драйвера */
    struct device *dev = device_get_binding("LED-DRIVER");

    /* получаем ссылку на API драйвера*/
    const struct led_driver_api *api = dev->driver_api;

    /* вызываем доступные драйверу методы API */
    api->led_on(dev);
    api->toggle(dev, TRUE);
}

Зачем так сложно и почему просто не подёргать светодиод обращаясь к портам напрямую? Если проект невелик, то вопрос вполне резонный и я даже отвечу, что вы правы — ни к чему такое усложнение. Но если проект довольно сложный то очень легко получить конфликтную или ошибочную ситуацию да и просто запутаться в коде.

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

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

А на каком выводе контроллера зажжётся светодиод?

В вышеприведённом коде совершенно неясно на какой-же ноге зажжётся светодиод. Как-же быть? Есть варианты.

Захардкодить прямо в драйвере

Тупейший метод — прошить нужный пин жестко в драйвере и пусть разработчик просто вешает всё на нужные выводы. Несмотря на кажущуюся глупость метод вполне имеет право на жизнь. Некоторые контроллеры например имеют аппаратную поддержку некоторых функций и только на определённых пинах. Ну например АЦП может быть только на определённой ноге. И тут уже ограничение накладывается именно железом, а не прихотями разработчика драйвера.

Добавить метод в API

Вполне себе тоже метод — просто в API добавляем нужный метод типа set_pin(dev, <номер вывода>) и драйвер будет использовать эти данные для работы.

Так-же для хранения неких конфигурационных данных в структуре device *dev в коде выше есть поле config. У каждого драйвера там хранится своя информация, поэтому чтобы знать, что и куда там складывать нужно изучить заголовочный файл драйвера и поискать информацию там.

/* подключаем описание API драйвера */
#include "led_driver_api.h";

void main(void)
{
    /* получаем указатель на конфигурацию драйвера */
    struct device *dev = device_get_binding("LED-DRIVER");

    dev->config->pin = <номер пина>; // тут прописывам нужный пин
    ...
}

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

Прописать в файл настроек приложения

Zephyr поддерживает систему конфигурирования Kconfig, используемую так-же в частности ядром Linux. Фактически это некий файл конфигурации, состоящих из набора переменных ключ => значение. Во время сборки все они подставляются в код в виде переменных. Очевидно, что в случае большого проекта количество переменных будет довольно большим и управлять ими будет сложно, поэтому система Kconfig имеет графический интерфейс для управления этими переменными.

Собственно настройка проекта через Kconfig и является наиболее кошерным методом настройки проекта. В данном примере мы как раз видим опции драйвера DHT11/22. Тут как раз можно во первых включить в код поддержку данного драйвера (в данном случае это была галочка на предыдущем экране), выбрать GPIO, номер порта, имя драйвера и т. п.

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

void main(void)
{
    /* получаем указатель на конфигурацию драйвера */
    struct device *dev = device_get_binding(CONFIG_DHT_NAME);

Ну вот в целом и всё про использование драйверов. Подробнее про каждый драйвер можно узнать изучив исходники в папке drivers кода Zephyr RTOS. Там есть и API и опции Kconfig. Ну, а в готовом проекте уже можно поднастроить какие-то опции драйвера из графической оболочки.

В следующей статье начну писать конкретный драйвер устройства ну и покажу на примере как это делается.

Комментарии