Introduction
In the previous article, we briefly introduced the difference between bare-metal drivers and device drivers, as well as the driver architecture of character devices in the Linux kernel. Without an operating system, upper-layer applications directly access the driver interface, and application engineers need to know the driver interface for each device. When accessing Flash, they call functions like Drv_FlashRead()/Drv_FlashWrite(); when using serial ports, they call Drv_SerialSend()/Drv_SerialWrite() and so on. In simple terms, the number of different read/write devices mounted corresponds to the number of different read/write interface functions that upper-layer applications must use. In contrast, under the Linux operating system, the user layer does not need to know these classifications; for application engineers, read and write operations only require the read()/write() two functions, without needing to care about how many types of read/write interfaces exist at the lower level. When reading from Flash, they use read(“flash”,…,…); when reading from the serial port, they use read(“serial”,…,…)(the quotes represent the file name/device name as reflected by the driver to the upper layer, and the ellipsis represents other parameters). The operating system and file system will distinguish which underlying driver function to call based on the different parameters passed in. This process will involve the major and minor device numbers and the file_operations structure mentioned earlier.
LED Interface under Bare-Metal Driver
Here we will assume that the LED is controlled via a GPIO port.
When the upper-layer application needs to operate the LED, it can call these three functions. It seems quite simple; as long as the lower level implements these three functions, it can work. However, the upper-layer application must clearly understand the functions of these three functions. This is only for the LED; other devices have their own interface functions, so application engineers need to clearly know the driver interface for each device, which is obviously not very convenient.
Device Drivers under Linux
As mentioned earlier, drivers with an operating system need to be implemented according to the driver architecture provided by the operating system. Therefore, device drivers under Linux must also be implemented according to a certain architecture. Similarly, when implementing the LED driver, we need to implement the initialization, lighting, and extinguishing operations for the LED; however, the operating system requires a unified driver format for implementation, so we cannot directly provide upper-layer applications with driver interfaces like Drv_LedInit(), Drv_LedOn(), Drv_LedOff(); when working under an operating system, one must comply with the requirements of the operating system. Referring to the driver architecture diagram from the previous article, we can see that the user space accesses the underlying devices through system calls. The operating system requires special information (device numbers) to identify which device the application layer wants to access, so the operating system requires each driver to have its own device number, allowing the operating system to find you. Otherwise, the upper-layer application is just a simple read() system call, and the operation could become chaotic in an instant; with so many devices at the lower level, how could it quickly locate the corresponding interface? If they could converse, it might look like this: Application layer: Hi, Operating System! Please call Flash for me; I need to get some data from it. Operating System: Oh, okay! Let me find it for you, just a moment. Then the operating system opens its management data, only to find that it cannot complete this task because it has no idea who Flash is; many devices look similar, and it cannot distinguish between Flash, LED, and serial port; let alone some twin brothers (different models of the same type of device), it finds that there are also cases of having the same name. At this point, the operating system came up with a solution: it assigns an ID to every device, like an ID card number, as a unique identifier. Moreover, to improve efficiency, it stipulates that all devices of the same type (same kind) have the same major device number, and the minor device number is used to specifically distinguish a certain device, just like the first three digits of an ID card number distinguish provinces, followed by three digits that distinguish cities. Therefore, when designing device drivers under Linux, we need to first allocate device numbers.
Then, in order to manage these devices more conveniently, the operating system defines a data structure for managing each type of device (which can be understood as a table). When the driver registers with the system, it must first fill out this table, so that when the operating system finds the driver, it can quickly understand the device information and functionality (operation interfaces).
When designing the driver, you must complete the filling out of this table, showcasing the skills possessed by the device to the operating system (struct file_operations), and then provide this table to the operating system, so that the operating system can efficiently manage the devices. Thus, when the upper-layer application wants to access a certain device, the operating system can quickly find this table based on the device number and locate the corresponding operation (through struct file_operations).
The above provides a relatively complete driver code. When we load the driver through insmod, the system will automatically recognize and execute the module_init() function, then the driver will execute the Drv_LedInit() function, completing the allocation of major and minor device numbers and filling the cdev structure, adding cdev to the system. From the code, we can see that the previously mentioned Drv_LedOn() and Drv_LedOff() functions in bare-metal drivers are placed in the ioctl() function, so that when the upper-layer application uses the ioctl() system call, it can control the LED without needing to care about how the underlying LED’s on/off operations are named and implemented. Therefore, by now we should have a general understanding that drivers with an operating system need to do relatively more work than bare-metal drivers, mainly to achieve a unified interface for the operating system, providing convenience for upper-layer applications.