Basic knowledge that embedded underlying drivers need to know

Let me talk about the conclusion first, it can, it must be possible, it must be possible!

However, the key point of the problem is persistence. In the programmer industry, it is usually 10 o'clock when you get home from get off work, and then watch two hours of boring learning videos. I think most people will not be able to persist.

However, I want to say, "Linux driver development is actually not difficult. The difficulty is that there are no reliable books and teaching videos on the market." It’s the experience value, and the question is that others have designed it this way, as long as it can be used, it’s a ghost if he can explain it to you.

The reason why linux driver development is not difficult is because a very simple and common driver framework has been implemented in the kernel, which has not changed much since version 2.6, which is enough to show that the driver framework is excellent. However, the current books and teaching videos on the market do not pay enough attention to explaining the content of the driver framework, but only focus on the details of a single driver. As a microcontroller engineer, the difference between you and a linux driver engineer is only a driver framework.

Having said so much, it's time for the dry goods. I still stick to my consistent philosophy, learning anything new should be from far to near, first grasp the overall situation as a whole, and then delve into the details. In principle, as long as you carefully read the following content, you are almost an introduction. Without further ado, let's serve.

Prerequisite knowledge:

1. The overall linux driver module is designed with object-oriented thinking, and each node in the driver is described as an object.

2. Objects are usually described in the form of a structure. The variables in the structure represent the properties of the object, and the function pointers represent the behavior of the object.

3. The inheritance relationship between objects is embodied in the form of an embedded parent class structure object.

4. The similar objects in the driver are generally connected in series in the form of a linked list, and the linked list is expressed in the form of an embedded struct list_head.

5. The container_of macro is widely used in the kernel to realize the function of obtaining the starting address of the entire structure through the addresses of the internal elements of the known structure object.

Example: inheritance implementation method

/* Parent class */
struct ANIMAL { int age; int weight; }; /* Subclass */ struct DOG { struct ANIMAL animal; /* Realize the inheritance relationship by embedding the parent class object */ int variety; }; /* Obtain the start address of the dog object through the address of the animal object in the dog object*/ struct DOG *dog = container_of(ptr_animal, struct DOG, animal);











Example: How to use a linked list

struct xxx_dev { int id; int num; struct list_head node; /* By embedding the struct list_head node in the object, the linked list function is realized */ }; /* When traversing the linked list, the known node address can be obtained with the help of the container_of macro The starting address of the outer object xxx_dev. */ struct xxx_dev *dev = container_of(prt_node, struct xxx_dev, node);






Core driver framework:

Different components are highly abstracted in the Linux kernel, and the "bus-device-driver" model is used to organize a certain layer of driver code, and multiple layers can be superimposed. The model structure is as follows, the bus acts as a bridge and link, connecting devices and corresponding drivers.

picture

(Core Driver Framework)

Devices: Hardware devices or virtual devices mounted on various buses, such as temperature and humidity sensors mounted on the I2C bus, I2C controllers mounted on the platform bus, etc., are abstractly described as device objects. The structure struct device is used to represent the base class of the device, which is used to describe various parameters of the hardware device. Specifically, various types of devices can be inherited and extended by embedding the base class object.

Driver: A collection of variables that record the state of the hardware device and functions that control the work of the hardware, responsible for initializing the hardware device, and providing an operation interface to the upper layer code, such as various bus controller drivers in the SOC, and external bus device drivers, etc. Use the structure struct device_driver to represent the driver base class, and specific types of drivers can implement inheritance and extension by embedding base class objects.

Bus: Represents various physical or virtual buses. As a bridge and link, the bus is used to connect devices and drivers, and provides functions such as driver registration, device discovery, and device registration/uninstallation. Common buses such as: platform bus, I2C bus, SPI bus, USB bus, etc. Among them, the platform bus is the bus type that driver engineers have been in contact with the longest. Some books call the platform bus a virtual bus, and say that those without actual physical buses are classified as platform buses. I think this statement is wrong. The platform bus should refer to the buses used for internal interconnection in the SOC, such as AXI, AHB, APB buses in the ARM SOC, etc. A large number of controllers connected to these buses can be directly accessed through address mapping, so these controllers are generally is called a platform device, and the connected bus is called a platform bus. Use struct bus_type to represent the base class of the bus, and various types of buses can be inherited and extended by embedding the base class.

Driver framework inheritance relationship:

As mentioned above, the base class of each object of "bus-device-driver" is defined in the Linux driver framework, and other subclasses are inherited from the base class. The inheritance relationship is as follows:

picture

(inheritance relationship)

User interface:

All hardware is born to achieve certain specific functions. Drivers operate hardware devices to provide services to upper-layer applications, but the Linux kernel divides the operating space into kernel space (kernel space) and user space (user space) for safety. space) in two parts, where the kernel code runs in the kernel space, and the application program runs in the user space. The application program calls the kernel and various services provided by the driver through the system call interface. The schematic diagram is as follows:

