Здраствуйте меня зовут Дмитрий. Недавно я написал статью как собрать прошивку для одноплатного компьютера Orange PI i96 с нуля. Если вы не читали то очень советую. И там я упоминал что для того чтобы собрать прошивку на новом ядре Linux, мне пришлось переписать драйверы с учетом архитектуры Device Tree, которую использует современное ядро. В этой статье я опишу как я это сделал.


Для начала надо уточнить что поддержка Orange PI i96 присутствует в ядре изначально. Но она очень очень ограничена. По сути там есть драйвер прерываний, драйвер консоли, драйвер портов общего назначения и файл Device Tree, и это все. Нельзя даже монтировать SD карту как корневую директорию, не говоря уже о работающем Wi-Fi. К счастью у нас есть исходник всех необходимых драйверов для ядра старой версии (3.10.62). Но для того чтобы их использовать нам нужно переписать их под новую архитектуру Device Tree которая используется в новых версиях ядра.

Что такое Device Tree?

Раньше само ядро инициализировало устройства. А поскольку у каждого одноплатного компьютера свой набор устройств, то ядро у каждого было свое. Теперь список устройств задается в файле Device Tree, а ядро уже считывает устройства из этого файла. Меняется только файл Device Tree.

Вот пример записи для шины mmc. Через эту шину подключается SD карта. Файл Device Tree целиком можно увидеть в моём репозитории.

mmc0: mmc@50000 {
			compatible = "rda,8810pl-mmc";
			reg = <0x50000 0x1000>;
			interrupts = <3 IRQ_TYPE_LEVEL_HIGH>;
			detpin-gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>;
			vmmc-supply = <&ldo_sdmmc>;
			clocks = <&sysclk CLK_RDA_APB2>;
			
			mmc_id = <0>;
			max-frequency = <30000000>;
			caps = <(1 << 0)>;
			pm_caps = <0>;
			sys_suspend = <1>;
			clk_inv = <1>;
			mclk_adj = <1>;
			
			status = "disabled";
		};

Первая строка (compatible = "rda,8810pl-mmc") по ней система идентифицирует драйвер который отвечает за устройство. В драйвере должна быть соответствующая строка в структуре of_device_id. Следующие пять строк это выделение ресурсов для драйвера. Здесь происходит выделение:

  1. Адресного пространства по которому располагается устройство.

  2. Прерывание для драйвера устройства.

  3. Номер пина интерфейса общего назначения.

  4. Регулятор напряжения устройства.

  5. Тактовый генератор который тактирует устройство.

Об этих ресурсах будет сказано ниже. Хочу лишь отметить что некоторые ресурсы предоставляют другие устройства. Ссылки на них имеют вид &"имя устройства" (ссылка на контроллер прерываний тоже есть но она задается на все дерево при помощи строчки "interrupt-parent = <&intc>" в корне). Как вы понимаете ваше устройство может загрузится только после того как будет загружены все устройства на которые оно ссылается. А если одно из этих устройств по каким-то причинам не загрузится, то и ваше устройство загружаться не будет.

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

Адресное пространство устройства.

Инициализация устройства включая инициализацию ресурсов происходит в функции Probe. Здесь также происходит выделение адресного пространства по которому будет происходить обращение к устройству. Через это адресное пространство можно обратится к регистрам устройства.

Вот так выглядит шина mmc с точки зрения драйвера:

{
	REG32		SDMMC_CTRL;	    		//0x00000000
	REG32		Reserved_00000004;		//0x00000004
	REG32		SDMMC_FIFO_TXRX; 		//0x00000008
	REG32		Reserved_0000000C[509];	//0x0000000C
	REG32		SDMMC_CONFIG;			//0x00000800
	REG32		SDMMC_STATUS;			//0x00000804
	REG32		SDMMC_CMD_INDEX; 		//0x00000808
  	REG32		SDMMC_CMD_ARG;  	   	//0x0000080C
	REG32		SDMMC_RESP_INDEX;		//0x00000810
	REG32		SDMMC_RESP_ARG3; 		//0x00000814
	REG32		SDMMC_RESP_ARG2; 		//0x00000818
	REG32		SDMMC_RESP_ARG1; 		//0x0000081C
	REG32		SDMMC_RESP_ARG0; 		//0x00000820
	REG32		SDMMC_DATA_WIDTH;		//0x00000824
	REG32		SDMMC_BLOCK_SIZE;		//0x00000828
	REG32		SDMMC_BLOCK_CNT; 		//0x0000082C
	REG32		SDMMC_INT_STATUS;		//0x00000830
	REG32		SDMMC_INT_MASK; 		//0x00000834
	REG32		SDMMC_INT_CLEAR; 		//0x00000838
	REG32		SDMMC_TRANS_SPEED;		//0x0000083C
	REG32		SDMMC_MCLK_ADJUST;		//0x00000840
} HWP_SDMMC_T;

Все что может сделать драйвер считать значение из одного из этих регистров или записать что-нибудь в эти регистры. Других способов взаимодействия с устройством у драйвера нет.

Выделение памяти происходит функцией platform_get_resource:

res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

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

Чтобы конвертировать этот адрес в виртуальный мы должны использовать функцию ioremap:

host->base = ioremap(res->start, resource_size(res));

После этого мы наконец-то сможем обратится к нашему устройству.

Прерывания

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

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

