Любой драйвер встраиваемого устройства рано или поздно сталкивается с одной и той же задачей: получить доступ к железу. Это означает узнать, по какому адресу отображены регистры периферии, какой номер прерывания выделен устройству, откуда взять тактовый сигнал и как получить GPIO-пин. На первый взгляд это рутина. На деле же способ, которым драйвер запрашивает и освобождает эти ресурсы, определяет, насколько надёжным окажется код при ошибках инициализации, при горячем отключении устройства и при многократной загрузке модуля.
Linux предоставляет для этого стройную систему: platform resource management. Она включает API для описания ресурсов платформенных устройств, функции их получения в методе probe() и механизм managed-ресурсов через префикс devm_, который автоматически освобождает всё захваченное при отсоединении устройства. Понимание этой системы отличает драйвер, написанный по-настоящему правильно, от драйвера, который просто работает.
Что такое платформенное устройство и откуда оно берёт ресурсы
Платформенные устройства в Linux описывают периферию, которую невозможно обнаружить автоматически. Шины вроде PCI умеют перечислять подключённые устройства самостоятельно. Периферийные блоки SoC, GPIO-контроллеры, встроенные таймеры, I2C-контроллеры, аппаратные энкодеры на таком самообнаружении неспособны. Ядро узнаёт о них из Device Tree или из статических таблиц платформенных данных. Результат одинаков в обоих случаях: структура platform_device с прикреплённым массивом ресурсов типа struct resource.
Каждый ресурс описывает один аппаратный объект: диапазон адресов MMIO, номер прерывания, диапазон портов ввода-вывода. Поле flags определяет тип ресурса: IORESOURCE_MEM для памяти, IORESOURCE_IRQ для прерывания, IORESOURCE_IO для портов. Поля start и end задают границы диапазона.
При использовании Device Tree ресурсы формируются автоматически из свойств reg и interrupts узла. Для статической регистрации их описывают явно в коде платформы:
static struct resource my_device_resources[] = {
{
.start = 0xe0004000,
.end = 0xe0004fff,
.flags = IORESOURCE_MEM,
.name = "regs",
},
{
.start = 42,
.end = 42,
.flags = IORESOURCE_IRQ,
.name = "irq",
},
};
static struct platform_device my_device = {
.name = "my-peripheral",
.id = 0,
.num_resources = ARRAY_SIZE(my_device_resources),
.resource = my_device_resources,
};
После регистрации через platform_device_register() эти ресурсы становятся доступны драйверу через стандартный API в методе probe().
Получение ресурсов памяти и отображение регистров
Первый и самый частый запрос в probe() — получить регион MMIO и отобразить его в виртуальное адресное пространство ядра. Без этого шага невозможно читать и писать регистры устройства. Раньше это делалось в три явных шага: platform_get_resource(), request_mem_region(), ioremap(). Каждый шаг требовал обработки ошибки и явного вызова обратной функции при выходе из probe().
Современный подход сводит всё к одной строке. devm_ioremap_resource() проверяет корректность запрошенного региона, запрашивает его у ядра через devm_request_mem_region() и выполняет отображение через devm_ioremap() — всё за один вызов. Функция используется в ядре более тысячи четырёхсот раз и де-факто стала стандартом:
static int my_driver_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
/* base теперь указывает на виртуальный адрес регистров */
iowrite32(0x1, base + CTRL_REG_OFFSET);
return 0;
}
Для платформенных устройств существует ещё более короткая форма, объединяющая platform_get_resource() и devm_ioremap_resource() в единый вызов:
base = devm_platform_ioremap_resource(pdev, 0);
Параметр 0 указывает индекс ресурса памяти. Если устройство имеет два региона MMIO, второй получают с индексом 1. Если регионы именованы в Device Tree, используют вариант по имени:
base = devm_platform_ioremap_resource_byname(pdev, "regs");
Оба исходных API, request_mem_region() и ioremap(), считаются устаревшими. От разработчиков драйверов ожидается использование devm_* вариантов.
После получения виртуального адреса работа с регистрами ведётся исключительно через специальные аксессоры. Прямое разыменование указателя для MMIO-памяти недопустимо: компилятор может кэшировать значения, а оптимизатор убирать повторяющиеся чтения. Правильный способ:
u32 val = ioread32(base + STATUS_REG);
iowrite32(val | ENABLE_BIT, base + CTRL_REG);
Прерывания и принцип автоматического освобождения ресурсов
Получить номер прерывания от платформенного устройства просто. platform_get_irq() возвращает виртуальный номер IRQ, уже преобразованный из аппаратного номера через irqdomain. Отрицательное значение означает ошибку, и его нужно проверять:
int irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
Регистрация обработчика прерывания через devm_request_irq() означает, что при отсоединении устройства ядро автоматически вызовет free_irq(). Старый подход с request_irq() требовал явного отката при ошибке в probe() и дублировал логику освобождения в remove(). Новый подход с devm_request_irq() позволяет просто вернуть код ошибки — откат произойдёт автоматически.
ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
IRQF_SHARED, "my-device", priv);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ %d: %d\n", irq, ret);
return ret;
}
Именно здесь и проявляется настоящая ценность managed-ресурсов. Вместо цепочки меток goto failed_irq:, goto failed_ioremap:, goto failed_alloc: в конце probe() — прямой return ret в любой точке. Ядро само разматывает все захваченные ресурсы в обратном порядке. Это не просто удобство: это устранение целого класса ошибок в обработке сбоев инициализации.
Тактовые сигналы, регуляторы питания и GPIO
Платформенные устройства часто зависят от ресурсов, которые не вписываются в категории памяти и прерываний. Тактовый сигнал нужно получить из подсистемы CCF (Common Clock Framework), включить и выключить в нужный момент. Регулятор питания нужно активировать перед инициализацией устройства. GPIO-пин нужно запросить и установить направление.
Для всех трёх случаев существуют managed-варианты:
/* Тактовый сигнал */
struct clk *clk = devm_clk_get(&pdev->dev, "bus_clk");
if (IS_ERR(clk))
return PTR_ERR(clk);
ret = clk_prepare_enable(clk);
if (ret)
return ret;
/* Регулятор питания */
struct regulator *vdd = devm_regulator_get(&pdev->dev, "vdd");
if (IS_ERR(vdd))
return PTR_ERR(vdd);
ret = regulator_enable(vdd);
if (ret)
return ret;
/* GPIO */
struct gpio_desc *reset_gpio = devm_gpiod_get(&pdev->dev,
"reset",
GPIOD_OUT_LOW);
if (IS_ERR(reset_gpio))
return PTR_ERR(reset_gpio);
Строковые параметры "bus_clk", "vdd" и "reset" — это имена потребителей, которые сопоставляются с записями в Device Tree. Для тактового сигнала это свойство clock-names, для регулятора — <name>-supply, для GPIO — <name>-gpios. Если ресурс опциональный и его отсутствие не является ошибкой, используют суффикс _optional:
struct clk *opt_clk = devm_clk_get_optional(&pdev->dev, "ref_clk");
/* opt_clk будет NULL если clk не задан в DT, но не IS_ERR() */
Разница между IS_ERR() и NULL здесь принципиальная. Обязательный ресурс возвращает ошибку при отсутствии. Опциональный возвращает NULL, и код драйвера просто пропускает соответствующий шаг инициализации.
Дерево ресурсов ядра и контроль над адресным пространством
За всеми вызовами request_mem_region() и аналогами стоит глобальная структура данных ядра: дерево ресурсов. Оно отслеживает, какие диапазоны физических адресов и портов ввода-вывода уже заняты. Попытка захватить уже занятый регион вернёт ошибку вместо того, чтобы позволить двум драйверам работать с одним периферийным блоком одновременно.
Текущее состояние этого дерева доступно через procfs:
# Карта физической памяти
cat /proc/iomem
# Пример вывода:
# e0004000-e0004fff : my-peripheral
# e0005000-e0005fff : another-device.regs
# Карта портов ввода-вывода
cat /proc/ioports
Если регион уже занят чужим драйвером, его имя видно в этом выводе. Это первый инструмент при отладке конфликтов ресурсов. Второй инструмент — dmesg: при конфликте devm_ioremap_resource() печатает сообщение с указанием занятого диапазона и его владельца.
Проверить список всех активных irq-обработчиков с привязкой к устройствам можно аналогично:
cat /proc/interrupts
Столбец "PID" показывает, какой поток обслуживает каждое прерывание, а последний столбец — имя, переданное при регистрации через devm_request_irq().
Написание полного probe с управляемыми ресурсами
Собирая всё вместе, полный метод probe() для реального платформенного устройства принимает компактную и читаемую форму. Каждый шаг либо завершается успехом, либо возвращает ошибку без ручной уборки:
struct my_priv {
void __iomem *base;
struct clk *clk;
int irq;
};
static int my_driver_probe(struct platform_device *pdev)
{
struct my_priv *priv;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
priv->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(priv->clk))
return PTR_ERR(priv->clk);
ret = clk_prepare_enable(priv->clk);
if (ret)
return ret;
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0)
return priv->irq;
ret = devm_request_irq(&pdev->dev, priv->irq,
my_irq_handler, 0,
dev_name(&pdev->dev), priv);
if (ret)
return ret;
platform_set_drvdata(pdev, priv);
/* Инициализация устройства */
iowrite32(CTRL_ENABLE, priv->base + CTRL_REG);
dev_info(&pdev->dev, "Initialized at %pa\n",
&pdev->resource[0].start);
return 0;
}
static int my_driver_remove(struct platform_device *pdev)
{
struct my_priv *priv = platform_get_drvdata(pdev);
clk_disable_unprepare(priv->clk);
return 0;
}
Метод remove() делает ровно одно: отключает тактовый сигнал, который не был получен через devm_. Всё остальное, выделенная память, IRQ, MMIO-отображение, ядро освобождает самостоятельно в момент отсоединения устройства. Никаких goto, никаких дублирующих блоков освобождения, никакого ручного учёта того, что уже было захвачено, а что ещё нет.
Именно эта архитектурная ясность и отличает современный Linux-драйвер от написанного десять лет назад по старым образцам. Managed-ресурсы не делают код магически правильным, но они убирают целый класс трудноуловимых ошибок: утечки ресурсов при частичной инициализации, двойное освобождение при горячем отключении, рассинхронизацию между probe() и remove(). Когда правила управления ресурсами соблюдаются последовательно, драйвер становится предсказуемым, а его поведение при сбоях перестаёт зависеть от аккуратности конкретного разработчика.