1. Review
In previous chapters, we learned how to write a character device driver, such as 05_char_device/mychardev.c. The general process is as follows:
static int __init mychardev_init(void)
{
// 1. Allocate device number
alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
// 2. Initialize and register cdev
cdev_init(&cdev_test, &cdev_fops_test);
cdev_add(&cdev_test, dev_num, 1);
// 3. Create device class and device node
class_test = class_create(THIS_MODULE, CLASS_NAME);
device_create(class_test, NULL, dev_num, NULL, DEV_NAME);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
This code can run, but there is a fatal problem:
Hardware information is mixed with driver logic!
For example, if we need to operate hardware such as GPIO, I2C, SPI, we need to hardcode register addresses, interrupt numbers, etc. in the driver:
#define GPIO_BASE_ADDR 0x12340000 // Hardware address hardcoded
#define IRQ_NUM 56 // Interrupt number hardcoded2
The problem is:
- If we switch to a different chip, the GPIO address changes, requiring driver code modification.
- If we want to modify hardware parameters without recompiling the driver, it's not possible.
This clearly does not conform to Linux kernel's design philosophy of "driver and hardware information separation".
2. Bus-Device-Driver Model
To solve the above problems, the Linux kernel designed a Bus Model (Bus-Device-Driver Model):
Device: Describes hardware resources
- "I am a GPIO controller"
- "My register base address is 0x12340000"
- "My interrupt number is 56"
Driver: Implements hardware operation logic
- "I can control GPIO devices"
- "As long as you tell me the address and interrupt number, I can work"
Bus: Responsible for matching devices and drivers
- "Compatible string matches, matching successful!"
- "Driver, here is your device, go to work!"
3. What is Platform Bus
There are many types of buses in the Linux kernel:
| Bus Type | Corresponding Hardware |
|---|---|
i2c_bus_type | I2C bus devices |
spi_bus_type | SPI bus devices |
usb_bus_type | USB bus devices |
platform_bus_type | Pseudo-bus (Platform bus) |
Platform bus is a "pseudo-bus". It is not a real physical bus (unlike I2C, SPI which have specific hardware bus protocols), but a virtual bus created by the Linux kernel specifically for managing devices that cannot be classified under other buses.
Which devices use Platform bus?
Almost all peripherals directly attached to the SoC chip use Platform bus:
GPIOcontrollerUARTserial controllerRTCreal-time clockWatchdogADC/DACanalog-to-digital converterPWMpulse width modulatorDMAcontroller
Why do they use Platform bus?
Because they have no real bus connection, directly accessed through Memory-Mapped I/O (MMIO), without complex bus protocols.
4. Platform Bus Workflow
1. Define nodes in Device Tree
You need to write hardware description information in the device tree file:
/ {
mychardev@0 {
compatible = "lckfb,mychardev"; // Vendor name, device name
reg = <0x12340000 0x1000>; // Register address range
interrupts = <56>; // Interrupt number
dev-id = <0xFFA8>; // Device ID
status = "okay"; // Device status
};
};2
3
4
5
6
7
8
9
2. Parse Device Tree
This step is done automatically by the kernel, we don't need to write any code!
When the kernel starts, it will automatically convert nodes in the device tree into struct platform_device structures and register them on the Platform bus:
- Read device tree file (
.dtb) - Parse each node in the device tree
- Automatically create corresponding
struct platform_devicestructure for each node - Automatically call
platform_device_register()to register on Platform bus
// Kernel automatically creates this structure based on device tree
struct platform_device my_pdev = {
.name = "mychardev",
.id = 0,
.resource = {
// Kernel converts reg = <0x12340000 0x1000> from device tree into this
{
.start = 0x12340000, // Register start address
.end = 0x12340FFF, // Register end address (0x12340000 + 0x1000 - 1)
.flags = IORESOURCE_MEM, // Resource type: memory
},
// Kernel converts interrupts = <56> from device tree into this
{
.start = 56, // Interrupt number
.flags = IORESOURCE_IRQ, // Resource type: interrupt
},
},
// Device tree dev-id = <0xFFA8> is also saved, can be read via of_property_read_u32()
};
// Kernel automatically calls this function to register device on Platform bus
platform_device_register(&my_pdev);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
What the kernel does for us:
- Converts
reg = <0x12340000 0x1000>to memory resource (IORESOURCE_MEM) - Converts
interrupts = <56>to interrupt resource (IORESOURCE_IRQ) - Saves
dev-id = <0xFFA8>as device tree property, which the driver can read
3. Driver Registration Matching with Device
The Platform driver we write will register with the kernel:
The code below is what we need to write in the driver!
static struct platform_driver my_driver = {
.probe = my_probe, // Function called when device matching succeeds
.remove = my_remove, // Function called when device is removed
.driver = {
.name = "mychardev", // Driver name
.of_match_table = of_match_ptr(my_of_match), // Device tree matching table
},
};
// Define matching table
static const struct of_device_id my_of_match[] = {
{ .compatible = "lckfb,mychardev" }, // Match node with compatible = "lckfb,mychardev" in device tree
{ /* Terminator, must be present */ }
};
// Register driver with kernel
module_platform_driver(my_driver);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4. Bus Matching and Calling probe
When the kernel finds:
- Device's
compatible = "lckfb,mychardev"(parameter from device tree)** - Driver's
of_match_tablealso has"lckfb,mychardev"(parameter from driver program)**
The Platform bus will:
- Call the driver's
probe()function - Pass the matched
platform_devicepointer toprobe() - Driver obtains hardware resources and initializes device in
probe()
5. Framework We Need to Write
What we need to do is as follows:
- Define matching table (matching
compatiblefield in device tree; if matching succeeds, the driver is called)
static const struct of_device_id my_of_match[] = {
{ .compatible = "lckfb,mychardev" }, // Must be exactly the same as compatible in device tree!
{ /* Terminator */ }
};
MODULE_DEVICE_TABLE(of, my_of_match); // Export matching table to kernel2
3
4
5
- Implement
probefunction (automatically called when device matching succeeds)
static int my_probe(struct platform_device *pdev)
{
struct resource *res;
u32 dev_id;
int irq;
// Get register address resource from platform_device
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
return -ENODEV;
}
// Get interrupt number from platform_device
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
return irq;
}
// Read custom property dev-id from device tree
if (of_property_read_u32(pdev->dev.of_node, "dev-id", &dev_id)) {
return -EINVAL;
}
// Allocate character device number.......
// Initialize cdev.......
// Create device node.......
// ioremap register address.......
// Register interrupt handler.......
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Key function descriptions:
- Use
platform_get_resource()to get register address - Use
platform_get_irq()to get interrupt number - Use
of_property_read_u32()to read custom properties
- Implement
removefunction (called when device is removed or driver is unloaded)
static int my_remove(struct platform_device *pdev)
{
// Release resources:
// 1. Unregister character device
// 2. Release interrupt
// 3. iounmap address mapping
return 0;
}2
3
4
5
6
7
8
9
- Define
platform_driverstructure
static struct platform_driver my_driver = {
.probe = my_probe, // Called when matching succeeds
.remove = my_remove, // Called when removed
.driver = {
.name = "mychardev", // Driver name
.of_match_table = my_of_match, // Specify matching table
},
};2
3
4
5
6
7
8
- Register driver (automatically implements
initandexit)
module_platform_driver(my_driver);6. Why Use Platform Driver
Compared to traditional character device drivers, Platform drivers:
| Traditional Character Device Driver | Platform Driver | |
|---|---|---|
| Hardware Information | Hardcoded in driver code | Described in device tree |
| Code Reusability | Poor (address changes require code modification) | Strong (only need to change device tree) |
| Multi-device Support | Need to copy-paste code | One driver automatically supports multiple devices |
| Initialization Function | module_init() | probe() (automatically called) |
| Device Matching | None | Platform bus automatically matches |
| Get Hardware Resources | Manual macro definition | platform_get_resource() |
| Standardization | Non-standard | Linux kernel standard practice |