Pull to refresh
0
НТЦ Метротек
Разработка и производство Ethernet устройств etc.

Linux, отложенная загрузка драйверов и неработающие прерывания

Reading time 9 min
Views 29K
Сегодня я расскажу о неожиданных проблемах, которые возникли при подключении матричной клавиатуры к ARM-борде под управлением Linux в приборе Беркут-ETN (ETN — новая аппаратная ревизия Беркут-ET). А конкретно о том, почему драйвер adp5589 не захотел получать прерывания и как мы смогли заставить его это делать.

Кому интересно — добро пожаловать под кат.



Оглавление статьи:

  • Описание железной части
  • Где у нас проблема?
  • Пару слов о Device Tree
  • Немного о регистрации устройств и драйверов
  • Механизм отложенной загрузки драйверов
  • Как заставить всё работать

Описание железа вокруг клавиатуры:

У самой клавиатуры контроллера нет — она подключена по шине I2C с помощью специального контроллера матричной клавиатуры — микросхемы adp5589. У микросхемы есть линия прерывания, заведённая на один из GPIO пинов ARM SoCа. В итоге схема подключения выглядит примерно так:



portb — это порт, на пин которого заведено прерывание от контроллера клавиатуры;
intc — главный контроллер прерываний;
i2c0 — контроллер шины i2c.

Драйвер adp5589 по каким-то причинам упорно не хочет получать номер прерывания. Что же может быть причиной такого поведения? Возможно, для загрузки драйвера клавиатуры не хватает каких-то ресурсов. Может быть не успели загрузиться устройства, от которых он зависит? Давайте посмотрим, от каких устройств он может зависеть:

Во-первых — от контроллера шины I2C, к которой он подключен.
Во-вторых — от контроллера порта, на пин которого у нас заведена линия прерывания.

Теперь посмотрим в каком порядке загружаются драйвера этих устройств:

gic
designware-i2c
adp5589
dw-apb-gpio-port

Ага! Вот и причина — когда загружается драйвер клавиатуры, его interrupt-parent ещё не загружен. Как результат — драйвер клавиатуры не получает номер прерывания. Стандартное решение этой проблемы — механизм отложенной загрузки драйверов.

Его суть в том, что драйвер может потребовать повторной загрузки, если какой-нибудь нужный ему ресурс ещё не доступен. А потребовать он это может, вернув значение -EPROBE_DEFER из своей функции probe. Тогда этот драйвер будет повторно загружен позже. К тому времени или нужный ресурс уже будет доступен, или загрузка драйвера снова будет отложена.

Добавляем проверку в функцию probe драйвера клавиатуры:

if (!client->irq) {
    dev_err(&client->dev, "no IRQ boss?\n");
    return  -EPROBE_DEFER;
}

В надежде смотрим на новый порядок загрузки:
gic
adp5589
designware-i2c
dw-apb-gpio-port
(deferred)adp5589
(deferred)adp5589
(deferred)adp5589

Что-то пошло не так — драйвер клавиатуры повторно загрузился после драйвера GPIO, но прерывания так и не получил. Похоже, придётся зарываться в исходный код глубже, чем ожидалось.

Напрашивается три возможных пути решения:

  • Захардкодить номер прерывания прямо в драйвер
  • Каким-либо способом задать порядок загрузки драйверов
  • Разобраться с механизмом отложенной загрузки драйверов, который почему-то не заработал

Первый вариант:

Вариант рабочий, но не желательный. Подойдёт в качестве временного, но если что-нибудь поменять в аппаратной части (например выход прерывания подключить к другому порту GPIO), то изменения придётся вносить не только в Device Tree, но и в исходный код драйвера.

Второй вариант:

Явно задать порядок загрузки драйверов возможности нет. Так что этот вариант не подходит.

Третий вариант:

Самый правильный. Его и будем рассматривать.

Здесь, пожалуй, стоит кратко рассказать про такую вещь как Device Tree, так как далее будут отсылки к ней.

