Драйверы GPIO, UART и ADC в экосистеме Zephyr

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

Мы переходим к самому интересному — оживлению железа. В Arduino вы привыкли к digitalWrite(): одной команде, которая прячет внутри всю магию. В Zephyr RTOS подход профессиональный: вы получаете полный контроль над ресурсами, но взамен соблюдаете строгий регламент работы с периферией.

От «магии» к системному подходу

В Zephyr нельзя просто отправить сигнал на «пин 13». Операционная система управляет ресурсами через Дескриптор устройства (device handle).

Дескриптор устройства — это программный «ключ» или ссылка на конкретный модуль (GPIO, UART, ADC), который ОС подготовила к работе. Чтобы взаимодействовать с железом, вы сначала запрашиваете у системы этот дескриптор.

Алгоритм работы с любым драйвером всегда состоит из трех шагов:

  1. Kconfig (prj.conf): включаете поддержку нужного типа драйвера.
  2. Device Tree (.overlay): привязываете функции к конкретным ножкам чипа nRF52840.
  3. Код C: получаете дескриптор и вызываете функции API драйвера.

GPIO: Управление цифровыми пинами

Для работы с GPIO в Zephyr используется структура gpio_dt_spec. Она упаковывает в себя всё: порт, номер пина и настройки из Device Tree.

Как делать не стоит

Избегайте «магических чисел» (hardcode) в коде. Это делает программу хрупкой:

// Плохо: при смене платы придется переписывать код везде
gpio_pin_configure(dev, 13, GPIO_OUTPUT_ACTIVE); 

Правильный подход в Zephyr

Сначала активируйте GPIO в prj.conf: CONFIG_GPIO=y

Затем используйте безопасный метод получения данных из Device Tree:

#include <zephyr/drivers/gpio.h>

// Извлекаем настройки светодиода по его псевдониму (alias) из Device Tree
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);

void main(void) {
    // 1. Проверяем готовность драйвера — это страховка от сбоев
    if (!device_is_ready(led.port)) {
        return;
    }

    // 2. Конфигурируем пин на выход
    gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);

    while (1) {
        gpio_pin_toggle_dt(&led); // Инвертируем состояние
        k_msleep(500);
    }
}

Совет: Всегда используйте device_is_ready(). Если вы забыли включить драйвер в Kconfig или ошиблись в Device Tree, эта проверка предотвратит «зависание» или непредсказуемое поведение прошивки. 🛡️


UART: Обмен данными

В nRF52840 модуль UART незаменим для логов и связи с внешними устройствами.

Для активации в prj.conf добавьте: CONFIG_SERIAL=y CONFIG_UART_INTERRUPT_DRIVEN=y (если нужны прерывания)

Функция printk() в Zephyr заменяет привычный Serial.print(). Но для прямой передачи данных используйте API драйвера UART:

#include <zephyr/drivers/uart.h>

// Получаем дескриптор конкретного модуля UART
const struct device *uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart0));

void send_uart_data(const char *msg) {
    for (int i = 0; i < strlen(msg); i++) {
        // Побайтовая передача через драйвер
        uart_poll_out(uart_dev, msg[i]);
    }
}

ADC: Чтение аналоговых сигналов

Работа с ADC в nRF52840 требует настройки точности: разрешения и времени накопления заряда.

В prj.conf добавьте: CONFIG_ADC=y

Чтение напряжения

У nRF52840 12-битное разрешение. Чтобы перевести «сырые» данные (RAW) в милливольты, используйте формулу:

ResultmV=ADCrawVref2ResolutionResult_{mV} = \frac{ADC_{raw} \cdot V_{ref}}{2^{Resolution}}

Обычно Vref=600V_{ref} = 600 мВ (внутренняя опора с делителем 1/6).

#include <zephyr/drivers/adc.h>

// Настройка канала
static const struct adc_channel_cfg channel_cfg = {
    .gain = ADC_GAIN_1_6,
    .reference = ADC_REF_INTERNAL,
    .acquisition_time = ADC_ACQ_TIME_DEFAULT,
    .channel_id = 0,
    .input_positive = SAADC_CH_PSELP_PSELP_AnalogInput0 // Пин AIN0
};

void read_battery() {
    int16_t buf;
    struct adc_sequence sequence = {
        .channels    = BIT(0),
        .buffer      = &buf,
        .buffer_size = sizeof(buf),
        .resolution  = 12,
    };

    const struct device *adc_dev = DEVICE_DT_GET(DT_NODELABEL(adc));
    adc_read(adc_dev, &sequence); 
    // В buf теперь значение от 0 до 4095
}

Чек-лист: почему не работает драйвер?

Если периферия «молчит», проверьте цепочку по списку:

ЭтапЧто проверитьТипичная ошибка
KconfigФайл prj.confЗабыли CONFIG_XXX=y
Device TreeФайл .overlayОшибка в номере пина или пропущен status = "okay";
ИнициализацияКод CНе вызван или не прошел device_is_ready()
APIКод CВызов функций, не поддерживаемых этим драйвером

Сейчас наш код выполняется линейно, как в Arduino. Но nRF52840 умеет гораздо больше. В следующей теме мы научимся распределять задачи между независимыми потоками и использовать системные таймеры, чтобы создать настоящую многозадачную систему. 🚀