Зачем драйверу нужны прерывания? Если вы когда-нибудь программировали Arduino, то знаете, что любую программу Arduino можно разделить на две части. Это инициализация за это отвечает функция Setup, и опрос состояний портов за это отвечает функция Loop. Работу этой функции можно описать так: delay, опорос портов, снова delay, снова опрос портов и так бесконечно. Как вы понимаете аналогом Setup является функция probe. А аналогом Loop является прерывание по таймеру.

Практически каждый драйвер регистрирует свое прерывания для которого он создает обработчик прерывания:

ret = request_irq(host->irq, rda_mmc_irq, 0x0, mmc_hostname(mmc), host);

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

Зачем все так усложнять? Ну потому что если мы будем использовать комбинацию из бесконечного цикла и функции delay. То процессор не сможет делать ничего кроме как заниматься этим самым бесконечным циклом. А так таймер позволяет разгрузить процессор для каких-то более важных и полезных дел.

Если бы мы писали код для Arduino "по взрослому". То мы должны были запрограммировать таймер, что-бы он время от времени вызывал прерывание, а код из loop перенести в обработчик прерывания. Таким образом мы разгрузили-бы процессор для других дел.

Пины общего назначения.

Если вы программировали на Arduino, то знаете что доступ к пинам общего назначения происходит по номерам. Хотим обратится к пину номер один обращаемся и ничего для этого делать не надо (ну может задать направление пина). В Linux все не так. Чтобы получить доступ к пину нужно получить дескриптор этого пина.

host->det_pin = gpiod_get(&pdev->dev, "detpin", GPIOD_IN);

Дескриптор от номера отличается тем что, он существует только в одном экземпляре. Если один драйвер получит дескриптор, то ни один другой драйвер не сможет получить этот-же дескриптор пока его не вернет тот кто его взял. При помощи функции gpiod_put:

gpiod_put(host->det_pin);

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

После получения дескриптора пина, мы можем управлять им, или запрограммировать прерывание для пина при помощи функции request_irq:

ret = request_irq(gpiod_to_irq(host->det_pin), rda_mmc_det_irq, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | IRQF_NO_SUSPEND, "SD_det_pin", host);

Которое вызовет обработчик прерывания если понадобится.

Если вы посмотрите мой репозиторий для i96 то вы найдете там драйвер для портов общего назначения rda-gpio_old.c. Но ведь как я сказал этот драйвер уже есть в ядре Linux. Зачем же я его продублировал? Дело в том что с тем драйвером который идет в ядре у меня не работали прерывания от пинов. Поэтому мне пришлось переделать старый драйвер под новое ядро, только тогда заработали прерывания. Поэтому я его назвал old.

Также драйвер получает регулятор напряжения. При помощи него можно изменять напряжение на SD карте тут все просто. Тоже самое с тактовым генератором который тактирует карту, при помощи него можно изменять частоту карты (и возможно даже разогнать её, но это не точно).

Интерфейс устройства.

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

Драйвер может просто экспортировать одну из своих функций:

EXPORT_SYMBOL(rda_mmc_set_sdio_irq);

Соответственно какой-нибудь другой драйвер может эту функцию вызвать. Но такой способ взаимодействия плох. Драйвер вызывающий экспортируемую функцию становится зависимым от драйвера который её экспортирует. После этого такой драйвер нельзя загрузить в систему если не загружен драйвер от которого он зависит. Посмотреть зависимости драйвера (или модуля как это называется в Linux) можно при помощи команды:

modinfo "имя драйвера"

В строке depends мы увидим драйверы от которых зависит наш.

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

В нашем случае таким стандартным интерфейсом является mmc-host:

struct mmc_host {
	struct device		*parent;
	struct device		class_dev;
	int			index;
	const struct mmc_host_ops *ops;
	struct mmc_pwrseq	*pwrseq;
	unsigned int		f_min;
	unsigned int		f_max;
	unsigned int		f_init;
	u32			ocr_avail;
	u32			ocr_avail_sdio;	/* SDIO-specific OCR */
	u32			ocr_avail_sd;	/* SD-specific OCR */
	u32			ocr_avail_mmc;	/* MMC-specific OCR */
	struct wakeup_source	*ws;		/* Enable consume of uevents */
	u32			max_current_330;
	u32			max_current_300;
	u32			max_current_180;
....
}

Я не стал приводить всю структуру потому что уж больно она длинная.

mmc_host регистрируется функцией:

mmc_add_host(mmc);

Конечно у интерфейса есть всякие параметры важные и не очень. Но самый важный параметр это mmc_host_ops.

static const struct mmc_host_ops rda_mmc_ops = {
	.request	= rda_mmc_request,
	.get_ro 	= rda_mmc_get_ro,
	.get_cd		= rda_mmc_get_cd,
	.set_ios	= rda_mmc_set_ios,
	.enable_sdio_irq = rda_mmc_enable_sdio_irq,
};

Если бы мы говорили в терминах C++ то мы назвали это методами интерфейса. Но поскольку у нас просто C, то эта структура содержит в себе указатели на функции нашего драйвера. И когда ОС захочет что-то прочитать, или наоборот записать что-то на SD карту, она будет вызывать именно эти функции которые определены в данной структуре.

Собственно это все что я хотел рассказать.

Ссылка на репозиторий с драйверами.

Ссылка на статью в где я собираю прошивку.

On English please.