
Здраствуйте меня зовут Дмитрий. Недавно я написал статью как собрать прошивку для одноплатного компьютера 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. Следующие пять строк это выделение ресурсов для драйвера. Здесь происходит выделение:
-
Адресного пространства по которому располагается устройство.
-
Прерывание для драйвера устройства.
-
Номер пина интерфейса общего назначения.
-
Регулятор напряжения устройства.
-
Тактовый генератор который тактирует устройство.
Об этих ресурсах будет сказано ниже. Хочу лишь отметить что некоторые ресурсы предоставляют другие устройства. Ссылки на них имеют вид &»имя устройства» (ссылка на контроллер прерываний тоже есть но она задается на все дерево при помощи строчки «interrupt-parent = <&intc>» в корне). Как вы понимаете ваше устройство может загрузится только после того как будет загружены все устройства на которые оно ссылается. А если одно из этих устройств по каким-то причинам не загрузится, то и ваше устройство загружаться не будет.
Дальше идет секция с параметрами которые может считать драйвер. И последняя строка это строка которая определяет будет запущено устройство или нет.
Адресное пространство устройства.
Инициализация устройства включая инициализацию ресурсов происходит в функции Probe. Здесь также происходит выделение адресного пространства по которому будет происходить обращение к устройству. Через это адресное пространство можно обратится к регистрам устройства.
Вот так выглядит шина mmc с точки зрения драйвера:
{ REG32SDMMC_CTRL; //0x00000000 REG32Reserved_00000004;//0x00000004 REG32SDMMC_FIFO_TXRX; //0x00000008 REG32Reserved_0000000C[509];//0x0000000C REG32SDMMC_CONFIG;//0x00000800 REG32SDMMC_STATUS;//0x00000804 REG32SDMMC_CMD_INDEX; //0x00000808 REG32SDMMC_CMD_ARG; //0x0000080C REG32SDMMC_RESP_INDEX;//0x00000810 REG32SDMMC_RESP_ARG3; //0x00000814 REG32SDMMC_RESP_ARG2; //0x00000818 REG32SDMMC_RESP_ARG1; //0x0000081C REG32SDMMC_RESP_ARG0; //0x00000820 REG32SDMMC_DATA_WIDTH;//0x00000824 REG32SDMMC_BLOCK_SIZE;//0x00000828 REG32SDMMC_BLOCK_CNT; //0x0000082C REG32SDMMC_INT_STATUS;//0x00000830 REG32SDMMC_INT_MASK; //0x00000834 REG32SDMMC_INT_CLEAR; //0x00000838 REG32SDMMC_TRANS_SPEED;//0x0000083C REG32SDMMC_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 deviceclass_dev; intindex; const struct mmc_host_ops *ops; struct mmc_pwrseq*pwrseq; unsigned intf_min; unsigned intf_max; unsigned intf_init; u32ocr_avail; u32ocr_avail_sdio;/* SDIO-specific OCR */ u32ocr_avail_sd;/* SD-specific OCR */ u32ocr_avail_mmc;/* MMC-specific OCR */ struct wakeup_source*ws;/* Enable consume of uevents */ u32max_current_330; u32max_current_300; u32max_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 карту, она будет вызывать именно эти функции которые определены в данной структуре.
Собственно это все что я хотел рассказать.
Ссылка на репозиторий с драйверами.
Ссылка на статью в где я собираю прошивку.
ссылка на оригинал статьи https://habr.com/ru/articles/782858/
Добавить комментарий