192. Периферия из программы: gpiochip, i2c-dev, spidev, sysfs
На микроконтроллере вывод переключают записью в регистр: GPIOC->BSRR
и всё (раздел 8). На приборе с Linux регистрами владеет ядро, а программа
просит его об услуге через файл в /dev. Разница не в сложности,
а в том, кто отвечает за железо — и этот раздел о том, как выглядит
та же работа с другой стороны.
Числа взяты из заголовков ядра Debian 12 прогоном _verify/linux191/uapi.c
в образе stm-linux:1; сверяет _verify/linux191_check.py.
Как это называется в документации
Оригинал Перевод userspace пользовательское пространство: ваши процессы, не ядро character device символьное устройство: файл в /dev, к которому обращаются черезioctlioctl команда устройству вне read/write line (GPIO) линия: один вывод контроллера GPIO consumer кто занял линию: имя, видное в системе line bias (GPIO) подтяжка линии внутри контроллера
192.1 Кто владеет выводом
Писать в регистры GPIO из программы нельзя, и это не запрет ради запрета. Ядру нужно знать, кто занял вывод: иначе два процесса одновременно включат один и тот же светодиод, а драйвер шины I²C обнаружит, что его ножку кто-то переключил вручную.
| Что нужно | Файл | Чем обращаются |
|---|---|---|
| вывод, вход, прерывание по фронту | /dev/gpiochipN |
ioctl, API версии 2 |
| шина I²C напрямую | /dev/i2c-N |
ioctl I2C_SLAVE и I2C_RDWR |
| шина SPI напрямую | /dev/spidevB.C |
ioctl SPI_IOC_MESSAGE(n) |
| ШИМ, светодиоды, АЦП | /sys/class/pwm, leds, iio |
чтение и запись текстовых файлов |
Правило, которое экономит дни: если у устройства есть драйвер в ядре —
пользуйтесь драйвером, а не прямой работой с шиной. Часы ds1307 из
дерева устройств (раздел 191.3) сами появятся как /dev/rtc0, и читать их
через i2c-dev не нужно. Прямой доступ — для того, чего в ядре нет:
своей микросхемы, отладки, разовой проверки.
192.2 GPIO: /dev/gpiochipN
Старый способ через /sys/class/gpio (запись номера в export) объявлен
устаревшим: номер линии там глобальный и разъезжается от ядра к ядру,
а освободить забытую линию некому. Нынешний способ — символьное устройство
на каждый контроллер и запрос линий через ioctl.
Порядок такой: открыть /dev/gpiochipN, узнать, что это за контроллер,
запросить нужные линии одним запросом, получить обратно файловый
дескриптор на сами линии и дальше работать с ним. Закрыли дескриптор —
линии освободились сами, даже если программа упала.
#include <linux/gpio.h>
int chip = open("/dev/gpiochip1", O_RDWR);
struct gpiochip_info info;
ioctl(chip, GPIO_GET_CHIPINFO_IOCTL, &info); /* имя, метка, сколько линий */
struct gpio_v2_line_request req = {0};
req.offsets[0] = 21; /* линия, которую дерево назвало, но не отдало драйверу */
req.num_lines = 1;
req.config.flags = GPIO_V2_LINE_FLAG_OUTPUT;
strncpy(req.consumer, "handbook", sizeof(req.consumer) - 1);
ioctl(chip, GPIO_V2_GET_LINE_IOCTL, &req); /* req.fd — дескриптор линий */
struct gpio_v2_line_values v = { .mask = 1, .bits = 1 };
ioctl(req.fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &v); /* зажечь */
v.bits = 0;
ioctl(req.fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &v); /* погасить */
close(req.fd); /* линия свободна */
Что за этим стоит — из заголовков ядра:
| Значение | Что это |
|---|---|
GPIO_GET_CHIPINFO_IOCTL = 0x8044B401 |
спросить имя контроллера и число линий |
GPIO_V2_GET_LINEINFO_IOCTL = 0xC100B405 |
спросить про одну линию: занята ли и кем |
GPIO_V2_GET_LINE_IOCTL = 0xC250B407 |
запросить линии и получить дескриптор |
GPIO_V2_LINE_GET_VALUES_IOCTL = 0xC010B40E |
прочитать значения |
GPIO_V2_LINE_SET_VALUES_IOCTL = 0xC010B40F |
записать значения |
struct gpiochip_info |
68 байт |
struct gpio_v2_line_info |
256 байт |
struct gpio_v2_line_request |
592 байта |
struct gpio_v2_line_event |
48 байт |
GPIO_V2_LINES_MAX |
64 линии за один запрос |
Флаги настройки — те же понятия, что и в регистрах микроконтроллера
(раздел 8.2): GPIO_V2_LINE_FLAG_INPUT = 0x4,
GPIO_V2_LINE_FLAG_OUTPUT = 0x8,
GPIO_V2_LINE_FLAG_EDGE_RISING = 0x10,
GPIO_V2_LINE_FLAG_BIAS_PULL_UP = 0x100.
Значения передаются не по одной линии, а маской: mask — какие линии
трогаем, bits — что в них писать. Вся структура — 16 байт,
и одним вызовом переключается до 64 линий сразу.
Прерывание по фронту выглядит как чтение из того же дескриптора:
линию запрашивают с флагом EDGE_RISING (или FALLING), а дальше
poll и read возвращают по событию структуру на 48 байт — с номером
линии и отметкой времени от монотонных часов ядра. Это прямой аналог
EXTI из раздела 7, только время проставляет ядро, а не вы.
На практике всё это пишут не руками, а через libgpiod: библиотека прячет
ioctl, а её команды gpiodetect, gpioinfo, gpioget, gpioset
позволяют проверить проводку до написания кода — как раз то, чего
на микроконтроллере обычно не хватает.
192.3 I²C: /dev/i2c-N
Шина открывается как файл. Дальше два пути: простой — сказать ядру адрес
ведомого и пользоваться read/write; правильный — передать пару
сообщений одним вызовом.
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
int fd = open("/dev/i2c-0", O_RDWR);
uint8_t reg = 0x75, val = 0;
struct i2c_msg msgs[2] = {
{ .addr = 0x68, .flags = 0, .len = 1, .buf = ® }, /* записали номер регистра */
{ .addr = 0x68, .flags = I2C_M_RD, .len = 1, .buf = &val }, /* прочитали байт */
};
struct i2c_rdwr_ioctl_data tr = { .msgs = msgs, .nmsgs = 2 };
ioctl(fd, I2C_RDWR, &tr);
Это та же транзакция с повторным стартом, что и в разделе 17.4, только старт, адрес и подтверждения выставляет драйвер контроллера.
| Значение | Что это |
|---|---|
I2C_SLAVE = 0x703 |
назначить адрес ведомого для read/write |
I2C_SLAVE_FORCE = 0x706 |
то же, даже если адрес занят драйвером |
I2C_TENBIT = 0x704 |
десятибитная адресация (раздел 17.2) |
I2C_RDWR = 0x707 |
несколько сообщений одной транзакцией |
I2C_RETRIES = 0x701, I2C_TIMEOUT = 0x702 |
сколько раз повторять и сколько ждать |
struct i2c_msg |
16 байт на 64-разрядной системе, 12 — на 32-разрядном ARM прибора |
I2C_RDWR_IOCTL_MAX_MSGS |
42 сообщения за вызов |
I2C_M_RD = 0x1, I2C_M_TEN = 0x10 |
чтение; десятибитный адрес |
Адрес — семибитный. В addr кладут 0x68, а не 0xD0: сдвиг
и бит направления добавляет драйвер. Это ровно та путаница, о которой
раздел 17.2, — но здесь ядро решает её за вас.
⚠️
I2C_SLAVEоткажет, если адрес уже занят драйвером в ядре. Это не ошибка, а защита: часыds1307из дерева устройств держит драйверrtc. Либо пользуйтесь драйвером, либо уберите устройство из дерева, либо (для отладки)I2C_SLAVE_FORCE— понимая, что двое будут говорить с одной микросхемой.
192.4 SPI: /dev/spidevB.C
Имя файла — номер шины и номер выбранного кристалла: spidev0.0 — первая
шина, первый CS. Передача всегда обмен: сколько байт отправили, столько
и приняли (раздел 18.1).
#include <linux/spi/spidev.h>
int fd = open("/dev/spidev0.0", O_RDWR);
uint8_t mode = SPI_MODE_0, bits = 8;
uint32_t speed = 24000000;
ioctl(fd, SPI_IOC_WR_MODE, &mode);
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
uint8_t tx[3] = { 0x9F, 0, 0 }, rx[3] = { 0 }; /* JEDEC ID памяти, раздел 41 */
struct spi_ioc_transfer t = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = sizeof(tx),
.speed_hz = speed,
.bits_per_word = bits,
};
ioctl(fd, SPI_IOC_MESSAGE(1), &t);
| Значение | Что это |
|---|---|
struct spi_ioc_transfer |
32 байта |
SPI_IOC_WR_MODE = 0x40016B01, SPI_IOC_RD_MODE = 0x80016B01 |
задать и прочитать режим |
SPI_IOC_WR_BITS_PER_WORD = 0x40016B03 |
бит в слове |
SPI_IOC_WR_MAX_SPEED_HZ = 0x40046B04 |
верхняя частота |
SPI_IOC_MESSAGE(1) = 0x40206B00 |
одна передача |
SPI_IOC_MESSAGE(2) = 0x40406B00 |
две подряд, без отпускания CS между ними |
SPI_CPHA = 0x1, SPI_CPOL = 0x2 |
те же два бита, что и в CR1 микроконтроллера |
SPI_MODE_0 = 0x0, SPI_MODE_3 = 0x3 |
режимы из таблицы раздела 18.2 |
SPI_3WIRE = 0x10 |
полудуплекс по одной линии данных |
Обратите внимание на номера SPI_IOC_MESSAGE(1) и SPI_IOC_MESSAGE(2):
они различаются — 0x40206B00 и 0x40406B00. В номер ioctl входит
размер передаваемых данных, а он равен 32 × n: 0x20 и 0x40.
Отсюда правило — число в SPI_IOC_MESSAGE(n) обязано совпадать с длиной
массива структур. Драйвер читает ровно n структур: при меньшем n лишние
передачи молча не выполнятся, при большем он прочтёт память за массивом,
и итог зависит от мусора — EINVAL, EFAULT или странная передача.
192.5 Что осталось в sysfs
Не всё переехало в символьные устройства. Часть периферии управляется текстовыми файлами, и это удобно: работает из командной строки и из любого языка.
| Куда | Что делает |
|---|---|
/sys/class/pwm/pwmchip0/ |
export, затем period и duty_cycle в наносекундах, enable |
/sys/class/leds/<имя>/ |
brightness, а trigger умеет мигать сам: heartbeat, timer |
/sys/bus/iio/devices/ |
АЦП, датчики: сырое значение и множитель для перевода в вольты |
/sys/class/thermal/ |
температура кристалла в тысячных долях градуса |
ШИМ здесь задают периодом и временем высокого уровня в наносекундах,
а не значениями PSC, ARR и CCR (раздел 13.5): пересчёт делает
драйвер. Светодиод из дерева устройств раздела 191.3 с
linux,default-trigger = "heartbeat" начнёт мигать сам, без единой
строки кода, — удобный признак «система жива».
192.6 Грабли
Открыли /dev/gpiochip0 и не нашли нужную линию. Контроллеров
несколько, нумерация внутри каждого своя. gpiodetect показывает
контроллеры, gpioinfo — линии с именами и тем, кто их занял.
Линия занята. Её держит драйвер (светодиод, шина, сенсор) по дереву устройств. Освободить можно, только убрав узел из дерева, — а не «форсом».
Значение записали, а вывод не изменился. В gpio_v2_line_values
забыли mask: без бита в маске ядро не трогает линию.
Линия гаснет сразу после записи. Закрыли дескриптор — ядро вернуло линию в исходное состояние. Держать дескриптор открытым столько, сколько нужен уровень.
ioctl на SPI возвращает EINVAL или шлёт не то. Число
в SPI_IOC_MESSAGE(n) не совпадает с числом структур: номер ioctl
считается из размера, и драйвер читает ровно n структур.
Адрес I²C не тот. В addr идёт семибитный адрес; восьмибитный
из документации на датчик даст обращение по другому адресу.
Права. По умолчанию /dev/gpiochip* и /dev/spidev* часто принадлежат
root, а групп gpio и spi может не быть вовсе. Группу создают, отдают
ей устройство правилом udev (раздел 193.5), а пользователя службы добавляют
в неё — usermod -aG или строкой SupplementaryGroups=. Запускать прибор
от root ради светодиода не нужно.
192.7 Итоги
- Регистрами владеет ядро; программа работает через файлы в
/devиioctl. Это не обходной путь, а способ не подраться за вывод. - GPIO —
/dev/gpiochipNи API версии 2: запрос линий возвращает дескриптор, закрытие освобождает линии.struct gpio_v2_line_request— 592 байта, до 64 линий за раз, значения — маской в 16 байтах. - Прерывание по фронту — чтение событий по 48 байт с отметкой времени от ядра.
- I²C —
/dev/i2c-N:I2C_RDWR=0x707передаёт до 42 сообщений одной транзакцией; адрес семибитный, сдвиг делает драйвер. - SPI —
/dev/spidevB.C:struct spi_ioc_transfer32 байта,SPI_IOC_MESSAGE(n)кодирует размер в самом номере —0x40206B00для одной передачи и0x40406B00для двух. - ШИМ, светодиоды и АЦП остались в
sysfsи задаются текстом: период и заполнение в наносекундах вместоPSC/ARR/CCR. - Если у железки есть драйвер в ядре — пользуйтесь драйвером; прямой доступ нужен для своего и для отладки.
Дальше: раздел 193 «Служба прибора: systemd, журнал, права и сторожевой таймер».