Как стать автором
Обновить

Embedded Linux в двух словах. Второе

Время на прочтение25 мин
Количество просмотров33K

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

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

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


По результатам прошлой части имеется плата с базовой системой и намерение добавить туда некоторый функционал, причем, чтобы при редактировании какой-либо составляющей, будь то пользовательская программа, драйвер ядра или просто настройки конфигурации, не пришлось делать множество телодвижений для сборки новой системы, в идеале же, ограничиться одной командой. Такую задачу может решить система сборки, наиболее известными являются Yocto Project, OpenWrt, Buildroot, все со своими преимуществами и недостатками, и, из перечисленных, здесь будет использоваться последняя.

Buildroot

Как говорят на официальном сайте, Buildroot это простой, эффективный, легкий в использовании инструмент для создания встраиваемых систем посредством кросскомпиляции, Buildroot для Всех говорят они, и, да, во многом всё так и есть.

Проверив зависимости (раздел 2), можно скачивать

git clone -b 2021.02 https://git.buildroot.net/buildroot

И, например, чтобы создать минимальную систему для BeagleBone Black, а там есть готовый, и не один, конфиг для этой платы, нужно этот конфиг установить и запустить сборку

cd buidlroot
make beaglebone_defconfig
make

В результате, Buildroot сам скачает нужные исходники, соберет набор инструментов для кросскомпиляции, загрузчик, ядро Linux, системные утилиты, библиотеки, вобщем все то, что упоминалось в предыдущей статье и еще сверху. Установленные beaglebone_defconfig'ом настройки можно посмотреть командой:

make menuconfig

Для вывода списка всех предустановленных конфигураций:

make list-defconfigs

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

make help

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

Для начала нужно создать папку, где будут сконцентрированы все дополнительные материалы, используемые в сборке конкретной платы, Buildroot рекомендует использовать для этого папку /board. Пусть рабочим названием платы будет "smilebrd" и теперь все что касается проекта будет находится в /board/smilebrd/

Время установить базовые настройки Buildroot для платы, все, о чем известно на данный момент.

make defconfig
make menuconfig

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

Архитектура целевой платформы, тут достаточно очевидно, ARM, Cortex-A8

Target options ---> 
	Target Architecture (ARM (little endian))
	Target Architecture Variant (cortex-A8)

Сборка, нужно указать название конфига, который будет добавлен к уже имеющимся, после выполнения savedefconfig

Build options --->
	($(CONFIG_DIR)/configs/smilebrd_defconfig) Location to save buildroot config 

В качестве набора инструментов будет использоваться, описанный в предыдущей статье, gcc-arm-10.2-2020.11-x86_64-arm-none-linux-gnueabihf, также нужно указать некоторые параметры для корректного его использования, библиотекой будет полноценная glibc - место позволяет, и добавить поддержку C++

Toolchain ---> 
  Toolchain type (External toolchain)
  Toolchain (Custom toolchain)
  Toolchain origin (Pre-installed toolchain)
  ($(HOME)/toolchain/gcc-arm-10.2-2020.11-x86_64-arm-none-linux-gnueabihf) Toolchain path
  ($(ARCH)-none-linux-gnueabihf) Toolchain prefix 
  External toolchain gcc version (10.x)
  External toolchain kernel headers series (4.20.x)
  External toolchain C library (glibc/eglibc)
  [*]Toolchain has C++ support?

В конфигурацию системы можно добавить название системы вместе с выдаваемым приветствием, и изменить подсистему инициализации на systemd, а командную оболочку на bash. Также в этом разделе можно задать скрипт, который будет запущен перед компоновкой файловой системы. Здесь он используется для копирования в директорию сборки файла uEnv.txt, о нем речь шла в предыдущей статье, а также копирования настроек для автоматического запуска пользовательского приложения, о самом приложении речь пойдет позже

System configuration ---> 
  (smile_board) System hostname
  (Welcome to smile board) System banner
  Init system (systemd)
  /bin/sh (bash)
  (board/smilebrd/post-build.sh) Custom scripts to run before creating filesystem images

post-build.sh:

#!/bin/sh
BOARD_DIR="$(dirname $0)"

cp $BOARD_DIR/uEnv.txt $BINARIES_DIR/uEnv.txt

cp $BOARD_DIR/smilebrd_serv.service $TARGET_DIR/etc/systemd/system/smilebrd_serv.service

Далее идут настройки ядра Linux. Как и с набором инструментов, будет использоваться готовое ядро, т.е. Buildroot не будет ничего качать самостоятельно, а распакует и соберет всё из исходников в указанном тарболе. Немного кастомизированна конфигурация ядра, т.к. в нее нужно добавить пункты по сборке свежеразработанных драйверов для свежеразработанного смайл пульта, еще отмечено, что нужно собрать дерево устройств и из чего собственно собирать, а также поддержка OpenSSL