Device Tree — это одна из форм описания аппаратной части устройства, на котором мы хотим использовать Linux. Представляется в виде дерева узлов, в которых задаётся нужная информация. DT существует в виде текстовых человекочитаемых файлов (.dts; .dtsi) и собираемого из них бинарного файла (.dtb).

Для примера рассмотрим кусочек .dts файла описывающий структуру подключения нашего контроллера клавиатуры к другим устройствам SoCа.

i2c0: i2c@ffc04000 {
     compatible = "snps,designware-i2c";     
     keybs@34 { 
         compatible = "adi,adp5589";
         interrupts = <19 IRQ_TYPE_LEVEL_LOW>; 
         interrupt-parent = <&portb>; 
    }; 
};
intc: intc@fffed000 {
     compatible = "arm,cortex-a9-gic";
     #interrupt-cells = <3>;
     interrupt-controller;
};
portb: gpio-controller@0 {
     compatible = "snps,dw-apb-gpio-port";
     interrupt-controller;
     #interrupt-cells = <2>;
     interrupts = <0 165 4>;
     interrupt-parent = <&intc>;
};

(Не интересующие нас сейчас узлы и свойства вырезаны для облегчения понимания)

i2c0, keybs, inc и portb — узлы, всё остальное — их свойства. Из кода сразу становится видно что чип контроллера клавиатуры подключен к I2C шине. В свойстве compatible — строка, которая описывает производителя и модель устройства. Именно по этому свойству ОС понимает какой драйвер нужно связать с этим устройством.

interrupt-controller — свойство, указывающее, что это устройство может являться контроллером прерываний, а interrupt-parent указывает к кому подключено прерывание от текущего устройства.

#interrupt-cells — свойство, указывающее на количество параметров, которыми описываются прерывания данного контроллера прерываний, а interrupts — свойство, в котором задаются параметры для данного прерывания.

Например в portb указано: #interrupt-cells = <2> Это значит, что в узлах, для которых portb это interrupt-parent в свойстве interrupts нужно описать два параметра. portb это interrupt-parent для keybs. Смотрим в keybs. Там указано: interrupts = <19 IRQ_TYPE_LEVEL_LOW>. Что это значит?

Здесь описываются два параметра. Первый — это номер пина в порте portb, на который у нас заведена линия прерывания от контроллера клавиатуры. Второй — тип прерывания (по низкому или высокому уровню). Как узнать сколько для контроллера прерываний нужно описывать параметров, и что каждый из них значат? Обычно про это написано в документации. Так, про portb написано в этом файле: Documentation/devicetree/bindings/gpio/snps-dwapb-gpio.txt.

&portb — ссылка на узел portb (в нашем случае ссылка на portb будет равна /soc/gpio@ff709000/gpio-controller@0)
Остальные свойства нам пока не понадобятся, про них, и вообще про Device Tree, подробно можно почитать здесь: devicetree.org/Device_Tree_Usage.

Ещё не лишним будет упомянуть про процесс регистрации устройств и драйверов (не беспокойтесь, к основной теме мы вернёмся уже в следующем абзаце). Согласно Linux Device Model:

Устройство — физический или виртуальный объект, который подключен к шине(возможно тоже виртуальной)
Драйвер — программный объект, который может быть связан с устройством и может выполнять какие-либо функции управления.
Шина — устройство, предназначенное быть “точкой крепления” других устройств. Базовая функциональность всех шин, поддерживаемых ядром, определяется структурой bus_type. В этой структуре объявлена вложенная структура subsys_private, в которой объявлены два списка: klist_devices и klist_drivers.
klist_devices — список устройств, которые подключены к шине.
klist_drivers — список драйверов, которые могут управлять устройствами на этой шине.
Устройства и драйвера добавляются в эти списки с помощью функций device_register и driver_register. Кроме того, device_register и driver_register связывают устройство с подходящим драйвером. device_register проходит по списку драйверов и пытается найти драйвер, подходящий для данного устройства. (driver_register проходит по списку устройств и пытается найти устройства, которыми он может управлять) Проверка подходит ли драйвер для устройства производится с помощью функции match(dev, drv), указатель на которую есть в структуре bus_type.