picture

(system call diagram)

However, there are various types of hardware, and the number of corresponding drivers is too numerous to enumerate, and it is still changing. It is impossible to provide a system call interface for each driver. Fortunately, the operation steps of most devices are very similar, which can be summarized as follows: For: initialization, reading, writing, closing and other basic steps. According to the different functional attributes and usage methods of the devices, the devices are roughly divided into three categories in the kernel: character devices, block devices and network devices. Among them, character devices and block devices reuse the system call interfaces (open, release, read, write, ioctl and other interfaces) provided by VFS (virtual file system) because the operation steps are very similar to file operations, which are used in the kernel respectively. struct cdev and struct block_device indicate that in the user space, they exist as special files in the /dev directory, and you can use ls -ls /dev to view the attributes of each file, among which the attributes crw-rw-rw- are characters starting with 'c' Device, attribute brw-rw----block device starting with b.

cros@cros-pc:~$ ls -ls /dev/
total 0
0 crw------- 1 root root 5, 1 May 28 00:04 console
0 crw-rw-rw- 1 root root 1, 7 May 28 00:04 full
0 crw-rw---- 1 root kvm 10, 232 May 28 00:04 kvm
0 brw-rw---- 1 root disk 8, 0 May 28 00:04 sda
0 brw-rw---- 1 root disk 8, 1 May 28 00:04 sda1
0 brw-rw---- 1 root disk 8, 2 May 28 00:04 sda2

Network devices cannot reuse VFS system interfaces due to different operation methods, so they can only provide several exclusive system call interfaces. The first parameter of the following SYSCALL_DEFINEx macro is the name of the system call:

