Timeline
Timeline
2025-11-11
init
This article introduces the basics of Linux character device driver development, focusing on the composition and operation of device numbers. The article first explains the roles of major and minor device numbers, as well as the device number dev_The storage format of t in the Linux kernel (the high 12 bits are the major device number, and the low 20 bits are the minor device number). Subsequently, the usage of device number operation macros (MINORBITS, MINORMASK, MAJOR, MINOR, MKDEV) is introduced in detail, and the static device number allocation function register is compared._chrdev_region() and the dynamic device number allocation function alloc_chrdev_The parameters, return value, and applicable scenarios of region(), and also explains the device number release function unregister._The usage of chrdev_region(). Finally, the article introduces the core structure cdev of character devices, including the meaning of its fields and its registration and usage in the kernel, laying the foundation for later writing complete character device drivers.
Linux Driver Notes
| Table of Contents | Links |
|---|---|
| 1. Linux Driver Framework | |
| 2. Linux Driver Loading Logic | |
| 3. Character Device Basics | |
| 4. Concurrency and Race Conditions | |
| 5. Advanced Character Device Topics | |
| 6. Interrupts | |
| 7. Platform Bus | |
| 8. Device Tree | |
| 9. Device Model | |
| 10. Hotplug | |
| 11. pinctrl Subsystem | |
| 12. GPIO subsystem | |
| 13. Input subsystem | |
| 14. 1-Wire | |
| 15. I2C | |
| 16. SPI | |
| 17. UART | |
| 18. PWM | |
| 19. RTC | |
| 20. Watchdog | |
| 21. CAN | |
| 22. Network devices | |
| 23. ADC | |
| 24. IIO | |
| 25. USB | |
| 26. LCD |
Device ID
In Linux systems, every device has a corresponding device number, and device numbers havemajor device numberandsecondary device numberThe distinction:
- major device number: IdentifierDevice Type。
- secondary device number: atDistinguish between multiple device instances managed by the same driver
Example:
- Major device number 13 represents input subsystem(Handling input devices).
- Minor device number 64 →
/dev/input/event0(possibly a USB keyboard)- Minor device number 65 →
/dev/input/event1(possibly a USB mouse)- Major device number 188 represents USB serial device。
- Secondary device number 0 →
/dev/ttyUSB0- Secondary device number 1 →
/dev/ttyUSB1
BeforeBefore registering a character device driver, you need to apply for a device number first.。
include/linux/types.h
1234567 | typedef u32 __kernel_dev_t;typedef __kernel_fd_set fd_set;typedef __kernel_dev_t dev_t;typedef __kernel_ino_t ino_t;typedef __kernel_mode_t mode_t;typedef unsigned short umode_t; |
The device number dev_t is of type u32,The high 12 bits are the master device number.,The lower 20 bits are the minor device number.。
Device number operation macros
include/linux/kdev_t.h
123456 |
- MINORBITSIndicates the number of bits of the minor device number, 20 bits in total.
- MINORMASKMinor device number mask, used to extract the minor device number from dev_t (
(dev) & MINORMASK) - MAJORIndicates from dev_t, get the major device number, essentially shifting dev_t shifted right by 20 bits
- MINORIndicates getting the minor device number from dev_t, essentially taking the value of the lower 20 bits.
- MKDEVUsed to combine the major device number and minor device number into a dev_t type device number.
Device number allocation function
In Linux drivers, the following two methods can be used to allocate device numbers.
include/linux/fs.h
123 | extern int register_chrdev_region(dev_t, unsigned, const char *);extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *); |
register_chrdev_region()
Through the
register_chrdev_region(dev_t from, unsigned count, const char *name)The function performs static device number allocation.- Function prototype:
1register_chrdev_region(dev_t from, unsigned count, const char *name)- Function purpose: Statically allocate device numbers, applying for a pre-specified device number.
- Parameter meaning:
from: A custom dev_t type device number, for example, MKDEV(100,0) indicates a starting major device number of 100 and a starting minor device number of 0.count: The number of minor devices, indicating how many minor device numbers there are under the same major device number.name: the name of the device to apply for
- Function return value: Returns 0 on success, and a negative number on failure.
Note that statically allocated device numbers are limited:
include/linux/fs.hdefined in:
1234567 /* fs/char_dev.c *//* Marks the bottom of the first segment of free char majors *//* Marks the top and bottom of the second segment of free char majors */
alloc_chrdev_region()
- Through the
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char* name)The function performs dynamic device number allocation.
- Function prototype:
1 | alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name) |
Function purpose: Dynamically allocate device numbers. The kernel automatically assigns an unused device number. Compared with static allocation, dynamic allocation avoids conflicts caused by registering the same device number.
Parameter meaning:
dev *: will store the allocated device number in the dev variablebaseminor: the starting address of the minor device number. Minor device numbers usually start from 0, so this parameter is generally set to 0.count: the number of devices to apply forname: the name of the device to apply for
Function return value: Returns 0 on success, and a negative number on failure.
Device number release function
unregister_chrdev_region()
include/linux/fs.h
1 | extern void unregister_chrdev_region(dev_t, unsigned); |
- Function: device number release function. After unregistering a character device, the device number must be released.
- Function parameters:
dev_tType: the device number to be releasedunsignedType: the number of device numbers to be released
example
my_driver.c
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465 | static int major = 0;static int minor = 0;module_param(major, int, S_IRUGO);module_param(minor, int, S_IRUGO);static dev_t dev_id;static int __init my_driver_init(void){ int ret; if (major) { dev_id = MKDEV(major, minor); printk(KERN_INFO "major from module_param: %d", MAJOR(dev_id)); printk(KERN_INFO "minor from module_param: %d", MINOR(dev_id)); ret = register_chrdev_region(dev_id, 1, "my_driver device"); if (ret < 0) { printk(KERN_ERR "register_chrdev_region error\n"); } else { printk(KERN_INFO "register_chrdev_region ok\n"); } } else { ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device"); if (ret < 0) { printk(KERN_ERR "alloc_chrdev_region error\n"); } else { printk(KERN_INFO "alloc_chrdev_region ok\n"); printk(KERN_INFO "major allocated: %d", MAJOR(dev_id)); printk(KERN_INFO "minor allocated: %d", MINOR(dev_id)); } } printk(KERN_INFO "my_driver: Module loaded\n"); return 0;}static void __exit my_driver_exit(void){ unregister_chrdev_region(dev_id, 1); printk(KERN_INFO "unregister_chrdev_region ok\n"); printk(KERN_INFO "my_driver: Module unloaded\n");}module_init(my_driver_init);module_exit(my_driver_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Zhao Hang");MODULE_DESCRIPTION("my_driver Kernel Module"); |
Test:
1234567891011121314151617181920212223242526272829303132 | ~ # insmod my_driver.ko[ 18.414274] my_driver: loading out-of-tree module taints kernel.[ 18.421489] alloc_chrdev_region ok[ 18.421652] major allocated: 511[ 18.421667] minor allocated: 0[ 18.421910] my_driver: Module loaded~ # cat /proc/devices | grep my_driver511 my_driver device~ # rmmod my_driver.ko[ 27.856484] unregister_chrdev_region ok[ 27.856638] my_driver: Module unloaded~ # cat /proc/devices | grep my_driver~ #~ # insmod my_driver.ko major=512 minor=0[ 16.721502] my_driver: loading out-of-tree module taints kernel.[ 16.728530] major from module_param: 512[ 16.728574] minor from module_param: 0[ 16.728753] CHRDEV "my_driver device" major requested (512) is greater than the maximum (511)[ 16.729588] register_chrdev_region error[ 16.729760] my_driver: Module loaded~ # rmmod my_driver.ko[ 36.863583] unregister_chrdev_region ok[ 36.864560] my_driver: Module unloaded~ # insmod my_driver.ko major=500 minor=0[ 40.101231] major from module_param: 500[ 40.101265] minor from module_param: 0[ 40.101430] register_chrdev_region ok[ 40.101657] my_driver: Module loaded~ # rmmod my_driver.ko[ 50.626705] unregister_chrdev_region ok[ 50.626876] my_driver: Module unloaded |
Register character device
include/linux/cdev.h
123456789101112131415161718192021222324252627282930313233343536373839 | /* SPDX-License-Identifier: GPL-2.0 */struct file_operations;struct inode;struct module;struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count;} __randomize_layout;void cdev_init(struct cdev *, const struct file_operations *);struct cdev *cdev_alloc(void);void cdev_put(struct cdev *p);int cdev_add(struct cdev *, dev_t, unsigned);void cdev_set_parent(struct cdev *p, struct kobject *kobj);int cdev_device_add(struct cdev *cdev, struct device *dev);void cdev_device_del(struct cdev *cdev, struct device *dev);void cdev_del(struct cdev *);void cd_forget(struct inode *); |
In C, if you only need to use a certain struct’s pointer, and you don’t need to know its internal members (i.e., you don’t dereference it or use sizeof on it), you can just do forward declaration:
1struct file_operations; // tells the compiler: there is a struct type called file_operationsThis way you can define a pointer:
1struct file_operations *fops;but you cannot access members (because the compiler doesn’t know the struct contents yet):
1fops->read(...); // ❌ Compilation error: incomplete typeThis is very common in header files, used to decouple dependencies and speed up compilation。
And
externYes Storage class specifier, used to declare variable or function of the external linkage attribute. Its purpose is to tell the compiler: “This variable/function is defined elsewhere; this is just a declaration.”
GCC/Clang attribute(
__randomize_layout),
for the kernel Struct layout randomization, increasing the difficulty for attackers to predict structure offsets, thereby improving kernel security.
cdev structure
| field | meaning |
|---|---|
kobj | inherited fromstruct kobject, so that character devices can be attached under sysfs (i.e./sys/class/...such as paths). |
owner | Points to the module that owns the device (THIS_MODULE), to prevent the device from still being in use when the module is unloaded. |
ops | Pointer to device operation function table (struct file_operations), define read/write/ioctl and other behaviors. |
list | Internal linked list, shares all thecdevof the inode (i.e./dev/xxxfile) passedinode->i_devicesLinked together to form a “back-reference linked list”. |
dev | Device number, type isdev_t(including the major device number and minor device number). |
count | indicates thiscdevThe number of consecutive device numbers controlled (usually 1). |
cdev_init
fs/char_dev.c
123456789101112131415 | /** * cdev_init() - initialize a cdev structure * @cdev: the structure to initialize * @fops: the file_operations for this device * * Initializes @cdev, remembering @fops, making it ready to add to the * system with cdev_add(). */void cdev_init(struct cdev *cdev, const struct file_operations *fops){ memset(cdev, 0, sizeof *cdev); INIT_LIST_HEAD(&cdev->list); kobject_init(&cdev->kobj, &ktype_cdev_default); cdev->ops = fops;} |
- Function: Initialize a statically allocated
struct cdev. Establish the connection between cdev and file_operations.
Note: call
cdev_initIt is best to call afterwardscdev_test.owner = THIS_MODULE;Pointing the owner field to this module can prevent the module from being unloaded while its operations are in use.
cdev_add
fs/char_dev.c
1234567891011121314151617181920212223242526272829 | /** * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this * device * * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. */int cdev_add(struct cdev *p, dev_t dev, unsigned count){ int error; p->dev = dev; p->count = count; if (WARN_ON(dev == WHITEOUT_DEV)) return -EBUSY; error = kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); if (error) return error; kobject_get(p->kobj.parent); return 0;} |
- Function: Add a cdev structure to the system, that is, add a character device.
cdev_map: Globalkobj_mapStructure (hash table)p: points tostruct cdev- Note:
cdevEmbeddedstruct kobject kobj, socdevcan be viewed as akobject
cdev_del
fs/char_dev.c
12345678910111213141516 | /** * cdev_del() - remove a cdev from the system * @p: the cdev structure to be removed * * cdev_del() removes @p from the system, possibly freeing the structure * itself. * * NOTE: This guarantees that cdev device will no longer be able to be * opened, however any cdevs already open will remain and their fops will * still be callable even after cdev_del returns. */void cdev_del(struct cdev *p){ cdev_unmap(p->dev, p->count); kobject_put(&p->kobj);} |
- Function: delete a character device in the system
example
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 | static int major = 0;static int minor = 0;module_param(major, int, S_IRUGO);module_param(minor, int, S_IRUGO);static dev_t dev_id;static struct cdev cdev_test;static struct file_operations cdev_test_ops = { .owner = THIS_MODULE,};static int __init my_driver_init(void){ int ret; if (major) { dev_id = MKDEV(major, minor); pr_info( "major from module_param: %d", MAJOR(dev_id)); pr_info( "minor from module_param: %d", MINOR(dev_id)); ret = register_chrdev_region(dev_id, 1, "my_driver device"); if (ret < 0) { pr_err( "register_chrdev_region error\n"); return ret; } else { pr_info( "register_chrdev_region ok\n"); } } else { ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device"); if (ret < 0) { pr_err( "alloc_chrdev_region error\n"); return ret; } else { pr_info( "alloc_chrdev_region ok\n"); pr_info( "major allocated: %d", MAJOR(dev_id)); pr_info( "minor allocated: %d", MINOR(dev_id)); } } cdev_init(&cdev_test, &cdev_test_ops); //Pointing the owner field to this module can prevent the module from being unloaded while its operations are in use. cdev_test.owner = THIS_MODULE; ret = cdev_add(&cdev_test, dev_id, 1); if (ret < 0) { pr_err( "cdev_add error\n"); unregister_chrdev_region(dev_id, 1); return ret; }else{ pr_info( "cdev_add ok\n"); } pr_info( "my_driver: Module loaded\n"); return 0;}static void __exit my_driver_exit(void){ cdev_del(&cdev_test); unregister_chrdev_region(dev_id, 1); pr_info( "unregister_chrdev_region ok\n"); pr_info( "my_driver: Module unloaded\n");}module_init(my_driver_init);module_exit(my_driver_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Zhao Hang");MODULE_DESCRIPTION("my_driver Kernel Module"); |
Note: delete the device first, then release the device number.
file_operations structure
Linux has a very important concept called ‘everything is a file’, meaning that devices in Linux are just like ordinary files. Accessing a device is like accessing a file. In applications, we can use system calls such as open, read, write, close, and ioctl to operate drivers. When we call the open function in an application, it ultimately executes the open function in the driver. Therefore, file_operations connects system calls with device drivers.
Calling the open function in the application
1 | fd = open("/dev/hello", O_RDWR); |
Executing the open function in the driver
123456789101112 | static int cdev_test_open(struct inode *, struct file*){ printk("This is cdev_test open"); return 0;}static struct file_operations cdev_test_ops={ .owner = THIS_MODULE, .open = cdev_test_open, .read = cdev_test_read, .write = cdev_test_write, .release = cdev_test_release} |
Definition of file_operations in Linux:
include/linux/fs.h
123456789101112131415161718192021222324252627282930313233343536373839404142 | struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void**); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); unsigned (*mmap_capabilities)(struct file *); ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); bool may_pollfree;} __randomize_layout; |
Common field analysis:
| Function pointer | User-space call | Function |
|---|---|---|
owner | — | Usually written asTHIS_MODULE, to prevent the module from being unloaded while in use. |
open | open() | Called when the device is opened, generally used for initialization or counting open times. |
release | close() | Called when the device is closed, used to release resources. |
read | read() | User reads data from the device. |
write | write() | User writes data to the device. |
unlocked_ioctl | ioctl() | User sends control commands. |
poll | poll()/select() | Implement non-blocking I/O (epoll mechanism) |
mmap | mmap() | Map physical memory to user space. |
llseek | lseek() | File position offset |
fasync | fcntl(F_SETFL, O_ASYNC) | Support asynchronous notification |
Return value convention:
ssize_t: >=0 success byte count, <0 errnoint/long: 0 on success, <0 errno- Pointer: success returns pointer, failure returns
ERR_PTR(-errno)
llseek
1 | loff_t (*llseek)(struct file *file, loff_t offset, int whence); |
- Function: File pointer offset operation (similar to lseek system call).
- Parameters:
file: File object.offset: Offset.whence:SEEK_SET: Relative to the beginning of the fileSEEK_CUR: Relative to the current file pointerSEEK_END: Relative to the end of the file
- Return value:
- New file pointer position (
loff_t) - On error, returns a negative value (e.g.
-EINVAL)
- New file pointer position (
read
1 | ssize_t (*read)(struct file *file, char __user *buf, size_t count, loff_t *pos); |
- Function: Read data from the file into user space.
- Parameters:
file: File object.buf: User-space buffer, used to receive data read from the kernel.count: Requested data transfer size (size of the user buffer).pos: Indicates the starting position for reading data from the file; it needs to be updated after reading.
- Return value:
- Success: actual number of bytes read
- Error: negative errno
read steps
- Prevent the read operation from exceeding the file size, and return end-of-file:
12 | if (*pos >= filesize) return 0; /* 0 means EOF */ |
- The number of bytes read must not exceed the file size. Adjust count appropriately:
12 | if (*pos + count > filesize) count = filesize - (*pos); |
- Find the starting position for reading:
1 | void *from = pos_to_address (*pos); /* convert pos into valid address */ |
- Copy data to the user-space buffer, returning an error on failure:
123 | sent = copy_to_user(buf, from, count);if (sent) return -EFAULT; |
- Advance the file’s current position by the number of bytes read, and return the number of bytes copied:
123 | *pos += count;return count; |
write
1 | ssize_t (*write)(struct file *file, const char __user *buf, size_t count, loff_t *pos); |
- Function: Write user data to the file/device.
- Parameters:
file: file objectbuf: user buffercount: length of data requested to be transferredpos: indicates the starting position in the file where the data should be written
- Return value:
- Success: number of bytes actually written
- Error: negative errno
write steps
- Check for errors or invalid requests from user space. This step is only meaningful if the device provides memory (EEPROM, I/O memory, etc.), which may have memory size limits:
123 | /* If an attempt is made to write beyond the end of the file, return an error. Here filesize corresponds to the size of the device memory (if any). */if ( *pos >= filesize ) return -EINVAL; |
- Adjust count for the remaining bytes so as not to exceed the file size. This step is also not required, related to the same condition as in step (1):
123 | /* The file size is forced to match the device memory size. */if (*pos + count > filesize) count = filesize - *pos; |
- Find the position where writing begins. This step is only relevant when the device has memory and the write() method is to write the specified data into it. Like step (2), this step is not required:
12 | /* Convert pos to a valid address*/void *from = pos_to_address( *pos ); |
- Copy data from user space and write it to the corresponding kernel space:
123456 | if (copy_from_user(dev->buffer, buf, count) != 0){ retval = -EFAULT; goto out;}/* Now move the data from dev->buffer to the physical device */ |
- Write to the physical device, returning an error on failure:
1234 | write_error = device_write(dev->buffer, count);if ( write_error ) return -EFAULT; |
- Move the current position of the file cursor by the number of bytes written. Finally, return the number of bytes copied:
123 | *pos += count;return count; |
mmap
1 | int (*mmap)(struct file *, struct vm_area_struct *); |
- Function: Support user-space memory mapping to the device.
- Parameters:
file: file objectvma: virtual memory area structure
- Return value:
- 0 success
- <0 error
mmap_supported_flags: Flags supported during mapping.
open
1 | int (*open)(struct inode *inode, struct file *file); |
- Function: When user space calls
open()It is called when opening a device or file. - Parameters:
inode: Points to the inode structure corresponding to the device, containing filesystem layer information and device number.file: Points to the file object (struct file), which stores file state, offset,private_dataetc.
- Return value:
0: Successfully opened<0: On error, returns the corresponding errno (such as-EBUSYindicates device busy,-ENOMEMout of memory)
- Typical Uses:
- Check whether the device is already in use.
- Initialize
file->private_datapointer - Increment the module reference count (
THIS_MODULE)
flush
1 | int (*flush)(struct file *file, fl_owner_t id); |
- Function: Called when the last reference to the file descriptor is closed, used to flush pending data in the device buffer.
- Parameters:
file: file objectid: The owner identifier of the file handle, usually the process corresponding to the file descriptor.
- Return value:
0: success<0: error
- Description:
- For most character device drivers,
flushno special implementation is needed, just return 0. - In block devices or network devices,
flushit may be used to commit cached data.
- For most character device drivers,
release
1 | int (*release)(struct inode *inode, struct file *file); |
- Function: When user space calls
close()It is called when closing the device or file. - Parameters:
inode: The inode corresponding to the devicefile: file object
- Return value:
0: Successfully closed<0: error
- Typical Uses:
- Release at
open()resources allocated when - Restore device state (e.g., mark as unoccupied)
- Reduce module reference count
- Release at
example
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192 | static int major = 0;module_param(major, int, S_IRUGO);MODULE_PARM_DESC(major, "cdev_test sample, major device number");static int minor = 0;module_param(minor, int, S_IRUGO);MODULE_PARM_DESC(minor, "cdev_test sample, minor device number");dev_t dev_num;struct cdev cdev;int cdev_test_open(struct inode *inode, struct file *file){ pr_info("cdev_test open"); return 0;}ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ pr_info("cdev_test read\n"); return 0;}ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t, loff_t *offset){ pr_info("cdev_test write\n"); return 0;}struct file_operations fops = { .owner = THIS_MODULE, .open = cdev_test_open, .read = cdev_test_read, .write = cdev_test_write,};static int __init cdev_test_init(void){ int err; if (major) { dev_num = MKDEV(major, minor); err = register_chrdev_region(dev_num, 1, "cdev test dev num"); if (err < 0) { pr_err("register_chrdev_region error\n"); goto chrdev_region_err; } pr_info("register_chrdev_region success\n"); } else { err = alloc_chrdev_region(&dev_num, 0, 1, "cdev test dev num"); if (err < 0) { pr_err("alloc_chrdev_region error\n"); goto chrdev_region_err; } pr_info("alloc_chrdev_region success\n"); } pr_info("dev_num: major[%d] minor[%d]", MAJOR(dev_num), MINOR(dev_num)); cdev_init(&cdev, &fops); pr_info("cdev_init success\n"); cdev.owner = THIS_MODULE; // Point owner to this module to prevent unloading the module during cdev operations err = cdev_add(&cdev, dev_num, 1); pr_info("cdev_add success\n"); if (err < 0) { pr_err("cdev_add error\n"); goto cdev_add_err; } return 0;cdev_add_err: unregister_chrdev_region(dev_num, 1);chrdev_region_err: return err;}static void __exit cdev_test_exit(void){ cdev_del(&cdev); pr_info("cdev is deleted\n"); unregister_chrdev_region(dev_num, 1); pr_info("chrdev_region unregister success\n");}module_init(cdev_test_init);module_exit(cdev_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is just a cdev test sample"); |
Device node
In the Linux operating system, everything is a file. ForFiles used for device access are called device nodes. By operating this “device file”, the program can operate the corresponding hardware.
1 | fd = open("/dev/hello", O_RDWR); |
This “device file” is the device node. Therefore, the Linux device node is a bridge between applications and drivers. Device nodes are created in the /dev directory.
12345678910111213141516 | [zhaohang@cyberboy linux_driver_learning]$ ll /devcrw------- 4,15 root 11 Nov 10:05 tty15crw------- 4,16 root 11 Nov 10:05 tty16crw------- 4,17 root 11 Nov 10:05 tty17crw------- 4,18 root 11 Nov 10:05 tty18crw------- 4,19 root 11 Nov 10:05 tty19crw------- 4,20 root 11 Nov 10:05 tty20crw------- 4,21 root 11 Nov 10:05 tty21crw------- 4,22 root 11 Nov 10:05 tty22crw------- 4,23 root 11 Nov 10:05 tty23crw------- 4,24 root 11 Nov 10:05 tty24crw------- 4,25 root 11 Nov 10:05 tty25crw------- 4,26 root 11 Nov 10:05 tty26crw------- 4,27 root 11 Nov 10:05 tty27crw------- 4,28 root 11 Nov 10:05 tty28crw------- 4,29 root 11 Nov 10:05 tty29 |
4,15Indicates the major device number and minor device number. In crw, c indicates a character device.
According to the different creation methods of device nodes, they are divided intoManually creating device nodesandAutomatically creating device nodes。
Ways to create nodes in Linux
Manually creating device nodes
Use the mknod command to manually create device nodes. The mknod command format is:
1 | $ mknod NAME TYPE MAJOR MINOR |
Parameter meaning:
NAME: The name of the node to createTYPE: b indicates a block device, c indicates a character device, p indicates a pipeMAJOR: The major device number of the device to linkMINOR: The minor device number of the device to link
For example: Use the following command to create a character device node named device_test, with the device’s major and minor device numbers being 236 and 0 respectively.
1 | $ mknod /dev/device_test c 236 0 |
For the above file_operations example:
123456789101112 | $ insmod cdev_test.ko[ 43.969734] cdev_test: loading out-of-tree module taints kernel.[ 43.979783] alloc_chrdev_region success[ 43.980242] dev_num: major[241] minor[0][ 43.980277] cdev_init success[ 43.981192] cdev_add success$ mknod /dev/cdev_test c 241 0$ ls /dev/cdev_test/dev/cdev_test$ cat /dev/cdev_test[ 68.723315] cdev_test open[ 68.723900] cdev_test read |
Automatic device node creation
Automatic device node creation is achieved using udev the mechanism.
udev is a user program, by detecting the hardware device status in the system, it cancreate or delete device files according to the hardware device status in the system.。
Automatically creating device nodesIn the driver, you first need to use the class_create() function to create a class, and the created class is located in the /sys/class/ directory.,Then use the device_create() function to create the corresponding device under this class., when the driver module is loaded, udev in user space will automatically respond and create device nodes based on the information under /sys/class/.
In embedded Linux, we use mdev,mdev is a simplified version of udev. When using a root filesystem built with busybox, busybox will automatically create mdev.
class_create function
linux/device/class.h
Use the header file linux/device/device.h because device.h references class.h.
12345678 | /* This is a #define to keep the compiler from merging different * instances of the __key variable */ |
- Function purpose: used to dynamically create a device class.
- Parameter meaning:
owner: A pointer to struct module type. Generally assigned THIS_MODULE.name: A char pointer, representing the name of the struct class variable to be created.
- Return value:
struct class *structure of type
class_destroy function
linux/device/class.h
1 | extern void class_destroy(struct class *cls); |
- Function purpose: used to delete a device class.
- Parameter meaning:
cls: callclass_create()returned by the functionstruct classpointer to the structure.
device_create function
linux/device/device.h
use class_After create creates the class, you also need to use device_The create function creates a device under the class.
123 | __printf(5, 6) struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); |
- Function purpose: used to create a device file under the class.
- Parameter meaning:
cls: Specifies the class to which the device to be created belongs.parent: Specifies the parent device of the device, or NULL if there is none.devt: Specifies the device number for creating the device.drvdata: Data passed to the device callback, or NULL if none.fmt: The device node name added to the system.
- Return value:
struct device *Type structure
device_destroy function
linux/device/device.h
1 | void device_destroy(struct class *cls, dev_t devt); |
- Function purpose: Used to delete a device in the class.
- Parameter meaning:
cls: Specifies the class to which the device to be created belongs.devt: Specifies the device number for creating the device.
example
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 | static int major = 0;module_param(major, int, S_IRUGO);MODULE_PARM_DESC(major, "mknod_test dev major num");static int minor = 0;module_param(minor, int, S_IRUGO);MODULE_PARM_DESC(minor, "mknod_test dev minor num");dev_t dev_num;int mknod_test_open(struct inode *inode, struct file *file){ pr_info("This is mknod_test open\n"); return 0;}ssize_t mknod_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ pr_info("This is mknod test read\n"); return 0;}int mknod_test_release (struct inode *inode, struct file *file){ pr_info("This is mknod test release\n"); return 0;}struct file_operations fops = { .owner = THIS_MODULE, .open = mknod_test_open, .read = mknod_test_read, .release = mknod_test_release,};struct cdev cdev;struct class *class;struct device *dev;static int __init mknod_test_init(void){ int err; pr_info("mknod_test module init\n"); if (major) { dev_num = MKDEV(major, minor); err = register_chrdev_region(dev_num, 1, "mknod test device num"); if (err < 0) { pr_err("register_chrdev_region error\n"); goto chrdev_region_err; } pr_info("register_chrdev_region success\n"); } else { err = alloc_chrdev_region(&dev_num, 0, 1, "mknod test device num"); if (err < 0) { pr_err("alloc_chrdev_region error\n"); goto chrdev_region_err; } pr_info("alloc_chrdev_region success\n"); } pr_info("dev_t dev_num: major[%d], minor[%d]\n", MAJOR(dev_num), MINOR(dev_num)); cdev_init(&cdev, &fops); cdev.owner = THIS_MODULE; err = cdev_add(&cdev, dev_num, 1); if (err < 0) { pr_err("cdev add error\n"); goto cdev_add_err; } class = class_create(THIS_MODULE, "chrdev"); if (IS_ERR(class)) { err = PTR_ERR(class); pr_err("class_create error\n"); goto class_create_err; } dev = device_create(class, NULL, dev_num, NULL, "mknod_test_device"); if(IS_ERR(dev)){ err = PTR_ERR(dev); pr_err("device create error\n"); goto device_create_err; } return 0; device_create_err: class_destroy(class);class_create_err: cdev_del(&cdev);cdev_add_err: unregister_chrdev_region(dev_num, 1);chrdev_region_err: return err;}static void __exit mknod_test_exit(void){ device_destroy(class, dev_num); class_destroy(class); cdev_del(&cdev); unregister_chrdev_region(dev_num, 1); pr_info("mknod_test module exit\n");}module_init(mknod_test_init);module_exit(mknod_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is just mknod test sample"); |
Test:
12345678910111213 | $ insmod mknod_test.ko[ 11.793662] mknod_test: loading out-of-tree module taints kernel.[ 11.803110] mknod_test module init[ 11.803371] alloc_chrdev_region success[ 11.804103] dev_t dev_num: major[241], minor[0]$ ls /dev/mknod_test_device/dev/mknod_test_device$ cat /dev/mknod_test_device[ 22.546620] This is mknod_test open[ 22.547827] This is mknod test read[ 22.548435] This is mknod test release$ rmmod mknod_test.ko[ 28.103036] mknod_test module exit |
Data exchange between user space and kernel space
The memory in kernel space and user space cannot access each other. However, many applications need to exchange data with the kernel, for example, applications useread()function to read data from the driver, and usewrite()function to write data to the driver. The above functionality requires the use ofcopy_from_user()andcopy_to_user()two functions to complete.
copy_from_user()The function copies data from user space to kernel space.copy_to_user()The function copies data from kernel space to user space.
Common ways for kernel space to obtain user space data:
- System calls (e.g., read/write)
- Soft interrupts (e.g., socket communication)
include/linux/uaccess.h
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 | /* * Architectures should provide two primitives (raw_copy_{to,from}_user()) * and get rid of their private instances of copy_{to,from}_user() and * __copy_{to,from}_user{,_inatomic}(). * * raw_copy_{to,from}_user(to, from, size) should copy up to size bytes and * return the amount left to copy. They should assume that access_ok() has * already been checked (and succeeded); they should *not* zero-pad anything. * No KASAN or object size checks either - those belong here. * * Both of these functions should attempt to copy size bytes starting at from * into the area starting at to. They must not fetch or store anything * outside of those areas. Return value must be between 0 (everything * copied successfully) and size (nothing copied). * * If raw_copy_{to,from}_user(to, from, size) returns N, size - N bytes starting * at to must become equal to the bytes fetched from the corresponding area * starting at from. All data past to + size - N must be left unmodified. * * If copying succeeds, the return value must be 0. If some data cannot be * fetched, it is permitted to copy less than had been fetched; the only * hard requirement is that not storing anything at all (i.e. returning size) * should happen only when nothing could be copied. In other words, you don't * have to squeeze as much as possible - it is allowed, but not necessary. * * For raw_copy_from_user() to always points to kernel memory and no faults * on store should happen. Interpretation of from is affected by set_fs(). * For raw_copy_to_user() it's the other way round. * * Both can be inlined - it's up to architectures whether it wants to bother * with that. They should not be used directly; they are used to implement * the 6 functions (copy_{to,from}_user(), __copy_{to,from}_user_inatomic()) * that are used instead. Out of those, __... ones are inlined. Plain * copy_{to,from}_user() might or might not be inlined. If you want them * inlined, have asm/uaccess.h define INLINE_COPY_{TO,FROM}_USER. * * NOTE: only copy_from_user() zero-pads the destination in case of short copy. * Neither __copy_from_user() nor __copy_from_user_inatomic() zero anything * at all; their callers absolutely must check the return value. * * Biarch ones should also provide raw_copy_in_user() - similar to the above, * but both source and destination are __user pointers (affected by set_fs() * as usual) and both source and destination can trigger faults. */static __always_inline __must_check unsigned long__copy_from_user_inatomic(void *to, const void __user *from, unsigned long n){ instrument_copy_from_user(to, from, n); check_object_size(to, n, false); return raw_copy_from_user(to, from, n);}static __always_inline __must_check unsigned long__copy_from_user(void *to, const void __user *from, unsigned long n){ might_fault(); if (should_fail_usercopy()) return n; instrument_copy_from_user(to, from, n); check_object_size(to, n, false); return raw_copy_from_user(to, from, n);}/** * __copy_to_user_inatomic: - Copy a block of data into user space, with less checking. * @to: Destination address, in user space. * @from: Source address, in kernel space. * @n: Number of bytes to copy. * * Context: User context only. * * Copy data from kernel space to user space. Caller must check * the specified block with access_ok() before calling this function. * The caller should also make sure he pins the user space address * so that we don't result in page fault and sleep. */static __always_inline __must_check unsigned long__copy_to_user_inatomic(void __user *to, const void *from, unsigned long n){ if (should_fail_usercopy()) return n; instrument_copy_to_user(to, from, n); check_object_size(from, n, true); return raw_copy_to_user(to, from, n);}static __always_inline __must_check unsigned long__copy_to_user(void __user *to, const void *from, unsigned long n){ might_fault(); if (should_fail_usercopy()) return n; instrument_copy_to_user(to, from, n); check_object_size(from, n, true); return raw_copy_to_user(to, from, n);}static inline __must_check unsigned long_copy_from_user(void *to, const void __user *from, unsigned long n){ unsigned long res = n; might_fault(); if (!should_fail_usercopy() && likely(access_ok(from, n))) { instrument_copy_from_user(to, from, n); res = raw_copy_from_user(to, from, n); } if (unlikely(res)) memset(to + (n - res), 0, res); return res;}extern __must_check unsigned long_copy_from_user(void *, const void __user *, unsigned long);static inline __must_check unsigned long_copy_to_user(void __user *to, const void *from, unsigned long n){ might_fault(); if (should_fail_usercopy()) return n; if (access_ok(to, n)) { instrument_copy_to_user(to, from, n); n = raw_copy_to_user(to, from, n); } return n;}extern __must_check unsigned long_copy_to_user(void __user *, const void *, unsigned long);static __always_inline unsigned long __must_checkcopy_from_user(void *to, const void __user *from, unsigned long n){ if (likely(check_copy_size(to, n, false))) n = _copy_from_user(to, from, n); return n;}static __always_inline unsigned long __must_checkcopy_to_user(void __user *to, const void *from, unsigned long n){ if (likely(check_copy_size(from, n, true))) n = _copy_to_user(to, from, n); return n;}static __always_inline unsigned long __must_checkcopy_in_user(void __user *to, const void __user *from, unsigned long n){ might_fault(); if (access_ok(to, n) && access_ok(from, n)) n = raw_copy_in_user(to, from, n); return n;}/* * Without arch opt-in this generic copy_mc_to_kernel() will not handle * #MC (or arch equivalent) during source read. */static inline unsigned long __must_checkcopy_mc_to_kernel(void *dst, const void *src, size_t cnt){ memcpy(dst, src, cnt); return 0;} |
The overall call chain is as follows
123456789 | copy_from_user(to, from, n) └─ check_copy_size(to, n, false) // Compile-time object size checking (FORTIFY_SOURCE) └─ _copy_from_user(to, from, n) ├─ might_fault() // Mark as potentially causing page faults (scheduling points) ├─ should_fail_usercopy() // Fault injection (for testing) ├─ access_ok(from, n) // Check whether the user pointer is valid (critical!) ├─ instrument_copy_from_user() // KASAN / UBSAN detection ├─ raw_copy_from_user(...) // Actual copy (arch-specific) └─ if (res) memset(..., 0, res) // **Zero-padding!** |
Zero-padding: If only part of the data is copied (e.g., due to a page fault),the remaining uncopied portion is zeroed. This is to prevent kernel information leakage (e.g., uninitialized memory on the stack being read by the user).
access_ok(): ensurefrompoints to user space and the length is valid (prevents access to kernel addresses)
check_object_size(): prevents buffer overflow (in conjunction with FORTIFY_SOURCE)
instrument_*: KASAN detects out-of-bounds accessEven if the copy fails, it does not panic; instead, it returns the number of uncopied bytes, which the driver can handle accordingly.
copy_to_user()
Function prototype:
1234567 | static __always_inline unsigned long __must_checkcopy_to_user(void __user *to, const void *from, unsigned long n){ if (likely(check_copy_size(from, n, true))) n = _copy_to_user(to, from, n); return n;} |
- Function purpose: copy data from kernel space to user space.
- Parameter meaning:
*toUser space pointer.fromKernel space pointer.nis the number of bytes copied from kernel space to user space
- Return value: 0 means success, others mean failure.
copy_from_user()
1234567 | static __always_inline unsigned long __must_checkcopy_from_user(void *to, const void __user *from, unsigned long n){ if (likely(check_copy_size(to, n, false))) n = _copy_from_user(to, from, n); return n;} |
- Function: copies data from user space to kernel space.
- Parameter meaning
*toKernel space pointer.fromUser space pointer.nNumber of bytes copied from user space to kernel space.
- Return value: 0 means success, others mean failure.
copy_struct_from_user()
This is the recommended interface for modern syscalls.
Problem solvedThe version of the struct passed from user space differs.
- If the struct passed by the user is smaller, the kernel automatically pads with zeros.
- If the struct passed by the user is larger, check whether the extra fields are zero.
Return value:
0Success-EFAULTAccess failure.-E2BIGNon-zero extended fields.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 | /** * copy_struct_from_user: copy a struct from userspace * @dst: Destination address, in kernel space. This buffer must be @ksize * bytes long. * @ksize: Size of @dst struct. * @src: Source address, in userspace. * @usize: (Alleged) size of @src struct. * * Copies a struct from userspace to kernel space, in a way that guarantees * backwards-compatibility for struct syscall arguments (as long as future * struct extensions are made such that all new fields are *appended* to the * old struct, and zeroed-out new fields have the same meaning as the old * struct). * * @ksize is just sizeof(*dst), and @usize should've been passed by userspace. * The recommended usage is something like the following: * * SYSCALL_DEFINE2(foobar, const struct foo __user *, uarg, size_t, usize) * { * int err; * struct foo karg = {}; * * if (usize > PAGE_SIZE) * return -E2BIG; * if (usize < FOO_SIZE_VER0) * return -EINVAL; * * err = copy_struct_from_user(&karg, sizeof(karg), uarg, usize); * if (err) * return err; * * // ... * } * * There are three cases to consider: * * If @usize == @ksize, then it's copied verbatim. * * If @usize < @ksize, then the userspace has passed an old struct to a * newer kernel. The rest of the trailing bytes in @dst (@ksize - @usize) * are to be zero-filled. * * If @usize > @ksize, then the userspace has passed a new struct to an * older kernel. The trailing bytes unknown to the kernel (@usize - @ksize) * are checked to ensure they are zeroed, otherwise -E2BIG is returned. * * Returns (in all cases, some data may have been copied): * * -E2BIG: (@usize > @ksize) and there are non-zero trailing bytes in @src. * * -EFAULT: access to userspace failed. */static __always_inline __must_check intcopy_struct_from_user(void *dst, size_t ksize, const void __user *src, size_t usize){ size_t size = min(ksize, usize); size_t rest = max(ksize, usize) - size; /* Deal with trailing bytes. */ if (usize < ksize) { memset(dst + size, 0, rest); } else if (usize > ksize) { int ret = check_zeroed_user(src + size, rest); if (ret <= 0) return ret ?: -E2BIG; } /* Copy the interoperable parts of the struct. */ if (copy_from_user(dst, src, size)) return -EFAULT; return 0;} |
No copy._struct_The to_user function.
get_user()
123456789 | // include/asm-generic/uaccess.h |
Reads a simple variable, handles faults internally. The macro copies the value of a user-space variable to kernel space. Note: x is set to 0 on error.
- x is the kernel variable that stores the result.
- ptr is the source address in user space.
The result of dereferencing ptr must be assignable to x without a cast.
Returns 0 on success, and an error code on failure.
-EFAULT。
put_user()
12345678910 | // include/asm-generic/uaccess.h |
This macro copies a kernel-space variable value to user space.
- x is the value to be copied to user space.
- ptr is the destination address in user space.
x must be assignable to the result of dereferencing ptr. In other words, they must have (or point to) the same type.
Returns 0 on success, and returns on error
-EFAULT。
example
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 | static int major = 0;static int minor = 0;module_param(major, int, S_IRUGO);module_param(minor, int, S_IRUGO);struct class *class;struct device *device;static dev_t dev_id;static struct cdev cdev_test;int cdev_test_open(struct inode *inode, struct file *file){ pr_info("cdev_test_open was called"); return 0;}ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off){ char kbuf[] = "This is my_driver read\n"; if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) { pr_err("copy kbuf to buf from kernel error\n"); return -1; } pr_info("copy kbuf to buf from kernel ok\n"); pr_info("cdev_test_read was called"); return 0;}ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off){ char kbuf[32] = { 0 }; if (copy_from_user(kbuf, buf, size) != 0) { pr_err("copy buf to kbuf from kernel error\n"); return -1; } printk("read from user: %s", kbuf); pr_info("cdev_test_write was called"); return 0;}int cdev_test_release(struct inode *inode, struct file *file){ pr_info("cdev_test_release was called"); return 0;}static struct file_operations cdev_test_ops = { .owner = THIS_MODULE, .open = cdev_test_open, .read = cdev_test_read, .write = cdev_test_write, .release = cdev_test_release };static int __init my_driver_init(void){ int ret; if (major) { dev_id = MKDEV(major, minor); pr_info("major from module_param: %d", MAJOR(dev_id)); pr_info("minor from module_param: %d", MINOR(dev_id)); ret = register_chrdev_region(dev_id, 1, "my_driver device"); if (ret < 0) { pr_err("register_chrdev_region error\n"); return ret; } else { pr_info("register_chrdev_region ok\n"); } } else { ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device"); if (ret < 0) { pr_err("alloc_chrdev_region error\n"); return ret; } else { pr_info("alloc_chrdev_region ok\n"); pr_info("major allocated: %d", MAJOR(dev_id)); pr_info("minor allocated: %d", MINOR(dev_id)); } } cdev_init(&cdev_test, &cdev_test_ops); //Pointing the owner field to this module can prevent the module from being unloaded while its operations are in use. cdev_test.owner = THIS_MODULE; ret = cdev_add(&cdev_test, dev_id, 1); if (ret < 0) { pr_err("cdev_add error\n"); unregister_chrdev_region(dev_id, 1); return ret; } else { pr_info("cdev_add ok\n"); } class = class_create(THIS_MODULE, "test"); // /dev/my_driver device = device_create(class, NULL, dev_id, NULL, "my_driver"); pr_info("my_driver: Module loaded\n"); return 0;}static void __exit my_driver_exit(void){ // Delete device node device_destroy(class, dev_id); // Delete the class of this device node class_destroy(class); // Delete cdev cdev_del(&cdev_test); // Release device number unregister_chrdev_region(dev_id, 1); pr_info("unregister_chrdev_region ok\n"); pr_info("my_driver: Module unloaded\n");}module_init(my_driver_init);module_exit(my_driver_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Zhao Hang");MODULE_DESCRIPTION("my_driver Kernel Module"); |
Test
Test code:
12345678910111213141516171819202122232425262728293031323334353637 | int main(int argc, char **argv){ int ret, fd; fd = open("/dev/my_driver", O_RDWR); if (fd < 0) { printf("open /dev/my_driver error\n"); return -1; } char rbuf[32] = { 0 }; ret = read(fd, rbuf, sizeof(rbuf)); if (ret < 0) { printf("read error\n"); goto err; } printf("read from driver:%s\n", rbuf); char wbuf[32] = "hello world\n"; ret = write(fd, wbuf, sizeof(wbuf)); if (ret < 0) { printf("write error\n"); goto err; }success: close(fd); return 0;err: close(fd); return -1;} |
makefile
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 | TARGET_EXEC := testSRC_DIRS := ./BUILD_DIR := ./buildCC = aarch64-linux-gnu-gccCFLAGS = -g -Wall# Source file collectionSRCS := $(shell find $(SRC_DIRS) -name '*.c' -or -name '*.s' -or -name '*.S')# Object file mappingOBJS := $(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))OBJS := $(patsubst %.s,$(BUILD_DIR)/%.o,$(OBJS))OBJS := $(patsubst %.S,$(BUILD_DIR)/%.o,$(OBJS))# Include pathINC_DIRS := $(shell find $(SRC_DIRS) -type d)INC_FLAGS := $(addprefix -I,$(INC_DIRS))# Automatically generate dependency filesCPPFLAGS := $(INC_FLAGS) -MMD -MP# Link the final executable$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS) @mkdir -p $(dir $@) $(CC) $(OBJS) -o $@ $(LDFLAGS)# Compilation rules (C)$(BUILD_DIR)/%.o: %.c @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@# Compilation rules (Assembly)# Assembly code that does not require preprocessing$(BUILD_DIR)/%.o: %.s @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@# Assembly code that requires preprocessing$(BUILD_DIR)/%.o: %.S @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@# Cleanupclean: rm -rf $(BUILD_DIR)deploy: cp $(BUILD_DIR)/$(TARGET_EXEC) ~/tftp# Automatic dependency inclusion-include $(OBJS:.o=.d) |
Test output:
12345678910111213141516 | ~ # insmod my_driver.ko[ 7.250701] my_driver: loading out-of-tree module taints kernel.[ 7.257204] alloc_chrdev_region ok[ 7.257387] major allocated: 511[ 7.257406] minor allocated: 0[ 7.257563] cdev_add ok[ 7.258587] my_driver: Module loaded~ # mdev -s~ # ./test[ 18.006803] cdev_test_open was called[ 18.007016] copy kbuf to buf from kernel okread from driver:This is my_driver read[ 18.007275] cdev_test_read was called[ 18.010813] read from user: hello world[ 18.011205] cdev_test_write was called |
File private data
Usually in driver development, we willDefine a custom device structure for the deviceandSet the device structure as file private dataThe device structure is used to store hardware-related information, such as device number, class, device name, etc.
Linux does not explicitly require the use of file private dataHowever, file private data can be seen everywhere in Linux driver source code, so it can be considered that using file private data is an ‘unwritten rule’ followed by Linux drivers. In fact, file private data also reflects Linux’s object-oriented thinking.
File private data means pointing the private data private_data to the device structure.
123456 | struct device_test dev1;static int cdev_test_open(struct inode *inode, struct file *file){ file->private_data = &dev1; return 0;} |
Then, in functions such as read and write, access the device structure through private_data.
1234 | static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off_t){ struct device_test *test_dev = (struct device_test *)file->private_data; return 0;} |
Global variables can also achieve this functionality, but as driver code complexity increases, using private_data is more manageable.
example
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 | static int major = 0;module_param(major, int, S_IRUGO);MODULE_DESCRIPTION("device num: major");static int minor = 0;module_param(minor, int, S_IRUGO);MODULE_DESCRIPTION("device num: minor");static char buf[] = "Hello World from kernel\n";struct test_data { dev_t dev_num; struct cdev cdev; struct class *class; struct device *dev; char kbuf[32];};static struct test_data *dat;static int file_private_test_open(struct inode *inode, struct file *file){ pr_info("file_private_test_open is called\n"); // Set private data file->private_data = dat; pr_info("file->private_data is set\n"); return 0;}static ssize_t file_private_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ struct test_data *data; size_t len; if (*offset != 0) { // EOF return 0; } data = file->private_data; len = min(size, strlen(dat->kbuf)); pr_info("file_private_test_read is called\n"); if (copy_to_user(buf, data->kbuf, len) != 0) { pr_err("copy_to_user error\n"); return -EFAULT; } *offset += len; return sizeof(data->kbuf);}static int file_private_test_release(struct inode *inode, struct file *file){ pr_info("file_private_test_release is called\n"); return 0;}static struct file_operations fops = { .owner = THIS_MODULE, .open = file_private_test_open, .read = file_private_test_read, .release = file_private_test_release,};static int __init file_private_test_init(void){ int err; pr_info("file_private_test init\n"); dat = (struct test_data *)kmalloc(sizeof(struct test_data), GFP_KERNEL); if (dat == NULL) { pr_err("no memory\n"); goto kmalloc_fail; } strscpy(dat->kbuf, buf, sizeof(dat->kbuf)); // Allocate device number if (major) { dat->dev_num = MKDEV(major, minor); err = register_chrdev_region(dat->dev_num, 1, "file_private_test chrdev region"); if (err < 0) { pr_err("register_chrdev_region error\n"); goto chrdev_region_fail; } } else { err = alloc_chrdev_region(&dat->dev_num, 0, 1, "file_private_test chrdev region"); if (err < 0) { pr_err("register_chrdev_region error\n"); goto chrdev_region_fail; } } // Add cdev cdev_init(&dat->cdev, &fops); err = cdev_add(&dat->cdev, dat->dev_num, 1); if (err < 0) { pr_err("cdev_add error\n"); goto cdev_add_fail; } // Create device node dat->class = class_create(THIS_MODULE, "char_test"); if (IS_ERR(dat->class)) { err = PTR_ERR(dat->class); pr_err("create class error"); goto class_create_fail; } dat->dev = device_create(dat->class, NULL, dat->dev_num, NULL, "char_test_dev"); if (IS_ERR(dat->dev)) { err = PTR_ERR(dat->dev); pr_err("create device error\n"); goto device_create_fail; } return 0;device_create_fail: class_destroy(dat->class);class_create_fail: cdev_del(&dat->cdev);cdev_add_fail: unregister_chrdev_region(dat->dev_num, 1);chrdev_region_fail: kfree(dat);kmalloc_fail: return err;}static void __exit file_private_test_exit(void){ device_destroy(dat->class, dat->dev_num); class_destroy(dat->class); cdev_del(&dat->cdev); unregister_chrdev_region(dat->dev_num, 1); kfree(dat); pr_info("file_private_test exit\n");}module_init(file_private_test_init);module_exit(file_private_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is a test sample for file private data"); |
Test:
123456789 | $ insmod file_private_data_test.ko[ 8.022277] file_private_data_test: loading out-of-tree module taints kernel.[ 8.032581] file_private_test init$ cat /dev/char_test_dev[ 14.998329] file_private_test_open is called[ 14.998580] file->private_data is set[ 14.999659] file_private_test_read is calledHello World from kernel[ 15.000469] file_private_test_release is called |
Scenarios for using file private data
In Linux, the major device number is used to represent a particular type of driver. The minor device number is used to represent individual devices under that type of driver.
Suppose our driver nowneeds to support devices with the same major device number but different minor device numbersOur driver can then be written using file private data.
container_of macro
include/linux/kernel.h
123456 |
ptrA pointer to a member within a structure.typeThe target structure type.memberThe name of the member in the structure.BUILD_BUG_ON_MSG(...)Compile-time type checking ensures that ptr matches the type of the structure member, avoiding type errors.__mptr - offsetof(type, member)Core formula: subtract the member’s offset within the structure from the member pointer to obtain the starting address of the structure.offsetof(type, member)A standard macro that calculates the byte offset of a member within a structure.
example
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 | struct container_data { dev_t dev_num; struct cdev cdev; struct device *dev; char kbuf[KBUF_SIZE];};static struct class *class;static struct container_data *dat1;static struct container_data *dat2;int container_of_test_open(struct inode *inode, struct file *file){ struct container_data *dat; // Through container_of to obtain a specific container_data dat = container_of(inode->i_cdev, struct container_data, cdev); file->private_data = dat; pr_info("container_of_test_open is called, dev major: %d, dev minor: %d\n", MAJOR(dat->dev_num), MINOR(dat->dev_num)); return 0;}ssize_t container_of_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ struct container_data *dat = file->private_data; size_t kbuf_len = strlen(dat->kbuf); size_t len = min(size, (size_t)(kbuf_len - *offset)); if (*offset >= kbuf_len) return 0; if (copy_to_user(buf, dat->kbuf + *offset, len)) return -EFAULT; *offset += len; return len;}ssize_t container_of_test_write(struct file *file, const char __user *buf, size_t size, loff_t *offset){ struct container_data *dat = file->private_data; size_t bytes = min(size, (size_t)(KBUF_SIZE - 1 - *offset)); if (*offset >= KBUF_SIZE - 1) return -ENOSPC; if (copy_from_user(dat->kbuf + *offset, buf, bytes) != 0) return -EFAULT; *offset += bytes; dat->kbuf[*offset] = '\0'; return bytes;}int container_of_test_release(struct inode *inode, struct file *file){ pr_info("container_of_test_release is called\n"); return 0;}static struct file_operations fops = { .owner = THIS_MODULE, .open = container_of_test_open, .read = container_of_test_read, .write = container_of_test_write, .release = container_of_test_release,};static int __init container_of_test_init(void){ int err; dat1 = (struct container_data *)kmalloc(sizeof(struct container_data), GFP_KERNEL); if (dat1 == NULL) { pr_err("no memory dat1"); err = -ENOMEM; goto kmalloc_dat1_fail; } memset(dat1, 0, sizeof(struct container_data)); dat2 = (struct container_data *)kmalloc(sizeof(struct container_data), GFP_KERNEL); if (dat2 == NULL) { pr_err("no memory dat2"); err = -ENOMEM; goto kmalloc_dat2_fail; } memset(dat2, 0, sizeof(struct container_data)); err = alloc_chrdev_region(&dat1->dev_num, 0, 2, "container_of_test chrdev region"); if (err < 0) { pr_err("alloc_chrdev_region error\n"); goto alloc_chrdev_region_fail; } dat2->dev_num = MKDEV(MAJOR(dat1->dev_num), MINOR(dat1->dev_num) + 1); cdev_init(&dat1->cdev, &fops); err = cdev_add(&dat1->cdev, dat1->dev_num, 1); if (err < 0) { pr_err("cdev_add dat1 error\n"); goto cdev_add_dat1_fail; } cdev_init(&dat2->cdev, &fops); err = cdev_add(&dat2->cdev, dat2->dev_num, 1); if (err < 0) { pr_err("cdev_add dat2 error\n"); goto cdev_add_dat2_fail; } class = class_create(THIS_MODULE, "chrdev"); if (IS_ERR(class)) { err = PTR_ERR(class); goto class_create_fail; } dat1->dev = device_create(class, NULL, dat1->dev_num, NULL, "container_of_test_dev%d", 0); if (IS_ERR(dat1->dev)) { err = PTR_ERR(dat1->dev); goto device_create_dat1_fail; } dat2->dev = device_create(class, NULL, dat2->dev_num, NULL, "container_of_test_dev%d", 1); if (IS_ERR(dat2->dev)) { err = PTR_ERR(dat2->dev); goto device_create_dat2_fail; } return 0;device_create_dat2_fail: device_destroy(class, dat1->dev_num);device_create_dat1_fail: class_destroy(class);class_create_fail: cdev_del(&dat2->cdev);cdev_add_dat2_fail: cdev_del(&dat1->cdev);cdev_add_dat1_fail: unregister_chrdev_region(dat1->dev_num, 2);alloc_chrdev_region_fail: kfree(dat2);kmalloc_dat2_fail: kfree(dat1);kmalloc_dat1_fail: return err;}static void __exit container_of_test_exit(void){ device_destroy(class, dat1->dev_num); device_destroy(class, dat2->dev_num); class_destroy(class); cdev_del(&dat2->cdev); cdev_del(&dat1->cdev); unregister_chrdev_region(dat1->dev_num, 2); kfree(dat2); kfree(dat1); pr_info("container_of_test exit\n");}module_init(container_of_test_init);module_exit(container_of_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is a test sample for container_of"); |
Miscellaneous device driver
Miscellaneous devices belong toa special type of character deviceIt is a wrapper for character devices, and in essence is also a character device.
In Linux, devices that cannot be categorized are defined as miscellaneous devices. Compared to character devices, miscellaneous devices have the following two advantages:
- Saves major device numbers: The major device number for miscellaneous devices is fixed at 10When multiple miscellaneous device drivers are registered in the system, they only need to be distinguished by their minor device numbers. Character devices, however, consume a major device number whether the device number is allocated dynamically or statically, resulting in a waste of major device numbers.
- Easy to use:**The misc device driver itself includes the operation of creating device nodes, so there is no need to additionally use the class_create() function and device_create() function.**It implements automatic device node creation. You only need to fill in the members of the file_operations structure in the driver.
Definition
include/linux/miscdevice.h
1234567891011121314151617 | struct device;struct attribute_group;struct miscdevice { int minor; const char *name; const struct file_operations *fops; struct list_head list; struct device *parent; struct device *this_device; const struct attribute_group **groups; const char *nodename; umode_t mode;};extern int misc_register(struct miscdevice *misc);extern void misc_deregister(struct miscdevice *misc); |
To define a misc device, generally you only need to fill in the three member variables: minor, name, and fops in the miscdevice structure.
minorThe minor device number of the misc device can be selected from the kernel source codeinclude/linux/miscdevice.hfrom the predefined minor device numbers in the file, or you can define your own minor device number (as long as it is not used by other devices). Usually this parameter is set to MISC_DYNAMIC_MINOR, which means the minor device number is automatically allocated.nameRepresents the name of the misc device. That is, after the misc device driver is successfully registered, a device node named name will be generated in the /dev directory.fopsPoints to the file_operations structure, representing the file operation set.
misc_register
1 | extern int misc_register(struct miscdevice *misc); |
Function purpose: Based on misc_class, construct a device, and mount the miscdevice structure to the misc_list, and initialize the structures related to the Linux device model. This serves to register the misc device.
Parameter meaning:
- misc: Pointer to the misc device structure
Function return value: Returns 0 on success, and a negative number on failure.
misc_deregister
1 | extern void misc_deregister(struct miscdevice *misc); |
- Function purpose: Removes the miscdevice from the misc_list, thereby serving to unload the misc device.
- Parameter meaning:
- misc: Pointer to the misc device structure
example
123456789101112131415161718192021222324252627282930 | static struct file_operations fops = { .owner = THIS_MODULE,};static struct miscdevice miscdev={ .minor=MISC_DYNAMIC_MINOR,// Dynamically allocate a minor device number .name="miscdev", .fops = &fops,};static int __init misc_device_test_init(void){ misc_register(&miscdev); return 0;}static void __exit misc_device_test_exit(void){ misc_deregister(&miscdev);}module_init(misc_device_test_init);module_exit(misc_device_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is a misc device sample"); |
Linux driver error handling
goto reverse-order release
Reverse-order release: the one requested later is released first
123456789101112131415161718192021222324252627282930313233343536373839 | static int __init my_driver_init(void){ int ret; ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "my_driver"); if (ret < 0) goto fail_alloc; cdev_init(&dev1.cdev_test, &cdev_test_fops); dev1.cdev_test.owner = THIS_MODULE; ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1); if (ret < 0) goto fail_cdev_add; dev1.class = class_create(THIS_MODULE, "test1"); if (IS_ERR(dev1.class)) { ret = PTR_ERR(dev1.class); goto fail_class_create; } dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1"); if (IS_ERR(dev1.device)) { ret = PTR_ERR(dev1.device); goto fail_device_create; } pr_info("my_driver initialized successfully\n"); return 0;fail_device_create: class_destroy(dev1.class);fail_class_create: cdev_del(&dev1.cdev_test);fail_cdev_add: unregister_chrdev_region(dev1.dev_num, 1);fail_alloc: return ret;} |
- The label order is “reverse release”, that is, the one requested later is released first。
PTR_ERR()+IS_ERR()used to determineclass_create()anddevice_create()the error.PTR_ERR()Convert the error pointer to an error code, and return the error code.gotocan avoid writing the same code multiple timesifcheck and clean up the code, making the code concise.
IS_ERR()
If a function’s return value is a pointer type, it will return a NULL pointer when the call fails, but returning NULL cannot tell the exact nature of the problem. Some functions need to return an actual error code so that engineers can make correct judgments based on the return value.
For any pointer, there are necessarily three cases:
- valid pointer
- NULL (that is, a null pointer)
- error pointer (that is, an invalid pointer)
In the Linux kernel, the so-callederror pointer points to the last page of kernel space, for example, for a 64-bit system, the address of the last page is 0xfffffffffffff000~0xffffffffffffffff,
The last page address is reserved, and this address range is associated with the error codes defined by the kernel. Therefore, error codes can be used to indicate the corresponding error conditions. If a pointer points to the address range of this page, it is defined as an error pointer.
In the Linux kernel source code, API functions related to error pointers are provided, mainly including IS_ERR()、PTR_ERR(), ERR_PTR() functions, etc., whose source code is ininclude/linux/err.hfile.
include/linux/err.h
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 | /* SPDX-License-Identifier: GPL-2.0 *//* * Kernel pointers have redundant information, so we can use a * scheme where we can return either an error code or a normal * pointer with the same return value. * * This should be a per-architecture thing, to allow different * error and pointer decisions. */static inline void * __must_check ERR_PTR(long error){ return (void *) error;}static inline long __must_check PTR_ERR(__force const void *ptr){ return (long) ptr;}static inline bool __must_check IS_ERR(__force const void *ptr){ return IS_ERR_VALUE((unsigned long)ptr);}static inline bool __must_check IS_ERR_OR_NULL(__force const void *ptr){ return unlikely(!ptr) || IS_ERR_VALUE((unsigned long)ptr);}/** * ERR_CAST - Explicitly cast an error-valued pointer to another pointer type * @ptr: The pointer to cast. * * Explicitly cast an error-valued pointer to another pointer type in such a * way as to make it clear that's what's going on. */static inline void * __must_check ERR_CAST(__force const void *ptr){ /* cast away the const */ return (void *) ptr;}static inline int __must_check PTR_ERR_OR_ZERO(__force const void *ptr){ if (IS_ERR(ptr)) return PTR_ERR(ptr); else return 0;} |
PTR_ERR()
Convert the error pointer into an error code.
include/linux/err.h
1234 | static inline long __must_check PTR_ERR(__force const void *ptr){ return (long) ptr;} |
ERR_PTR()
Convert the error code into a pointer.
1234 | static inline void * __must_check ERR_PTR(long error){ return (void *) error;} |
IS_ERR_OR_NULL()
Determine whether it is an error or NULL.
1234 | static inline bool __must_check IS_ERR_OR_NULL(__force const void *ptr){ return unlikely(!ptr) || IS_ERR_VALUE((unsigned long)ptr);} |
error code
include/uapi/asm-generic/errno-base.h
1234567891011121314151617181920212223242526272829303132333435363738394041 | /* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ |
Chinese translation:
12345678910111213141516171819202122232425262728293031323334 |
Example:
1234 | if (IS_ERR(dev1.class)) { ret = PTR_ERR(dev1.class); goto fail_class_create;} |
Errors sometimes cross kernel space and propagate to user space. If the returned error is a response to a system call (open, read, ioctl, mmap), the value is automatically assigned to the user-space errno global variable, on which callingstrerror(errno)can convert the error to a readable string:
123456 | if(wite(fd, buf, 1) < 0) { printf("something gone wrong! %s\n", strerror(errno)); } |
In kernel coding style:
- If the function name is an action or an imperative command, the error code returned by the function should be an integer;
- If the function name is a predicate, the function should return a boolean value succeeded (successful).
For example, add_work is a command, add_The work() function returns 0 for success and -EBUSY for failure.
Similarly, PCI device present is a predicate, pci_dev_The present() function returns 1 if it successfully finds a matching device; otherwise, it returns 0.
RK3568 Light up the LED
Schematic
The topeet RK3568 Working LED schematic is as follows

It can be seen that the LED is connected toGPIO0_B7this GPIO port. When GPIO0_When B7 is high, transistor Q16 conducts and LED9 lights up. When GPIO0_When B7 is low, transistor Q16 is cut off, and LED9 does not light up.
Query register address
Pin multiplexing of GPIO0B
First, we need to set the pin multiplexing of GPIO0B, which can be found in PMU_GRF.

Searching for GPIO0B7, we can see its corresponding register isPMU_GRF_GPIO0B_IOMUX_Hthe setting of bits 14:12

And the PMU_GRF register address can be seen below as 0xFDC20000

So the multiplexing register address = base address + offset address = 0xFDC20000 + 0xC = 0xFDC2000C.
Use the io command to view the address of this register:
1 | io -r -4 0xFDC2000C |
GPIO registers
From the Address Mapping in the TRM, we know that the GPIO0 address is 0xFDD60000

The GPIO chapter in the RK3568 TRM introduces:

It can be seen that the RK3568 registers are divided into:
Direction register (DDR)
- GPIO_SWPORT_DR_L
- GPIO_SWPORT_DR_H
Data register (DR)
- GPIO_SWPORT_DDR_L
- GPIO_SWPORT_DDR_H
External input register (EXT_PORT)
- GPIO_EXT_PORT
that is,
- GPIO_SWPORT_DDR_L/H Determines whether the pin is input or output
- GPIO_SWPORT_DR_L/H Determines the output level when the pin is configured as output
- GPIO_EXT_PORT Reflects the current actual level of the pin (read-only)
The data register and direction register are divided into L and H
- GPIOA and GPIOB addresses fall on L
- GPIOC and GPIOD addresses fall on H
Also:

It can be seen that the registers of the five groups GPIO0~GPIO4 are different.
We need to first set the GPIO direction register to output, then set the output level to high.


GPIO has four groups: GPIOA, GPIOB, GPIOC, GPIOD. Each group is further distinguished by numbering A0~A7, B0~B7, C0~C7, D0~D7.
GPIO0B7 is on L, so the offset address of the direction register is 0x0008, and the data register offset is 0x0000.
The base address of GPIO0 is 0xFDD60000. Therefore
Direction register address = base address + offset address = 0xFDD60000 + 0x0008 = 0xFDD60008
Data register address = base address + offset address = 0xFDD60000 + 0x0000 = 0xFDD60000
External input register address = base address + offset address = 0xFDD60000 + 0x0070 = 0xFDD60070
Direction register

It can be seen that bits 31:16 are writable, so GPIOB7 should be 16+8+8-1=31, that is,bit 31 sets write access。
And 0+8+8-1=15, that is,bit 15 corresponds to setting the direction of GPIO0B7 as input or output
Data register

Similarly, 16+8+8-1=31, that is,bit 31 sets write access。
And 0+8+8-1=15, that is,bit 15 corresponds to setting GPIO0B7 to output high or low level。
Summary
- Mux relationship registerThe base address is 0xFDC20000
- GPIO0B_IOMUX_The H offset address is 000C, so the address to operate on is base address + offset address = 0xFDC2000C
- GPIO0B_IOMUX_Bits 28 to 30 of H are bit write access, 0 is disable, 1 is enable
- GPIO0B_IOMUX_Setting bits 12 to 14 of H to 0 selects GPIO0_B7, 1 is PWM0_M0, 2 is CPU_AVS
- GPIO0B_IOMUX_The H offset address is 000C, so the address to operate on is base address + offset address = 0xFDC2000C
- The base address of GPIO0 is 0xFDD60000
- Direction registerAddress = base address + offset address = 0xFDD60008
- bit 31 corresponds to the direction write access of GPIO0_B7, 0 is disable, 1 is enable
- bit 15 corresponds to the direction of GPIO0_B7, 0 is input, 1 is output
- Data registerAddress = base address + offset address = 0xFDD60000
- bit 31 corresponds to the data write access of GPIO0_B7, 0 is disable, 1 is enable
- bit 15 corresponds to the data of GPIO0_B7, 0 outputs low, 1 outputs high
- Direction registerAddress = base address + offset address = 0xFDD60008
Use the io command to test
12345678910111213141516171819202122 | su root# Mux relationship registersudo io -4 -w 0xFDC2000C 0x70001000# Read to verify whether the write was successfulsudo io -4 -r 0xFDC2000C# Set outputsudo io -4 -w 0xFDD60008 0x80008000# Verifysudo io -4 -r 0xFDD60008# Read current valuesudo io -4 -r 0xFDD60000# Turn on the lightsudo io -4 -w 0xFDD60000 0x8000C000# Turn off the lightsudo io -4 -w 0xFDD60000 0x80004000 |
example
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 | /* Multiplexing register *//* Data register and direction register */// Direction register// Data register// External input register, read-onlystruct led_drv_data { dev_t dev_num; struct cdev cdev; struct class *class; struct device *dev; void __iomem *gpio0b_iomux; void __iomem *gpio0_swport_ddr_l; void __iomem *gpio0_swport_dr_l; void __iomem *gpio0_ext_port;};static struct led_drv_data *led_drv_data;int light_up_led_open(struct inode *inode, struct file *file){ file->private_data = led_drv_data; return 0;}ssize_t light_up_led_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ struct led_drv_data *drv_data = file->private_data; u32 val; if (size != sizeof(u32)) { pr_info("read need 4 bytes\n"); return -ENOMEM; } val = readl(drv_data->gpio0_ext_port); val &= 1 << 15;// gpio0_b7 val = val >> 15; if (copy_to_user(buf, &val, sizeof(u32)) != 0) { return -EFAULT; } return sizeof(u32);}ssize_t light_up_led_write(struct file *file, const char __user *buf, size_t size, loff_t *offset){ struct led_drv_data *drv_data = file->private_data; u32 val; if (size != sizeof(u32)) { pr_err("write need 4 bytes\n"); return -ENOMEM; } if (copy_from_user(&val, buf, sizeof(u32)) != 0) { return -EFAULT; } if (val > 0) { // Configure as GPIO output val = readl(drv_data->gpio0_swport_ddr_l); val |= 0x80008000; writel(val, drv_data->gpio0_swport_ddr_l); // Open val = readl(drv_data->gpio0_swport_dr_l); val |= 0x80008000; writel(val, drv_data->gpio0_swport_dr_l); } else if (val == 0) { // Configure as GPIO output val = readl(drv_data->gpio0_swport_ddr_l); val |= 0x80008000; writel(val, drv_data->gpio0_swport_ddr_l); // Shutdown val = readl(drv_data->gpio0_swport_dr_l); val |= 0x80000000; val &= 0xffff7fff; writel(val, drv_data->gpio0_swport_dr_l); } return sizeof(u32);}int light_up_led_release(struct inode *inode, struct file *file){ return 0;}struct file_operations fops = { .owner = THIS_MODULE, .open = light_up_led_open, .read = light_up_led_read, .write = light_up_led_write, .release = light_up_led_release,};static int __init light_up_led_init(void){ int err; u32 val; led_drv_data = kzalloc(sizeof(struct led_drv_data), GFP_KERNEL); if (led_drv_data == NULL) { err = -ENOMEM; goto kzalloc_fail; } err = alloc_chrdev_region(&led_drv_data->dev_num, 0, 1, "led chrdev region"); if (err < 0) goto alloc_chrdev_region_fail; cdev_init(&led_drv_data->cdev, &fops); led_drv_data->cdev.owner = THIS_MODULE; err = cdev_add(&led_drv_data->cdev, led_drv_data->dev_num, 1); if (err < 0) goto cdev_add_fail; led_drv_data->class = class_create(THIS_MODULE, "test_led"); if (IS_ERR(led_drv_data->class)) { err = PTR_ERR(led_drv_data->class); goto create_class_fail; } led_drv_data->dev = device_create(led_drv_data->class, NULL, led_drv_data->dev_num, NULL, "test_led%d", 0); if (IS_ERR(led_drv_data->dev)) { err = PTR_ERR(led_drv_data->dev); goto device_create_fail; } led_drv_data->gpio0b_iomux = ioremap(GPIO0B_IOMUX, 4); led_drv_data->gpio0_swport_ddr_l = ioremap(GPIO0_SWPORT_DDR_L, 4); led_drv_data->gpio0_swport_dr_l = ioremap(GPIO0_SWPORT_DR_L, 4); led_drv_data->gpio0_ext_port = ioremap(GPIO0_EXT_PORT, 4); // Set pin multiplexing to GPIO val = readl(led_drv_data->gpio0b_iomux); val |= 0x70000000; // write access val &= 0xFFFF8FFF; // gpio0_b7 writel(val, led_drv_data->gpio0b_iomux); return 0;device_create_fail: class_destroy(led_drv_data->class);create_class_fail: cdev_del(&led_drv_data->cdev);cdev_add_fail: unregister_chrdev_region(led_drv_data->dev_num, 1);alloc_chrdev_region_fail: kfree(led_drv_data);kzalloc_fail: return err;}static void __exit light_up_led_exit(void){ iounmap(led_drv_data->gpio0_ext_port); iounmap(led_drv_data->gpio0b_iomux); iounmap(led_drv_data->gpio0_swport_ddr_l); iounmap(led_drv_data->gpio0_swport_dr_l); device_destroy(led_drv_data->class, led_drv_data->dev_num); class_destroy(led_drv_data->class); cdev_del(&led_drv_data->cdev); unregister_chrdev_region(led_drv_data->dev_num, 1); kfree(led_drv_data);}module_init(light_up_led_init);module_exit(light_up_led_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("light up a led on topeet RK3568 board"); |
Test code:
123456789101112131415161718192021222324252627282930313233343536373839404142 | int main(int argc, char **argv){ int fd, err; uint32_t val = 0; fd = open("/dev/test_led0", O_RDWR); if (fd < 0) goto fail; if (argc == 1) { // read err = read(fd, &val, 4); if (err < 0) goto fail; printf("read:0x%08x\n", val); } else if (argc >= 2) { // write val = atoi(argv[1]); if (val < 0) { perror("unsupported\n"); exit(EXIT_FAILURE); } err = write(fd, &val, 4); if (err < 0) goto fail; printf("write val: %u success\n", val); } else { perror("argc error\n"); exit(EXIT_FAILURE); } return 0;fail: fprintf(stderr, "Error:%s[errno:%d]", strerror(errno), errno); exit(EXIT_FAILURE);} |