Теперь можно перейти и к основной теме — реализации механизма отложенной загрузки драйверов. Заглянем в файл drivers/base/dd.c Вот краткое описание того, что мы там увидим:

Для управления повторной загрузкой драйверов имеются два списка — deferred_probe_pending_list и deferred_probe_active_list.

deferred_probe_pending_list — список устройств, для загрузки драйвера которых не хватает каких-то ресурсов.
deferred_probe_active_list — список устройств, драйвер которых можно попробовать запустить повторно.

В функции really_probe вызывается функция probe для шины, на которой расположено устройство. В нашем случае это функция i2c_device_probe и выглядит это так dev->bus->probe(dev). Возвращаемое значение проверяется на ошибки, и, если оно равно -EPROBE_DEFER, то устройство добавляется в deferred_probe_pending_list.

Но самое интересное — как и когда драйвер вызывается заново. Пока драйверы возвращают -EPROBE_DEFER, устройства последовательно добавляются в deferred_probe_pending_list. Но как только для какого-либо драйвера функция probe завершилась успешно, все устройства из deferred_probe_pending_list переносятся в deferred_probe_active_list. Выглядит логично — возможно, того драйвера, который у нас последний был успешно загружен, и не хватало для нормальной загрузки отложенных драйверов. Повторная попытка запуска драйверов из deferred_probe_active_list производится функцией deferred_probe_work_func. В ней вызывается bus_probe_device для каждого устройства из списка.

Вызов bus_probe_device в конечном итоге снова приведёт нас к функции really_probe для пары из нашего устройства и его драйвера (см. выше).



Но подождите! Мы сейчас говорили о вызове функции probe для шины, на которой расположено устройство. То есть о i2c_device_probe. А как же функция probe драйвера клавиатуры? Нет, про неё мы не забыли, она как раз будет вызвана из i2c_device_probe. В этом можно убедиться, взглянув на её код в файле drivers/i2c/i2c-core.с:

Код i2c_device_probe
static int i2c_device_probe(struct device *dev)
{
	struct i2c_client	*client = i2c_verify_client(dev);
	struct i2c_driver	*driver;
	int status;

	if (!client)
		return 0;

	driver = to_i2c_driver(dev->driver);
	if (!driver->probe || !driver->id_table)
		return -ENODEV;

	if (!device_can_wakeup(&client->dev))
		device_init_wakeup(&client->dev,
					client->flags & I2C_CLIENT_WAKE);
	dev_dbg(dev, "probe\n");

	status = of_clk_set_defaults(dev->of_node, false);
	if (status < 0)
		return status;

	status = dev_pm_domain_attach(&client->dev, true);
	if (status != -EPROBE_DEFER) {
	//Вот и вызов probe драйвера клавиатуры (в нашем случае)
		status = driver->probe(client, i2c_match_id(driver->id_table,
					client));
		if (status)
			dev_pm_domain_detach(&client->dev, true);
	}

	return status;
}


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

В функцию adp5589_probe(struct i2c_client *client, const struct i2c_device_id *id) передаётся структура client, одно из полей которой — irq — номер прерывания, которое будет генерировать наше устройство (контроллер клавиатуры). adp5589_probe вызовется из функции i2c_device_probe(struct device * dev). В неё передаётся структура device, из указателя на которую вычисляется указатель на структуру i2c_client (с помощью магии макроса container_of).

Пару слов о container_of
Этот макрос принимает на вход указатель на поле структуры, тип этой структуры и имя поля на которое указывает указатель, а возвращает указатель на саму структуру.



Про его работу хорошо расписано здесь.

Значит нужно найти где заполняется структура i2c_client. Заполняется она в функции i2c_new_device(struct i2c_adapter * adap, struct i2c_board_info const * info); Конкретно поле irq копируется из одноимённого поля структуры i2c_board_info.