cros@cros-pc:~/home/cros/kernel$ grep -rn "SYSCALL_DEFINE*" net/socket.c
1213:SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
1254:SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol,
1363:SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
1392:SYSCALL_DEFINE2(listen, int, fd, int, backlog)
1425:SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
1506:SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
1524:SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
1556:SYSCALL_DEFINE3(getsockname, int, fd, struct sockaddr __user *, usockaddr,
1587:SYSCALL_DEFINE3(getpeername, int, fd, struct sockaddr __user *, usockaddr,
1619:SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
1663:SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
1675:SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
1720:SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size,
1731:SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
1765:SYSCALL_DEFINE5(getsockopt, int, fd, int, level, int, optname,
1795:SYSCALL_DEFINE2(shutdown, int, fd, int, how)
1988:SYSCALL_DEFINE3(sendmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags)
2057:SYSCALL_DEFINE4(sendmmsg, int, fd, struct mmsghdr __user *, mmsg,
2154:SYSCALL_DEFINE3(recvmsg, int, fd, struct user_msghdr __user *, msg,
2272:SYSCALL_DEFINE5(recvmmsg, int, fd, struct mmsghdr __user *, mmsg,
2317:SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)

In addition, in order to facilitate the user layer to operate the device, the kernel introduces the sys file system, located in the /sys directory, which describes the entire driver framework from different perspectives such as bus, device, and class, as follows:

cros@cros-pc:~$ ls -ls /sys/
total 0
0 drwxr-xr-x 2 root root 0 6月 2 10:25 block
0 drwxr-xr-x 43 root root 0 6月 2 10:25 bus
0 drwxr-xr-x 68 root root 0 6月 2 10:25 class
0 drwxr-xr-x 4 root root 0 6月 2 10:25 dev
0 drwxr-xr-x 24 root root 0 5月 28 00:04 devices
0 drwxr-xr-x 6 root root 0 5月 28 00:04 firmware
0 drwxr-xr-x 10 root root 0 5月 28 00:04 fs
0 drwxr-xr-x 2 root root 0 6月 2 10:25 hypervisor
0 drwxr-xr-x 15 root root 0 5月 28 00:04 kernel
0 drwxr-xr-x 182 root root 0 6月 2 10:25 module
0 drwxr-xr-x 3 root root 0 6月 2 10:25 power

The complete user interface is as follows:

picture

(User Interface Framework)

Code template:

Kernel module template:

The kernel driver module basically registers initialization functions and uninstall functions through the following templates as the entry and exit of the driver code.

/* Kernel module initialization function*/
static int __init xxx_init(void)
{ } /* Kernel module logout function*/ static void __exit xxx_exit(void) { } /* Register the initialization function so that it is automatically executed when the driver is installed automatically or manually Initialization function */ module_init(xxx_init); /* Register the logout function, so that when the driver is installed automatically or manually, the logout function is automatically executed */ module_exit(xxx_exit);








Bus code template:

/* Bus type structure*/
struct bus_type platform_bus_type = { .name = "platform", .dev_groups = platform_dev_groups, .match = platform_match, .uevent = platform_uevent, .pm = &platform_dev_pm_ops, }; /* Kernel module initialization function*/ int __init platform_bus_init(void) { int error; /* Take the platform bus as an example to demonstrate the bus registration process*/ error = bus_register(&platform_bus_type); return error; } /* Register the initialization function so that when the driver is installed automatically or manually, Automatically execute the initialization function */ module_init(platform_bus_init); /* Because the platform bus is an essential basic bus in the kernel, there is no uninstall function */



















Driver code template:

/* Device driver structure */
static struct platform_driver at91_twi_driver = { /* The probe function is responsible for parsing the parameters provided by the device object, performing hardware initialization, and providing an operation interface to the upper layer */ .probe = at91_twi_probe, /* The operation performed by device unloading */ .remove = at91_twi_remove, .id_table = at91_twi_devtypes .driver = { .name = "at91_i2c", /* field used to match with the device*/ .of_match_table = of_match_ptr(atmel_twi_dt_ids), .pm = at91_twi_pm_ops, }, } ; /* Kernel module initialization function*/ static int __init at91_twi_init(void) { /* Take the platform device driver as an example to demonstrate the driver registration process*/ return platform_driver_register(&at91_twi_driver); } /* Kernel module logout function*/ static void __exit at91_twi_exit(void) {





















/* Take the platform device driver as an example to demonstrate the driver uninstallation process*/
platform_driver_unregister(&at91_twi_driver);
}
/* Register the initialization function, so that when the driver is installed automatically or manually, the initialization function will be automatically executed */
module_init(at91_twi_init);
/* Registration cancellation Function, so that when the driver is installed automatically or manually, the logout function is automatically executed */
module_exit(at91_twi_exit);

Device code template:

PS: In the new version of the kernel, because of the introduction of the device tree, most devices are described in the device tree. During the kernel initialization process, the device tree will be automatically parsed, and the device will be generated and registered, so the following code is currently rare. Here Just to explain the principle and basic flow.

/* Platform device structure*/
static struct platform_device s3c24xx_uart_device0 = { .id = 0, }; static struct platform_device s3c24xx_uart_device1 = { .id = 1, }; static struct platform_device s3c24xx_uart_device2 = { .id = 2, }; static struct platform_device s3c24xx_uart_device3 = { .id = 3, }; struct platform_device *s3c24xx_uart_src[4] = { &s3c24xx_uart_device0, &s3c24xx_uart_device1, &s3c24xx_uart_device2, &s3c24xx_uart_device3, } ; /* module initialization function */ static int __init s3c_arch_init(void) { int ret; /* to Take the platform device as an example to demonstrate the device registration process*/























ret = platform_add_devices(s3c24xx_uart_src, nr_uarts);
return ret;
}

/* Register the module initialization function, there are many macros with similar functions, and the names are different*/
arch_initcall(s3c_arch_init);

Platform device driver framework:

The platform device driver is the most contacted and modified type of driver by developers, because it mainly includes various bus controllers built into the SOC, as well as built-in functional modules such as PWM, RTC, and WDT. It is basically related to the chip, so each SOC needs to develop the corresponding driver separately.

picture

(Examples of platform device drivers)

Bus device driver framework:

The bus device driver is more complicated than the platform device device, and generally includes two layers of drivers, the bottom layer is the bus controller driver, and the upper layer is the bus device driver. In addition, because there are various bus controllers, in order to unify the programming interface of the upper layer, a core layer will be added in the middle of the driver to realize the abstraction of the bus controller and provide a unified bus operation interface for the upper layer, similar to the one in the design pattern adapter pattern. Typical examples are struct i2c_adapter in the I2C driver framework and struct spi_master in the SPI driver framework. The following is the I2C driver framework, you can take a closer look.

picture

(I2C device driver framework)

There are also many device drivers that support hot plugging in the kernel, such as USB drivers. The same USB interface may or may not be connected to a device, may be connected to a USB flash drive, or may be connected to a mouse. For example, the mmc driver may have an MMC card inserted into the mmc interface, an SD card may also be inserted, and an SDIO network card may also be inserted. We can't assume what kind of device is connected to the interface, but we can judge whether the device is connected through the level signal. In order to be able to determine what device is connected to the interface and what parameters the device has, generally the corresponding association will specify a complete set of protocol standards (such as USB protocol, SD protocol). As long as the driver code communicates with the device according to the protocol, obtains the information provided by the other party, and then analyzes it according to the protocol, the detailed information of the connected hardware can be obtained. Then load the corresponding driver to use the hardware normally. The following is the mmc driver framework. Compared with the I2C driver framework, it mainly has more protocol analysis parts. You can take a closer look!

picture

(MMC driver framework)

Summarize:

It is still important to emphasize that learning new things must be from far to near, and gradually deepened. First know what each module does, then learn how to use it, and finally delve into the working principle and how to modify it. This is especially true for learning driver development. Familiarity with the basic driver framework and the specific framework of each module is the first step you need to do. The rest of the work is to configure registers and initialize hardware devices. Isn’t this what MCU engineers are doing now? ?

Guess you like

Origin blog.csdn.net/weixin_41114301/article/details/132548367