![](https://habrastorage.org/getpro/habr/upload_files/8b3/325/619/8b3325619512939e991fa1ca8127e703.jpg)
Hello my name is Dmitry. Recently I wrote article "Building firmware for Orange PI i96 (Orange PI 2g-iot) from scratch" . If you haven't read it yat, I highly recommend. And there I noticed that in order to build firware on current kernel, I have to rewrite drivers wirh new archetecture "Device tree". In this article I have revelate how I do it.
At first we need to notice that support of Orange PI i96 is exist in current kernel. But it very very limited. It have only Interupt driver (of timer), terminal driver, GPIO driver and Device Tree file of cose, and thats all. We cann't mount SD card as root directory, not to mention of working Wi-Fi. Fortunately we have all necessary drivers source in SDK kernel (3.10.62). But they dont use "Device tree" archetecture and we need rewrite they for use in new kernel.
What is Device Tree?
Earlier kernel inicializate devices by it's own. And since each single-board computer has its own set of devices, each had its own core. But now the list of devices is specified in the Device Tree file, and kernel is reading devices from this file. Changed only Device Tree file but not kernel.
This is Device Tree node for mmc bus. SD card connected via this bus. Device Tree file entirely you can see in my repository.
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";
};
Fist string (compatible = "rda,8810pl-mmc") use for identify the driver that is responsible for the device. Driver has string in of_device_id structure with same ID.
The next five lines are allocate resources for driver:
The address space where the device is located.
Device driver interrupt from timer.
GPIO pin number.
Device voltage regulator.
Device clock generator.
Some of resources are provided by other devices (referance look like &"name of device"). Our device load only after all devices witch provided resources has loaded. If at least one device not loaded, our device not loaded too.
Next comes a section with driver parameters. And last string this is device status.
Device address space
Device initialization, including initialization of address space, occurs in the Probe function. Through this address space you can access the device registers.
This is what the mmc bus looks like from the driver's point of view:
{
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;
All the driver can do is read a value from one of these registers or write something to these registers. This is all ways for interact with devices.
Address space is allocate by platform_get_resource function:
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
But it give us real address of device. Earlier computers actually accessed devices using their real addresses, but now they use virtual addresses. This addresses not coincide with the real ones at all.
Function ioremap is convert real address to virtual:
host->base = ioremap(res->start, resource_size(res));
After that we can access to device.
Interrupts
Interrupts is very huge teme. CPU devided by zero, interrupt. CPU has acsses to forbiden addres, interrupt. Even from program we can call interupt. But now we will consider external innterupts. This is innterupts from timer and GPIO pins.
Why driver use interupts from timer? If you program for Arduino, you must know that program devided to two parts. First this is incialization part, function "Setup". And port status calling, function "Loop". The operation of this function can be described as "delay", port calling, "delay", port calling again and again. As you understand, the analogue of Setup is the probe function. And analogue of Loop this is timer interrupt.
Driver is registrate interrupt handler, which will be calling when timer created interrupt:
ret = request_irq(host->irq, rda_mmc_irq, 0x0, mmc_hostname(mmc), host);
Function rda_mmc_irq this is interrupt handler, it will be calling with a certain frequency.
Why so difficult? This is because if we will use bunch of infinity cycle and delay. CPU can't do everything more except this. But timer allows to unload CPU.
If we write script for Arduino "like an adult", we need program timer for calling interupt, and move code from loop to interupt handler.
General Purpose Input Output ports (GPIO)
On Arduino GPIO pins accessed by numbers. But in Linux we need to get descriptor:
host->det_pin = gpiod_get(&pdev->dev, "detpin", GPIOD_IN);
The descriptor differs from the number that it exists in only one copy. If one driver got GPIO descriptor. Then no other driver will be able to receive the same descriptor until driver witch took return it by function gpiod_put:
gpiod_put(host->det_pin);
After we get GPIO descriptor we can control GPIO pin or set irq handler for it:
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);
If you open my i96 repository you can find GPIO driver rda-gpio_old.c, but GPIO driver exist in current Linux kernel. Why I had rewrite it? The fact is that with the driver that comes in the kernel, interruptions from pins did not work. Therefore, I had to remake the old driver for the new kernel.
Device interface
There are several ways a driver can interact with its operating system.
The driver can export one of it's functions:
EXPORT_SYMBOL(rda_mmc_set_sdio_irq);
And other driver can call this function. But this way of interaction is bad. Because it make driver that calls the exported function become depending from other. You can view the dependencies of a driver (or module as it is called in Linux) using the command:
modinfo "driver name"
The second way of interaction is to create a standard device interface that is defined in the operating system. In our case, this standard interface is 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;
....
}
I did not give the entire structure because it is too long.
mmc_host is registered by the function:
mmc_add_host(mmc);
Of course, the interface has all sorts of important and not so important parameters. But the most important parameter is 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,
};
If we were talking in C++ terms, we would call this interface methods. But because we use C only, then this structure contains pointers to the functions of our driver. And when the OS wants to read something, or write something to the SD card, it will call functions that are defined in this structure.
That's all.
Links:
Building firmware for Orange PI i96 (Orange PI 2g-iot) from scratch