struct i2c_client	*client;
client->irq = info->irq;

Структура i2c_board_info заполняется в функции of_i2c_register_devices(struct i2c_adapter * adap).

info.irq = irq_of_parse_and_map(node, 0); 

irq_of_parse_and_map — это обёртка для цепочки из двух функций — of_irq_parse_one и irq_create_of_mapping; функция of_irq_parse_one пытается найти узел, который заявлен в device tree как interrupt-controller для текущего устройства.
Помните эти несколько строчек в device tree?

expander: pca9535@20 { 
	interrupt-parent = <&portb>; 
}; 

Именно portb и ищет of_irq_parse_one, а по результатам своей работы заполняет структуру of_phandle_args, которая передаётся функции irq_create_of_mapping. irq_create_of_mapping уже и возвращает искомый номер прерывания.

В первый раз of_irq_parse_one не находит порт GPIO, на что ругается в лог:

irq: no irq domain found for /soc/gpio@ff709000/gpio-controller@0!

А что происходит при повторной загрузке драйвера? А ничего. Вызываются то только i2c_device_probe и adp5589_probe.
Вот в чём и проблема. Прерывание устанавливается только в первый раз и остаётся таким навечно, сколько бы мы не выполняли повторную загрузку нашего драйвера.

Проблему нашли, но как её исправить?

Можно попробовать перенести код получения прерывания в i2c_device_probe. До этого нам номер прерывания нигде не требуется, так что проблем возникнуть не должно.

Но лучше давайте заглянем в исходники более свежей версии ядра (у нас установлена версия 3.18) Вот что мы там увидим:
Установку прерывания i2c клиента перенесли в функцию i2c_device_probe.

if (!client->irq && dev->of_node) {
        int irq = of_irq_get(dev->of_node, 0);
        if (irq == -EPROBE_DEFER)
                return irq;
        if (irq < 0)
                irq = 0;
        client->irq = irq;
}

В структуре i2c_board_info хоть и осталось поле irq но оно не используется. Так что в новых версиях ядра проблема исправлена.

Осталось всего лишь перенести изменения в нашу версию. Все изменения коснутся файла drivers/i2c/i2c-core.c
Добавим в нашу i2c_device_probe установку прерывания клиента i2c, что появилась в свежей версии, а в функции of_i2c_register_devices удаляем установку прерывания.

Изменения в виде листинга из git diff
--- a/drivers/i2c/i2c-core.c
+++ b/drivers/i2c/i2c-core.c
@@ -626,6 +626,17 @@ static int i2c_device_probe(struct device *dev)
        if (!client)
                return 0;
 
+       if (!client->irq && dev->of_node) {
+               int irq = of_irq_get(dev->of_node, 0);
+
+               if (irq == -EPROBE_DEFER)
+                       return irq;
+               if (irq < 0)
+                       irq = 0;
+
+               client->irq = irq;
+       }
+
        driver = to_i2c_driver(dev->driver);
        if (!driver->probe || !driver->id_table)
                return -ENODEV;
@@ -1407,7 +1418,12 @@ static void of_i2c_register_devices(struct i2c_adapter *adap)
                        continue;
                }
 
-               info.irq = irq_of_parse_and_map(node, 0);
+               /*
+                * Now, we don't need to set interrupt here, because we set
+                * it in i2c_device_probe function 
+                * info.irq = irq_of_parse_and_map(node, 0);
+                */
+
                info.of_node = of_node_get(node);
                info.archdata = &dev_ad;


Проверяем — клавиатура работает. Смотрим в /proc/interrupt:

$ grep 'adp5589_keys' /proc/interrupts
305:          2         -  20  adp5589_keys

Нажмём несколько кнопок:

$ grep 'adp5589_keys' /proc/interrupts
305:          6         -  20  adp5589_keys

Проблема решена.
Tags:
Hubs:
+29
Comments 6
Comments Comments 6

Articles

Information

Website
metrotek.spb.ru
Registered
Founded
Employees
31–50 employees
Location
Россия