Kernel --->
  [*] Linux Kernel
  Kernel version (Custom tarball) ---> Custom tarball
  (file://$(HOME)/kernel/linux-5.4.92.tar.xz) URL of custom kernel tarball 
  Kernel configuration (Using a custom (def)config file)
  (board/smilebrd/kernel_smilebrd_defconfig) Configuration file path 
  [*] Build a Device Tree Blob (DTB)
  (am335x-boneblack) In-tree Device Tree Source file names
  [*] Needs host OpenSSL

Пакеты, устанавливаемые в систему. Здесь понадобится firmware для wifi адаптера TP-LINK TL-WN725N, он мал, доступен в продаже, недорог, с обязанностями справляется, внутри содержит чип Realtek 8188EU о чем и нужно указать в конфиге, а заодно добавить connman для управления подключением к wifi. Также, для взаимодействия с чатом посредством websocket я использовал утилиту wscat, она работает на nodejs, значит нужна поддержка nodejs и пакетного менеджера с нужным модулем

Target packages --->
	Hardware handling --->
		Firmware ---> 
    	[*] linux firmware
						Wifi firmware ---> 
            	[*] Realtek 81xx
	Interpreter languages and scripting --->
  	[*] nodejs
		[*] NPM for the target
					(wscat) Additional modules
	Networking applications --->
		[*] connman
		[*] 	enable WiFi support
		[*] 	enable command line client

Файловой системой будет ext4, с максимальным размером в 500 мегабайт, размер указывается просто в качестве планки, т.к. во встроенных системах, обычно, заранее известны все характеристики оборудования, и имеет смысл заранее узнать о превышении размера памяти, отведенного под корневую файловую систему

Filesystem images --->
	[*] ext2/3/4 root filesystem
	ext2/3/4 variant (ext4) ---> ext4
	(500M) exact size

Загрузчик U-Boot, подробнее про него, где брать и как настраивать, есть в предыдущей статье. Здесь же нужно указать, что используется именно U-Boot, где он находится, путь к настройкам U-Boot. Подойдет дефолтный конфиг для AM3358, но я, в образовательных целях, внес минимальные изменения, убрав 2-х секундную задержку при загрузке, это все отличия uboot_smilebrd_defconfig от am335x_evm_defconfig. Также нужно указать формат и наименование вторичного загрузчика

Bootloaders --->
  [*] U-Boot
      Uboot Version (Custom tarball) ---> Custom tarball
      (file://$(HOME)/u-boot/u-boot-2021.01.tar.xz) URL of custom U-Boot tarball
      U-Boot configuration (Using a custom board (def)config file)--->
      (board/smilebrd/uboot_smilebrd_defconfig) Configuration file path 
  [*] U-Boot needs dtc
  U-Boot binary format ---> 
  	[*] u-boot.img
  [*] Install U-Boot SPL binary image
  	(MLO) U-Boot SPL/TPL binary image name(s)

Теперь осталось сохранить полученную конфигурацию

make savedefconfig

И, чуть позже, добавить в сборку программу для общения со смайл пультом. Но прежде чем запускать сборку Buildroot, нужно создать, указанную выше, конфигурацию ядра Linux - kernel_smilebrd_defconfig. От дефолтного omap2plus_defconfig, использованного в предыдущей статье, новый конфиг отличается добавлением поддержки wifi адаптера и смайл пульта. Если драйвер wifi адаптера в ядре есть, то смайл пульта, очевидно, нет, и время этим заняться.

Linux Device Drivers

Чтобы определиться, что должен делать драйвер, нужно несколько слов сказать о том, с чем ему придется работать, о смайл пульте.

Изделие представляет собой плату с микроконтроллером STM32L475. Камень избыточен для своих задач, но выбор пал на него только из-за наличия множества пылящихся отладок NUCLEO, имеющих похожий на борту. Смайлы являются полигонами TSC-контроллера, т.е. сенсорными кнопками, в прорезях платы светодиоды, куда ж без них, коммуникация с BeagleBone Black происходит посредством I2C, где STM выступает в роли ведомого. Еще есть UART на случай, вдруг пригодится, и, собственно, всё. Отсутствие башенки кварцевого резонатора говорит о тактировании от внутреннего RC генератора, а отсутствие стабилизатора, о питании от платы BeagleBone Black. Под капотом микроконтроллера безHAL'ьное ядро на Scm-RTOS, для интересующихся, исходники можно посмотреть здесь.

Возвращаясь к драйверу, общий функционал у него такой: при касании сенсорной кнопки, пульт генерирует импульс на сигнальном выводе, давая понять, что ему есть что передать, это провоцирует прерывание на входном GPIO в BeagleBone Black, по сигналу прерывания запускается опрос пульта по I2C, данные о сработавшей кнопке подаются в приложение, которое сопоставляет кнопку с нужным смайлом и отправляет команду в чат. В обратную сторону предусмотрен только сигнал о подключении к серверу чата, для этого сигнала отведен отдельный светодиод на пульте. Итого в драйвере должен быть задействован интерфейс I2C, один GPIO, настроенный как вход, с подключенным прерыванием и один GPIO, настроенный как выход, для управления светодиодом.

Весь драйвер выглядит так
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>           // miscellaneous character module
#include <linux/kernel.h>
#include <linux/of.h>                   // Device Tree (of - open firmware)
#include <linux/i2c.h>                  // i2c devices
#include <linux/uaccess.h>
#include <linux/platform_device.h>      // platform devices
#include <linux/gpio/consumer.h>        // GPIO descriptor
#include <linux/interrupt.h>            // IRQ
#include <linux/wait.h>

#define CMD_CONTROL_LED 0x10
#define CMD_CONTROL_LED_ARG_OFF 0x00
#define CMD_CONTROL_LED_ARG_ON 0x01

// private smilebrd structure
struct smilebrd_dev {
    struct i2c_client* i2c_dev;
    struct platform_device* gpio_dev;
    struct gpio_desc* button;
    struct gpio_desc* led;
    unsigned int irq;
    unsigned int irq_f;
    unsigned int tsc_data;

    struct miscdevice miscdevice;
    char name[8];
};

static struct smilebrd_dev* smilebrd;

static DECLARE_WAIT_QUEUE_HEAD(wq);

/**
 * @brief Device callback for device node ioctl
 */
static long smilebrd_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    // now exist only SWITCH LED command
    switch(cmd) {
    case CMD_CONTROL_LED:
        if(arg == CMD_CONTROL_LED_ARG_ON) {
            gpiod_set_value(smilebrd->led, 1);
        }
        else {
            gpiod_set_value(smilebrd->led, 0);
        }
        break;
    default:
        break;
    }

    return 0;
}

/**
 * @brief Device callback for device node read
 */
static ssize_t smilebrd_read(struct file *filp, char __user *userbuf, size_t count, loff_t *ppos)
{
    // stopping the process until an interrupt is received
    wait_event_interruptible(wq, smilebrd->irq_f != 0);

    // interrupt received and handled
    smilebrd->irq_f = 0;

    // copy received device data from kernel space to user space
    if(copy_to_user(userbuf, &smilebrd->tsc_data, sizeof(smilebrd->tsc_data)) != 0) {
        return -EIO;
    }

    return sizeof(unsigned int);
}

/**
 * @brief Input GPIO interrupt handler
 */
static irqreturn_t smilebrd_gpio_irq_handler(int irq, void* dev_id)
{
    // send i2c data
    smilebrd->tsc_data = i2c_smbus_read_byte(smilebrd->i2c_dev);

    // set irq flag
    smilebrd->irq_f = 1;
    wake_up_interruptible(&wq);

    return IRQ_HANDLED;
}

/**
 * @brief File operations structure
 */
static const struct file_operations smilebrd_fops = {
    .owner = THIS_MODULE,
    .read = smilebrd_read,
    .unlocked_ioctl = smilebrd_ioctl,
};

/**
 * @brief Install GPIO
 */
static int smilebrd_gpio_probe(struct platform_device* pdev)
{
    int retval;
    smilebrd->gpio_dev = pdev;

    // input GPIO, check signal from smile board
    smilebrd->button = gpiod_get(&smilebrd->gpio_dev->dev, "button", 0);
    gpiod_direction_input(smilebrd->button);

    // output GPIO, controls "enable server" LED
    smilebrd->led = gpiod_get(&smilebrd->gpio_dev->dev, "led", 0);
    gpiod_direction_output(smilebrd->led, 0);

    // add debounce interval to input GPIO
    retval = gpiod_set_debounce(smilebrd->button, 1000 * 5);        // time unit 1 us, 1000 us * 5 = 5 ms, MAX = 7 ms
    if(retval != 0) {
        pr_err("could not set debounce interval\n");
    }

    // add interrupt to input GPIO
    smilebrd->irq = gpiod_to_irq(smilebrd->button);
    retval = request_threaded_irq(smilebrd->irq, NULL, smilebrd_gpio_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "smilepd_drv", NULL);
    if(retval != 0) {
        pr_err("could not register smilebrd irq handler\n");
        return retval;
    }

    pr_info("smilebrd gpio probed!\n");
    return 0;
}

/**
 * @brief Remove GPIO
 */
static int smilebrd_gpio_remove(struct platform_device* pdev)
{
    free_irq(smilebrd->irq, NULL);
    gpiod_put(smilebrd->button);
    gpiod_put(smilebrd->led);

    pr_info("smilebrd gpio remove\n");

    return 0;
}

/**
 * @brief Match driver data with device tree data
 */
static const struct of_device_id smilebrd_gpio_dt_ids[] = {
    { .compatible = "heavyc1oud,smilebrd_gpio", },
    {}
};
MODULE_DEVICE_TABLE(of, smilebrd_gpio_dt_ids);

static struct platform_driver smilebrd_gpio_drv = {
    .probe = smilebrd_gpio_probe,
    .remove = smilebrd_gpio_remove,
    .driver = {
        .name = "smilebrd_gpio",
        .of_match_table = of_match_ptr(smilebrd_gpio_dt_ids),
        .owner = THIS_MODULE,
    },
};

/**
 * @brief Install I2C
 */
static int smilebrd_i2c_probe(struct i2c_client* client, const struct i2c_device_id *id)
{
    // store pointer to device structure in the bus
    i2c_set_clientdata(client, smilebrd);

    // store pointer to I2C client into private structure
    smilebrd->i2c_dev = client;

    pr_info("smilebrd i2c probed!\n");

    return 0;
}

/**
 * @brief Remove I2C
 */
static int smilebrd_i2c_remove(struct i2c_client* client)
{
    // get device structure from device bus
    smilebrd = i2c_get_clientdata(client);

    pr_info("smilebrd i2c remove\n");

    return 0;
}

/**
 * @brief Match driver data with device tree data
 */
static const struct of_device_id smilebrd_i2c_dt_ids[] = {
    { .compatible = "heavyc1oud,smilebrd_i2c", },
    {}
};

static const struct i2c_device_id smilebrd_i2c_i2cbus_id[] ={
    {"smilebrd_i2c", 0},
    {},
};
MODULE_DEVICE_TABLE(i2c, smilebrd_i2c_i2cbus_id);

static struct i2c_driver smilebrd_i2c_drv = {
    .probe = smilebrd_i2c_probe,
    .remove = smilebrd_i2c_remove,
    .id_table = smilebrd_i2c_i2cbus_id,
    .driver = {
        .name = "smilebrd_i2c",
        .of_match_table = of_match_ptr(smilebrd_i2c_dt_ids),
        .owner = THIS_MODULE,
    },
};

/**
 * @brief Whole driver initialization
 */
static int __init smilebrd_init(void)
{
    // allocate mem for private structure
    smilebrd = kzalloc(sizeof(struct smilebrd_dev), GFP_KERNEL);

    // initialize misc device
    smilebrd->miscdevice.name = "smilebrd";
    smilebrd->miscdevice.minor = MISC_DYNAMIC_MINOR;
    smilebrd->miscdevice.fops = &smilebrd_fops;

    // register miscdevice
    if(misc_register(&smilebrd->miscdevice)) {
        pr_err("could not register smilebrd misc device\n");
        return EINVAL;
    }

    // register smilebrd gpio driver
    if(platform_driver_register(&smilebrd_gpio_drv)) {
        pr_err("could not register smilebrd gpio driver\n");
        return EINVAL;
    }

    // register smilebrd i2c driver
    if(i2c_register_driver(THIS_MODULE, &smilebrd_i2c_drv)) {
        pr_err("could not register smilebrd i2c driver\n");
        return EINVAL;
    }

    return 0;
}

/**
 * @brief Whole driver deinitialization
 */
static void __exit smilebrd_exit(void)
{
    // deregister miscdevice
    misc_deregister(&smilebrd->miscdevice);

    // deregister smilebrd gpio driver
    platform_driver_unregister(&smilebrd_gpio_drv);

    // deregister smilebrd i2c driver
    i2c_del_driver(&smilebrd_i2c_drv);

    // free mem previously allocated in smilebrd_init
    kfree(smilebrd);
}

module_init(smilebrd_init);
module_exit(smilebrd_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("HeavyC1oud vheavyC1oud@gmail.com");
MODULE_DESCRIPTION("smilebrd driver");

Упрощенно, модель устройств Linux можно представить в таком виде

Bootlin Linux Kernel and Driver Development Training
Bootlin Linux Kernel and Driver Development Training

"Сверху" драйвер взаимодействует с фремворком, фреймворк формирует в папке /dev специальный файл называемый узел устройства, который, в свою очередь, используется из пространства пользователя посредством open/read/write/close, может еще ioctl. Есть несколько специфичных характеристик, говорящих о сущности устройства представленного этим узлом, это тип, и два числа называемые major и minor.

В Linux, устройство может принадлежать одному из трех типов - символьное, блочное или сетевой интефейс, подавляющее большинство устройств, в том числе и смайл пульт, является символьными, т.е. к которому можно обращаться как к потоку байтов. Числа major и minor соответсвуют принятой нумерации, отражающей функционал устройства, например, USB-UART переходник для общения с BeagleBone Black, работает через символьное устройство /dev/ttyUSB0 с номерами major/minor - 188/0, что соответствует значению из диапазона символьных устройств, предназначенному для USB serial converters с порядковым номером конвертера 0.

Теперь, зная общую картину, можно заняться разбором драйвера смайл пульта, причем начать разбор стоит с конца

module_init(smilebrd_init);
module_exit(smilebrd_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("HeavyC1oud vheavyC1oud@gmail.com");
MODULE_DESCRIPTION("smilebrd driver");

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

module_init(smilebrd_init) и module_exit(smilebrd_exit) это также макросы, вообще в документации на ядро макросы настоятельно рекомендуют к использованию, т.к. обратная совместимость не является сильной стороной кода ядра, и от версии к версии, в неизменном виде, макросы живут дольше. Эти макросы задают функции, которые будут вызваны при загрузке и выгрузке модуля. Когда же происходит загрузка модуля? Сейчас просто скажу, что конкретно этот модуль будет загружаться при старте системы во время описи подключенных устройств, т.к. он указан в Device Tree или дереве устройств, более подробный ответ будет, когда придет время это дерево редактировать.

Вот что происходит во время загрузки модуля

static int __init smilebrd_init(void)
{
    // allocate mem for private structure
    smilebrd = kzalloc(sizeof(struct smilebrd_dev), GFP_KERNEL);

    // initialize misc device
    smilebrd->miscdevice.name = "smilebrd";
    smilebrd->miscdevice.minor = MISC_DYNAMIC_MINOR;
    smilebrd->miscdevice.fops = &smilebrd_fops;

    // register miscdevice
    if(misc_register(&smilebrd->miscdevice)) {
        pr_err("could not register smilebrd misc device\n");
        return EINVAL;
    }
  ...

Выделяется память под структуру, где хранится некоторая полезная информация, на усмотрение разработчика, и далее инициализируется "верхняя" часть, согласно приведенной модели устройств. Если устройство не принадлежит явно к какому-либо типу, под который выделен отдельный major номер, если драйвер достаточно прост и нетребователен, то можно немного облегчить себе жизнь и воспользоваться фреймворком Miscenallaneous device, при регистрации такого типа устройства, ядро автоматически присвоит ему major номер 10, с помощью MISC_DYNAMIC_MINOR определит свободный minor и создаст узел устройств для него, останется только в miscdevice.fops определить реализацию действий с файлом.

miscdevice.fops, т.е. file operations, представляет собой структуру со ссылками на обработчики операций open, read, write, poll, mmap и.т.д. Нужны будут только read и ioctl, также нужно добавить поле .owner, как правило, это всегда THIS_MODULE

static const struct file_operations smilebrd_fops = {
    .owner = THIS_MODULE,
    .read = smilebrd_read,
    .unlocked_ioctl = smilebrd_ioctl,
};

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

static ssize_t smilebrd_read(struct file *filp, char __user *userbuf, size_t count, loff_t *ppos)
{
    // stopping the process until an interrupt is received
    wait_event_interruptible(wq, smilebrd->irq_f != 0);

    // interrupt received and handled
    smilebrd->irq_f = 0;

    // copy received device data from kernel space to user space
    if(copy_to_user(userbuf, &smilebrd->tsc_data, sizeof(smilebrd->tsc_data)) != 0) {
        return -EIO;
    }

    return sizeof(unsigned int);
}

ioctl используется для управления светодиодом, сигнализирующем о подключении к серверу чата, больше декоративная функция

static long smilebrd_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    // now exist only SWITCH LED command
    switch(cmd) {
    case CMD_CONTROL_LED:
        if(arg == CMD_CONTROL_LED_ARG_ON) {
            gpiod_set_value(smilebrd->led, 1);
        }
        else {
            gpiod_set_value(smilebrd->led, 0);
        }
        break;
    default:
        break;
    }

    return 0;
}

Теперь о взаимодействии "снизу", согласно модели устройств Linux

ВСЕ НА ДНО
ВСЕ НА ДНО

"Снизу" драйвер взаимодействует с шинами Platform Device и I2C, последняя, очевидно, руководит I2C, а первая выводом для приема сигнала о касании сенсоров и выводом для управления светодиодом. Вообще, в ядре есть отдельные инcтрументы для работы с выводами, к которым подключены светодиоды или выводами общего назначения, можно воспользоваться ими. Также, начиная с версии ядра 4.8, поменялось взаимодействие с GPIO, если раньше обращение к выводам происходило через их номер, то сейчас для этого нужен дескриптор. Регистрация происходит всё в том же init'е

static int __init smilebrd_init(void)
{
		...
    // register smilebrd gpio driver
    if(platform_driver_register(&smilebrd_gpio_drv)) {
        pr_err("could not register smilebrd gpio driver\n");
        return EINVAL;
    }

    // register smilebrd i2c driver
    if(i2c_register_driver(THIS_MODULE, &smilebrd_i2c_drv)) {
        pr_err("could not register smilebrd i2c driver\n");
        return EINVAL;
    }

    return 0;
}

Резонный вопрос, что и зачем регистрируется? Что - структура содержащая данные о драйвере, Зачем - чтобы шина могла выполнять типовые операции при работе с этим драйвером, например, сопоставление с набором оборудования, указанного в дереве устройств или процедуры probe/remove, т.е. выделение/освобождение ресурсов ядра.

Подробнее, на примере I2C

static const struct of_device_id smilebrd_i2c_dt_ids[] = {
    { .compatible = "heavyc1oud,smilebrd_i2c", },
    {}
};

Сначала создается структура smilebrd_i2c_dt_ids типа of_device_id, где of это Open Firmware, или полностью Open Firmware Device Tree - язык для описания оборудования, подключенного к плате, т.е. оборудования, которое не может быть определено автоматически, как например PCI или USB устройства. I2C как раз относится к шинам не поддерживающим автоматическое определение оборудования, и, чтобы ядро узнало о наличии I2C устройсва, ему заранее нужно передать список с подобным оборудованием, он же Device Tree. Каждое устройство в этом списке имеет свое имя и именно оно должно быть указано в поле .compatible, так ядро сможет считать остальные параметры из Device Tree и, в соответствии с ними, настроить регистры периферии

static const struct i2c_device_id smilebrd_i2c_i2cbus_id[] ={
    {"smilebrd_i2c", 0},
    {},
};
MODULE_DEVICE_TABLE(i2c, smilebrd_i2c_i2cbus_id);

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

static const struct i2c_device_id smilebrd_i2c_i2cbus_id[] ={
    {"smilebrd_i2c", 0},
    {"smilebrd_2_i2c", 1},
    {},
  ...

Ещё одна структура, которая содержит две предыдущие, а также функции .probe/.remove вызываемые при подключении устройства

static struct i2c_driver smilebrd_i2c_drv = {
    .probe = smilebrd_i2c_probe,
    .remove = smilebrd_i2c_remove,
    .id_table = smilebrd_i2c_i2cbus_id,
    .driver = {
        .name = "smilebrd_i2c",
        .of_match_table = of_match_ptr(smilebrd_i2c_dt_ids),
        .owner = THIS_MODULE,
    },
};

Механизм примерно такой, в Device Tree есть запись о некоем смайл пульте такого вида "heavyc1oud,smilebrd_i2c", при старте, система видит эту запись и пытается найти драйвер с такой же записью, находит его и вызывает соответствующий .probe; соответствующий .remove будет вызван, если выгрузить модуль ядра

static int smilebrd_i2c_probe(struct i2c_client* client, const struct i2c_device_id *id)
{
    // store pointer to device structure in the bus
    i2c_set_clientdata(client, smilebrd);

    // store pointer to I2C client into private structure
    smilebrd->i2c_dev = client;

    pr_info("smilebrd i2c probed!\n");

    return 0;
}

static int smilebrd_i2c_remove(struct i2c_client* client)
{
    // get device structure from device bus
    smilebrd = i2c_get_clientdata(client);

    pr_info("smilebrd i2c remove\n");

    return 0;
}

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

static int smilebrd_gpio_probe(struct platform_device* pdev)
{
    int retval;
    smilebrd->gpio_dev = pdev;

    // input GPIO, check signal from smile board
    smilebrd->button = gpiod_get(&smilebrd->gpio_dev->dev, "button", 0);
    gpiod_direction_input(smilebrd->button);

    // output GPIO, controls "enable server" LED
    smilebrd->led = gpiod_get(&smilebrd->gpio_dev->dev, "led", 0);
    gpiod_direction_output(smilebrd->led, 0);

    // add debounce interval to input GPIO
    retval = gpiod_set_debounce(smilebrd->button, 1000 * 5);        // time unit 1 us, 1000 us * 5 = 5 ms, MAX = 7 ms
    if(retval != 0) {
        pr_err("could not set debounce interval\n");
    }

    // add interrupt to input GPIO
    smilebrd->irq = gpiod_to_irq(smilebrd->button);
    retval = request_threaded_irq(smilebrd->irq, NULL, smilebrd_gpio_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "smilepd_drv", NULL);
    if(retval != 0) {
        pr_err("could not register smilebrd irq handler\n");
        return retval;
    }

    pr_info("smilebrd gpio probed!\n");
    return 0;
}

В обработчике прерывания, по интерфейсу I2C, у пульта запрашиваются данные о кнопках, затем в узел устройства отправляется сигнал о наличии новой информации от пульта, прерывание обработано

static irqreturn_t smilebrd_gpio_irq_handler(int irq, void* dev_id)
{
    // send i2c data
    smilebrd->tsc_data = i2c_smbus_read_byte(smilebrd->i2c_dev);

    // set irq flag
    smilebrd->irq_f = 1;
    wake_up_interruptible(&wq);

    return IRQ_HANDLED;
}

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

Для начала нужно выбрать раздел для драйвера, т.к. устройство не принадлежит явно к какому-либо типу, то драйвер, имеет смысл, расположить в папке /drivers/staging/

cd /drivers/staging
mkdir smilebrd
touch smilebrd_dev.c

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

# SPDX-License-Identifier: GPL-2.0
config SMILEBRD
	tristate "Smile board driver"
	help
	this option adds a "smile board" device to manage some emoticons on the goodgame.ru site

Второй файл, Makefile, нужен для сборки драйвера

# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_SMILEBRD)	+= smilebrd_dev.o

Затем нужно добавить в вышестоящие Kconfig и Makefile информацию о новом модуле

В файл drivers/staging/Kconfig

...
source "drivers/staging/smilebrd/Kconfig"
...

В файл drivers/staging/Makefile

...
obj-$(CONFIG_SMILEBRD)      += smilebrd/

Теперь, при запуске конфигурации ядра, в меню раздела Device Drivers ---> Staging drivers ---> должен появиться новый драйвер

Ранее, при настройке параметров ядра в Buildroot, был указан конфиг kernel_smilebrd_defconfig, он отличается от omap2plus_defconfig, использованного в предыдущей статье, добавлением, в виде модулей, пунктов Device Drivers ---> Staging drivers ---> Realtek RTL8188EU Wireless LAN NIC driver и Device Drivers ---> Staging drivers ---> Smile board driver. Для создания конфига нужно выбрать вышеуказанные пункты и выйти с сохранением настроек, настройки сохранятся в, расположенный здесь же .config, останется скопировать всё его содержимое в файл, указанный в настройках Buildroot и kernel_smilebrd_defconfig готов. Теперь, чтобы ядро решило воспользоваться новым драйвером, нужно внести изменения еще в одном месте.

Device Tree

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

Давным давно, необходимые для ядра, сведения об оборудовании хранились в папках /arch/arm/plat-xxx и /arch/arm/mach-xxx, а информация ядру передавалась через, так называемые А-тэги, ATAGS, при этом, каждый, уважающий себя, производитель создавал свою версию платформы, которую нужно было поддерживать, пока, однажды, небезызвестный Линус Торвальдс не выразил некоторую озабоченность трудоемкостью поддержки этого зоопарка

Тогда было решено взять модель Device Tree, используемую на платформах архитектур SPARK и PowerPC.

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

Device Tree for Dummies
Device Tree for Dummies

Все ARM'овые деревья устройств расположены в папке /arch/arm/boot/dts/, то дерево, которое предстоит редактировать называется am335x-boneblack.dts. Такой же файл, только с расширением .dtb, использовался в предыдущей статье, загрузчик еще передавал его ядру при старте. DTB это Device Tree Binary или Device Tree Blob, т.е. результат компиляции DTS - Device Tree Source, еще есть DTSI - Device Tree Source Include, это файлы включаемые в .dts, и, как правило, содержащие какие-то базовые вещи.

В дерево устройств нужно внести те изменения, которые касаются непосредственно железа.

Отредактированный am335x-boneblack.dts
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2012 Texas Instruments Incorporated - http://www.ti.com/
 */
/dts-v1/;

#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
#include "am335x-boneblack-common.dtsi"

/ {
	model = "TI AM335x BeagleBone Black";
	compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
};

&cpu0_opp_table {
	/*
	 * All PG 2.0 silicon may not support 1GHz but some of the early
	 * BeagleBone Blacks have PG 2.0 silicon which is guaranteed
	 * to support 1GHz OPP so enable it for PG 2.0 on this board.
	 */
	oppnitro-1000000000 {
		opp-supported-hw = <0x06 0x0100>;
	};
};

&am33xx_pinmux {
	smilebrd_pins: pinmux_smilebrd_pins {
		pinctrl-single,pins = <
			AM33XX_PADCONF(AM335X_PIN_GPMC_A0, PIN_INPUT_PULLUP, MUX_MODE7)		/* (R13) gpio1_16 button pin*/
			AM33XX_PADCONF(AM335X_PIN_GPMC_A1, PIN_OUTPUT, MUX_MODE7)		 	/* (V14) gpio1_17 led pin*/
		>;
	};
};

&i2c2 {
	smilebrd_i2c: smilebrd_i2c@48 {
		compatible = "heavyc1oud,smilebrd_i2c";
		reg = <0x48>;
	};
};

/ {
	smilebrd_gpio: smilebrd_gpio {
		compatible = "heavyc1oud,smilebrd_gpio";
		pinctrl-names = "default";
		pinctrl-0 = <&smilebrd_pins>;
		button-gpios = <&gpio1 16 GPIO_ACTIVE_LOW>;
		led-gpios = <&gpio1 17 GPIO_ACTIVE_HIGH>;
		interrupt-parent = <&gpio1>;
		interrupts = <16 IRQ_TYPE_EDGE_FALLING>;
		status = "okay";
	};
};

Изменения начинаются с добавления новых данных в узел &am33xx_pinmux, здесь, амперсанд указывает, что используется ссылка на существующий узел am33xx_pinmux, существует он во включенном файле am335x-bone-common.dtsi, этот узел содержит данные о мультиплексировании выводов процессора, обычно, для всего многообразия периферии, выводов процессора не хватает, поэтому на каждый вывод назначается по несколько функций, это и есть мультиплексирование, т.е. в этом узле решается какую из функций задействовать.

В узел настроек мультиплексирования добавляются данные о том, что выводы R13 и V14 микросхемы будут использованы как выводы общего назначения и настроены, R13 как вход с подтяжкой к питанию, V14 как выход, по умолчанию используется выход push-pull

&am33xx_pinmux {
	smilebrd_pins: pinmux_smilebrd_pins {
		pinctrl-single,pins = <
			AM33XX_PADCONF(AM335X_PIN_GPMC_A0, PIN_INPUT_PULLUP, MUX_MODE7)	/* (R13) gpio1_16 button pin*/
			AM33XX_PADCONF(AM335X_PIN_GPMC_A1, PIN_OUTPUT, MUX_MODE7)		 	  /* (V14) gpio1_17 led pin*/
		>;
	};
};

По такому же принципу, добавляются данные к узлу &i2c2, процессор имеет на борту несколько интерфесов I2C, здесь используется второй по порядку. Сам узел является контроллером I2C, а добавляемые данные идентифицируют устройство, подключенное к этому контроллеру, так, по значению свойства compatible устройству сопоставляется драйвер, а значение свойства reg это номер устройства на шине I2C, т.е. значение которое фигурирует в первом байте при общении по протоколу I2C

&i2c2 {
	smilebrd_i2c: smilebrd_i2c@48 {
		compatible = "heavyc1oud,smilebrd_i2c";
		reg = <0x48>;
	};
};

Узел smilebrd_gpio является самостоятельным, поэтому у него нет ссылки в виде амперсанда, здесь также есть свойство compatible для подключения нужного драйвера, дальше идут свойства устанавлювающие связь с узлом мультиплексирования, свойства, определяющие номера используемых выводов и активный уровень, т.е. низкий уровень напряжения означает приход сигнала, а ACTIVE_HIGH для вывода светодиода означает, что к нему подключен анод светодиода и чтобы этот светодиод зажечь, нужно подать высокий уровень напряжения. Свойство interrupts определяет вывод, к которому подключено прерывание и спад сигнала в качестве его тригера. Свойство status со значением okay говорит, что оборудование в наличии и используется.

smilebrd_gpio: smilebrd_gpio {
	compatible = "heavyc1oud,smilebrd_gpio";
	pinctrl-names = "default";
	pinctrl-0 = <&smilebrd_pins>;
	button-gpios = <&gpio1 16 GPIO_ACTIVE_LOW>;
	led-gpios = <&gpio1 17 GPIO_ACTIVE_HIGH>;
	interrupt-parent = <&gpio1>;
	interrupts = <16 IRQ_TYPE_EDGE_FALLING>;
	status = "okay";
};

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

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

Топовый чат

Основной упор портала, куда будут отправляться смайлы, сделан на взаимодействие через браузер, но также существует API с помощью которого можно взаимодействовать напрямую. Взаимодействие происходит посредством websocket-соединения и JSON-кодирования. Я не буду подробно останавливаться на самой программе, принцип работы у нее такой, вначале создается отдельный процесс для запуска программы wscat работающей с вебсокетами, этот процесс соединяется с сервером чата и ждет команд, которые должны поступать через предварительно созданные каналы, pipes. Затем, запрашиватся список текущих стримеров, список сортирован по количеству зрителей, т.е. первый в списке с самым большим их количеством, к этому каналу и происходит подключение. После успешного подключения зажигается светодиод на смайл пульте, это делается посредством ioctl, предварительно открытого, узла dev/smilebrd. С помощью read процесс входит в режим ожидания сигнала о касании сенсора от смайл пульта, сигналом служит наличие данных о том, какого сенсора коснулись, определяется текст нужного смайла, и этот смайл отправляется в чат. Интересующиеся могут найти код здесь.

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

Процесс добавления пакета в Buildroot, немного напоминает добавление своего драйвера в исходники ядра, нужно создать папку для своей программы в разделе package/

mkdir buildroot/package/smilebrd_serv

Далее в этой папке создать два файла, Config.in и smilebrd_serv.mk, первый отвечает за добавление нового пункта меню в Buildroot, второй говорит как собирать программу

Config.in

config BR2_PACKAGE_SMILEBRD_SERV
        bool "smilebrd_serv"
        help
          Utility to work with smile board device and send some smiles
          to goodgame.ru chat.

Чтобы файл конфига заработал, нужно добавить ссылку на него в конфиг верхнего уровня, а именно в package/Config.in, добавить нужно в соответствующий раздел, где планируется отображать свою программу, я добавил в раздел menu "Miscellaneous"

...
source "package/smilebrd_serv/Config.in"
...

В файле с указаниями для сборки, пишется версия, она будет выступать в роли тега при скачивании исходников с github. Makefile, со всеми подробностями, скачивается оттуда же, в SMILEBRD_SERV_INSTALL_TARGET_CMDS указывается куда нужно разместить результат сборки и какие присвоить права, пункт $(eval $(generic-package)) говорит, что сборка будет осуществляться посредством Makefile

smilebrd_serv.mk

################################################################################
#
# smilebrd_serv
#
################################################################################

SMILEBRD_SERV_VERSION = v2.0
SMILEBRD_SERV_SITE = $(call github,heavyC1oud,smilebrd_serv,$(SMILEBRD_SERV_VERSION))

define SMILEBRD_SERV_BUILD_CMDS
	$(MAKE) CC="$(TARGET_CC)" LD="$(TARGET_LD)" -C $(@D)
endef

define SMILEBRD_SERV_INSTALL_TARGET_CMDS
	$(INSTALL) -D -m 0755 $(@D)/smilebrd_serv $(TARGET_DIR)/usr/bin
endef

$(eval $(generic-package))

Теперь в меню Buildroot должен появиться новый пакет

Target packages --->
		Miscellaneous --->
    	[*] smilebrd_serv

Всё, настройка завершена, можно сохранить конфиг командой

make savedefconfig

И выполнять сборку всего вышеперечисленного по команде

make

Результаты сборки помещаются в папку /output/images/, здесь будут файлы, необходимые для загрузки:

  • am335x-boneblack.dtb --> скомпилированное дерево устройств

  • MLO --> вторичный загрузчик

  • u-boot.img --> третичный загрузчик

  • u-Env.txt --> дополнительные параметры u-boot

  • zImage --> ядро Linux

И корневая файловая система:

  • rootfs.tar

Осталось разместить всё на SD карте, подробнее об этом есть в первой статье, и подключить интернет, это можно сделать при помощи connman

Если, командой top, заглянуть в список работающих процессов, можно увидеть программу smilebrd_serv, она запускается автоматически, после появления соединения с интернетом, напомню, что это было сделано в скрипте post-build.sh, также командой lsmod можно посмотреть список загруженных модулей в нем должен присутствовать smilebrd_dev. При перезагрузке, BeagleBone Black будет автоматически подключаться к указанной точке доступа, т.е. нужно лишь подождать когда загорится светодиод на пульте и можно слать смайлы, главное не злоупотребить

Итого, просто, легко в использовании, эффективно, одной командой, как и было обещано. Тем, кто не встречался раньше со встроенными системами на Linux, и, все-таки смог прорваться до этого обзаца, может показаться, что Buildroot'овский лозунг звучит неправдоподобно или даже цинично, но, нет, системы сборки действительно максимально упрощают процесс и довольно просты в использовании, прошедшие Linux From Scratch не дадут соврать.

В статье сложно описать нюансы построения встроенной системы на Linux, к тому же, все что касается ядра, стремительно устаревает, отчасти поэтому Грег Кроа-Хартман не очень любит вопросы про Linux Device Drivers четвертой редакции. Если же говорить про русскоязычные материалы, то их, впринципе, исчезающе мало.

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

  1. Linux Device Drivers, 3rd Edition [2005] - устаревшая, но рекомендуемая к прочтению книга

  2. Материалы тренингов от Bootlin - тренинги платные, материалы бесплатные

  3. Mastering Embedded Linux Programming [2015] - про встраиваемые системы, первая часть статьи во многом построена на этой книге

  4. Linux Driver Development for Embedded Processors [2018] - современный взгляд на драйверостроение, раз

  5. Linux Device Drivers Development [2017] - современный взгляд на драйверостроение, два

  6. Mastering Linux Device Driver Development [2021] - современнейший взгляд на драйверостроение, три

  7. Device tree for dummies [2013] - популярно про деревья устройств, слайды к лекции

  8. Мануал по Buildroot

  9. И, конечно же, актуальный Datasheet по ядру Linux

Теги:
Хабы:
Всего голосов 25: ↑24 и ↓1+31
Комментарии19

Публикации

Истории

Работа

QT разработчик
12 вакансий
Программист С
51 вакансия
Программист C++
145 вакансий

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн