Cover image for Linux Character Device Basics

Linux Character Device Basics


Timeline

Timeline

2025-11-11

init

This article introduces the basics of Linux character devices, discussing in detail the concept of device numbers (major and minor numbers), the dev_t type and its related operation macros, and summarizes the usage of static and dynamic allocation and release functions for device numbers, while also mentioning the cdev structure related to character device registration.

Linux Driver Notes

Table of ContentsLinks
1. Linux Driver Framework
2. Linux Driver Loading Logic
3. Character Device Basics
4. Concurrency and Competition
5. Advanced Character Device Progression
6. Interrupts
7. Platform Bus
8. Device Tree
9. Device Model
10. Hot Plug
11. pinctrl Subsystem
12. GPIO Subsystem
13. Input Subsystem
14. Single Bus
15. I2C
16. SPI
17. UART
18. PWM
19. RTC
20. Watchdog
21. CAN
22. Network Device
23. ADC
24. IIO
25. USB
26. LCD

Device Number

In the Linux system, each device has a corresponding device number, which consists ofMajor Device NumberandMinor Device Number:

  • Major Device Number: IdentifierDevice Type
  • Minor Device Number: Indistinguish between multiple device instances managed by the same driver

Example:

  • Major Device Number13indicatesinput subsystem(handles input devices).
  • Minor Device Number 64 →/dev/input/event0(likely a USB keyboard)
  • Minor Device Number 65 →/dev/input/event1(likely a USB mouse)
  • Major Device Number188indicatesUSB serial device
  • Minor number 0 →/dev/ttyUSB0
  • Minor number 1 →/dev/ttyUSB1

Beforeregistering a character device driver, you need to apply for a device number first

include/linux/types.h

1
2
3
4
5
6
7
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 major numberthe low 20 bits are the minor number

Device number operation macros

include/linux/kdev_t.h

1
2
3
4
5
6
#define MINORBITS	20
#define MINORMASK ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
  • MINORBITSindicates the number of bits for the minor number, a total of 20 bits
  • MINORMASKused to calculate the minor number
  • MAJORindicates extracting from dev_t the major number, essentially shifting dev_t right by 20 bits
  • MINORIndicates obtaining the minor device number from dev_t, essentially taking the lower 20 bits.
  • MKDEVUsed to combine the major and minor device numbers 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

1
2
3
extern int register_chrdev_region(dev_t, unsigned, const char *);

extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);

register_chrdev_region()

  1. Throughregister_chrdev_region(dev_t from, unsigned count, const char *name)function to statically allocate a device number.

    • Function prototype
    1
    register_chrdev_region(dev_t from, unsigned count, const char *name)
    • Function purpose: Statically allocate a device number, applying for a specified device number.
    • Parameter meaning
      • from: Custom dev_t type device number, e.g., MKDEV(100,0) indicates starting major number 100 and starting minor number 0.
      • count: The number of minor devices, indicating how many minor device numbers exist under the same major device number.
      • name: The name of the device being allocated.
    • Function return value: Returns 0 on success, negative on failure.

Note that statically allocated device numbers are limited:include/linux/fs.hDefined in:

1
2
3
4
5
6
7
/* fs/char_dev.c */
#define CHRDEV_MAJOR_MAX 512
/* Marks the bottom of the first segment of free char majors */
#define CHRDEV_MAJOR_DYN_END 234
/* Marks the top and bottom of the second segment of free char majors */
#define CHRDEV_MAJOR_DYN_EXT_START 511
#define CHRDEV_MAJOR_DYN_EXT_END 384

alloc_chrdev_region()

  1. Throughalloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char* name)the function to dynamically allocate device numbers.
  • Function prototype
1
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
  • Function purpose: Dynamically allocate a device number; the kernel automatically assigns an unused device number. Compared to static allocation, dynamic allocation avoids conflicts caused by registering the same device number.

  • Parameter meaning

    • dev *: Saves the allocated device number in the dev variable
    • baseminor: Starting address of the minor device number; minor device numbers usually start from 0, so this parameter is generally set to 0
    • count: Number of devices to allocate
    • name: Name of the device to allocate
  • Function return value: Returns 0 on success, negative 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, which releases the device number after unregistering a character device.
  • Function parameters:
    • dev_tType: The device number to be released.
    • unsignedType: The number of device numbers to release.

Example

my_driver.c

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>

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:

1
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
32
~ # 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_driver
511 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 a character class device.

include/linux/cdev.h

1
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
32
33
34
35
36
37
38
39
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_CDEV_H
#define _LINUX_CDEV_H

#include <linux/kobject.h>
#include <linux/kdev_t.h>
#include <linux/list.h>
#include <linux/device.h>

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 *);

#endif

In C language, if you only need to use a pointer to a certain structpointerwithout needing to know its internal members (i.e., without dereferencing or using sizeof), you can simply do aforward declaration

1
struct file_operations;  // Tell the compiler: there exists a struct type called file_operations.

This way you can define a pointer:

1
struct file_operations *fops;

But you cannot access members (because the compiler does not yet know the struct’s contents):

1
fops->read(...);  // ❌ Compilation error: incomplete type

This is very common in header files, used todecouple dependenciesandspeed up compilation

whileexternisa storage class specifier, used to declarevariablesorfunctionswith external linkage. Its purpose is to tell the compiler: ‘This variable/function is defined elsewhere; here it is only a declaration.’

GCC/Clang attribute(__randomize_layout),
Used for the kernel’sstruct layout randomization, it increases the difficulty for attackers to predict struct offsets, thereby improving kernel security.

cdev structure

FieldMeaning
kobjInherits fromstruct kobject, enabling character devices to be mounted under sysfs (i.e.,/sys/class/...paths such as).
ownerPoints to the module that owns the device (THIS_MODULE), preventing the device from being in use when the module is unloaded.
opsPoints to the device operation function table (struct file_operations), defining behaviors such as read/write/ioctl.
listInternal linked list, linking all inodes that share thiscdev(i.e.,/dev/xxxfiles) throughinode->i_devicesto form a “reverse reference linked list”.
devDevice number, type isdev_t(including major device number and minor device number).
countindicates thiscdevthe number of consecutive device numbers controlled (usually 1).

cdev_init

fs/char_dev.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 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 allocatedstruct cdev. Establish the connection between cdev and file_operations.

Note: after callingcdev_initit is best to callcdev_test.owner = THIS_MODULE;to point the owner field to this module, which prevents unloading the module while its operations are in use.

cdev_add

fs/char_dev.c

1
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
/**
* 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, i.e., add a character device.
  • cdev_map: globalkobj_mapstructure (hash table)
  • p: points tostruct cdev
  • Note:cdevis embedded instruct kobject kobj, socdevcan be regarded as akobject

cdev_del

fs/char_dev.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 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 from the system

Example

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>

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 prevents 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 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 the driver. When we call the open function in the application, it ultimately executes the open function in the driver. Thus, file_operations connects system calls with the driver.

Calling the open function in the application

1
fd = open("/dev/hello", O_RDWR);

Executing the open function in the driver

1
2
3
4
5
6
7
8
9
10
11
12
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

1
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
32
33
34
35
36
37
38
39
40
41
42
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);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
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;

Analysis of common fields:

Function pointerUser-space callFunction
ownerUsually written asTHIS_MODULE, to prevent the module from being unloaded while in use
openopen()Called when opening the device, typically used for initialization or counting open times
releaseclose()Called when closing the device, used to release resources
readread()User reads device data
writewrite()User writes data to the device
unlocked_ioctlioctl()User sends control commands
pollpoll()/select()Implement non-blocking I/O (epoll mechanism)
mmapmmap()Map physical memory to user space
llseeklseek()File position offset
fasyncfcntl(F_SETFL, O_ASYNC)Support asynchronous notification

Return value convention

  • ssize_t: >=0 success byte count, <0 errno
  • int/long: 0 success, <0 errno
  • Pointer: returns pointer on success, returnsERR_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 file beginning
      • SEEK_CUR: relative to current file pointer
      • SEEK_END: relative to file end
  • Return value
    • new file pointer position (loff_t
    • returns negative value on error (e.g.,-EINVAL

read

1
ssize_t (*read)(struct file *file, char __user *buf, size_t count, loff_t *pos);
  • Function: read data from file to user space.
  • Parameters
    • file: File object.
    • buf: Buffer received from user space.
    • count: Size of data requested for transfer (size of user buffer).
    • pos: Indicates the starting position for reading data from the file, which needs to be updated after reading.
  • Return value
    • Success: Number of bytes actually read
    • Error: Negative errno

Read steps

  1. Prevent read operation from exceeding file size and return end of file:
1
2
if (*pos >= filesize)
return 0; /* 0 means EOF */
  1. The number of bytes read cannot exceed the file size. Adjust count appropriately:
1
2
if (*pos + count > filesize)
count = filesize - (*pos);
  1. Find the starting position for reading:
1
void *from = pos_to_address (*pos); /* convert pos into valid address */
  1. Copy data to user space buffer, return error on failure:
1
2
3
sent = copy_to_user(buf, from, count);
if (sent)
return -EFAULT;
  1. Advance the current file position by the number of bytes read and return the number of bytes copied:
1
2
3
*pos += count;

return count;

write

1
ssize_t (*write)(struct file *file, const char __user *buf, size_t count, loff_t *pos);
  • Purpose: Write user data to file/device.
  • Parameters
    • file: file object
    • buf: user buffer
    • count: length of data to be transferred
    • pos: indicates the starting position in the file where data should be written
  • Return value
    • Success: number of bytes actually written
    • Error: negative errno

write steps

  1. 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:
1
2
3
4
5
/* If attempting to write beyond the end of the file, return an error
* Here,filesizecorresponds to the size of the device memory(if any)
*/

if ( *pos >= filesize ) return -EINVAL;
  1. Adjust count for the remaining bytes so as not to exceed the file size. This step is also not necessary, related to the same condition as in step (1):
1
2
3
/* The file size is forced to match the device memory size. */
if (*pos + count > filesize)
count = filesize - *pos;
  1. Find the starting position for writing. This step is relevant only when the device has memory and the write() method is to write the specified data into it. Like step (2), this step is not mandatory:
1
2
/* Convert pos to a valid address.*/
void *from = pos_to_address( *pos );
  1. Copy data from user space and write it to the corresponding kernel space:
1
2
3
4
5
6
if (copy_from_user(dev->buffer, buf, count) != 0){
retval = -EFAULT;
goto out;
}

/* Now move the data from dev->buffer to the physical device. */
  1. Write to the physical device, returning an error on failure:
1
2
3
4
write_error = device_write(dev->buffer, count);

if ( write_error )
return -EFAULT;
  1. Move the current file cursor position by the number of bytes written. Finally, return the number of bytes copied:
1
2
3
*pos += count;

return count;

mmap

1
int (*mmap)(struct file *, struct vm_area_struct *);
  • Purpose: Supports mapping user-space memory to the device.
  • Parameters
    • file: File object
    • vma: Virtual memory area structure
  • Return value
    • 0 on success
    • <0 on error
  • mmap_supported_flags: Flags supported during mapping.

open

1
int (*open)(struct inode *inode, struct file *file);
  • Function: Called when a user-space callopen()opens 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_data, etc.
  • Return value
    • 0: Successfully opened
    • <0: On error, returns the corresponding errno (e.g.,-EBUSYindicates device busy,-ENOMEMinsufficient memory)
  • Typical use
    • Check if the device is already occupied
    • Initializefile->private_dataPointer
    • Increase module reference count (THIS_MODULE

flush

1
int (*flush)(struct file *file, fl_owner_t id);
  • Function: Flush the file buffer to ensure that data written in user space is committed in the kernel buffer.
  • Parameters
    • file: File object
    • id: File handle owner identifier, typically the process corresponding to the file descriptor
  • Return value
    • 0: Success
    • <0: Error
  • Description
    • For most character device drivers,flushno special implementation is needed; simply return 0
    • In block devices or network devices,flushit may be used to submit cached data

release

1
int (*release)(struct inode *inode, struct file *file);
  • Function: Called when a user-space callclose()closes the device or file.
  • Parameters
    • inode: Device corresponding inode
    • file: File object
  • Return value
    • 0: Successfully closed
    • <0: Error
  • Typical use
    • Release resources allocated duringopen()resources allocated at the time
    • Restore device state (e.g., mark as unoccupied)
    • Decrease module reference count

Example

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>

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. Forthe file used for device access is called a device node. By operating this “device file”, the program can control the corresponding hardware.

1
fd = open("/dev/hello", O_RDWR);

This “device file” is the device node. Therefore, the Linux device node serves as a bridge between applications and drivers. Device nodes are created under the /dev directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[zhaohang@cyberboy linux_driver_learning]$ ll /dev
crw------- 4,15 root 11 Nov 10:05 tty15
crw------- 4,16 root 11 Nov 10:05 tty16
crw------- 4,17 root 11 Nov 10:05 tty17
crw------- 4,18 root 11 Nov 10:05 tty18
crw------- 4,19 root 11 Nov 10:05 tty19
crw------- 4,20 root 11 Nov 10:05 tty20
crw------- 4,21 root 11 Nov 10:05 tty21
crw------- 4,22 root 11 Nov 10:05 tty22
crw------- 4,23 root 11 Nov 10:05 tty23
crw------- 4,24 root 11 Nov 10:05 tty24
crw------- 4,25 root 11 Nov 10:05 tty25
crw------- 4,26 root 11 Nov 10:05 tty26
crw------- 4,27 root 11 Nov 10:05 tty27
crw------- 4,28 root 11 Nov 10:05 tty28
crw------- 4,29 root 11 Nov 10:05 tty29

4,15Indicates the major and minor device numbers. In crw, ‘c’ stands for character device.

Depending on the method of creating device nodes, they are divided intoManual creation of device nodesandAutomatic creation of device nodes

Methods of creating nodes under Linux

Manual creation of device nodes

Use the mknod command to manually create a device node. The format of the mknod command is:

1
$ mknod NAME TYPE MAJOR MINOR

Parameter meanings:

  • NAME: Name of the node to be created
  • TYPE: b indicates a block device, c indicates a character device, p indicates a pipe
  • MAJOR: Major device number of the device to link
  • MINOR: Minor device number of the device to link

For example: Use the following command to create a character device node named device_test, with major device number 236 and minor device number 0.

1
$ mknod /dev/device_test c 236 0

For the above file_operations example:

1
2
3
4
5
6
7
8
9
10
11
12
$ 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

Automatically creating device nodes

Automatically creating device nodes is achieved using theudevmechanism.

udev is a user-space program, which can detect the status of hardware devices in the system andcreate or delete device files based on the status of hardware devices in the system

Automatically creating device nodesIn the driver, you first need to use the class_create() function to create a class, which will be located in the /sys/class/ directory.,**Then use the device_create() function to create the corresponding device under this class.**When loading the driver module, 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 automatically creates mdev.

class_create function

linux/device/class.h

Include the header file using linux/device/device.h because device.h references class.h.

1
2
3
4
5
6
7
8
/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

  • Function purpose: Used to dynamically create a device class.
  • Parameter meaning
    • owner: A pointer to a struct module type. Generally assigned as THIS_MODULE.
    • name: A pointer of char type, representing the name of the struct class variable to be created.
  • Return valuestruct class *: A structure of type struct class
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

After creating the class using class_create, you also need to use device_create function to create a device under the class.

1
2
3
__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 this device, or NULL if none.
    • devt: specifies the device number for creating the device.
    • drvdata: Data added to the device callback; specify NULL if none.
    • fmt: The name of the device node 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 from a class.
  • Parameter meanings:
    • cls: Specifies the class to which the device to be created belongs.
    • devt: Specifies the device number for creating the device.

Example

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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

Memory in kernel space and user space cannot access each other. However, many applications need to exchange data with the kernel, for example, an application usesread()a function to read data from the driver, useswrite()To write data to the driver, the above functionality requires usingcopy_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.

User space ----> Kernel space

  • System call
  • Soft interrupt
  • Hardware interrupt

include/linux/uaccess.h

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/*
* 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);
}

#ifdef INLINE_COPY_FROM_USER
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;
}
#else
extern __must_check unsigned long
_copy_from_user(void *, const void __user *, unsigned long);
#endif

#ifdef INLINE_COPY_TO_USER
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;
}
#else
extern __must_check unsigned long
_copy_to_user(void __user *, const void *, unsigned long);
#endif

static __always_inline unsigned long __must_check
copy_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_check
copy_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;
}
#ifdef CONFIG_COMPAT
static __always_inline unsigned long __must_check
copy_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;
}
#endif

#ifndef copy_mc_to_kernel
/*
* 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_check
copy_mc_to_kernel(void *dst, const void *src, size_t cnt)
{
memcpy(dst, src, cnt);
return 0;
}
#endif

The overall call chain is as follows

1
2
3
4
5
6
7
8
9
copy_from_user(to, from, n)
└─ check_copy_size(to, n, false) // Compile-time object size check (FORTIFY_SOURCE)
└─ _copy_from_user(to, from, n)
├─ might_fault() // Mark potential page faults (scheduling points)
├─ should_fail_usercopy() // Fault injection (for testing)
├─ access_ok(from, n) // Check if 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 will be zeroed out. This is to prevent kernel information leaks (e.g., uninitialized memory on the stack being read by users).

  • access_ok(): Ensure thatfrompoints to user space and the length is valid (prevent access to kernel addresses)

  • check_object_size(): Prevent buffer overflow (in conjunction with FORTIFY_SOURCE)

  • instrument_*: KASAN detects out-of-bounds access

Even if the copy fails, it will not panic; instead, it returns the number of uncopied bytes, which the driver can handle accordingly.

copy_to_user()

Function prototype:

1
2
3
4
5
6
7
static __always_inline unsigned long __must_check
copy_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 meanings:
    • *tois a pointer to user space
    • fromis a pointer to kernel space
    • nis the number of bytes to copy from kernel space to user space
  • Return value: 0 indicates success, others indicate failure

copy_from_user()

1
2
3
4
5
6
7
static __always_inline unsigned long __must_check
copy_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: Copy data from user space to kernel space.
  • Parameter meaning
    • *tois a pointer to kernel space
    • fromis a pointer to user space
    • nis the number of bytes to copy from user space to kernel space
  • Return value: 0 indicates success, others indicate failure

copy_struct_from_user()

This is the recommended interface for modern syscalls.

Problem solved: Different versions of structures passed from user space.

  • If the struct passed by the user is smaller, the kernel automatically zero-pads
  • If the struct passed by the user is larger, check if the extra fields are zero

Return value

  • 0Success
  • -EFAULTAccess failure
  • -E2BIGNon-zero extended field
1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* 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 int
copy_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()

1
2
3
4
5
6
7
8
9
// include/asm-generic/uaccess.h
#define get_user(x, ptr) \
({ \
const void __user *__p = (ptr); \
might_fault(); \
access_ok(__p, sizeof(*ptr)) ? \
__get_user((x), (__typeof__(*(ptr)) __user *)__p) :\
((x) = (__typeof__(*(ptr)))0,-EFAULT); \
})

Reads a simple variable, handling faults internally. The macro copies the value of a user-space variable to kernel space. Note: x is set to 0 on error.

  • x represents the kernel variable storing 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 on error returns-EFAULT

put_user()

1
2
3
4
5
6
7
8
9
10
// include/asm-generic/uaccess.h
#define put_user(x, ptr) \
({ \
void __user *__p = (ptr); \
might_fault(); \
access_ok(__p, sizeof(*ptr)) ? \
__put_user((x), ((__typeof__(*(ptr)) __user *)__p)) : \
-EFAULT; \
})

This macro copies a kernel-space variable value to user space.

  • x represents 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 on error returns-EFAULT

Example

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#include "linux/printk.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

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 prevents 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:

1
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
32
33
34
35
36
37
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

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

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
TARGET_EXEC := test
SRC_DIRS := ./
BUILD_DIR := ./build

CC = aarch64-linux-gnu-gcc
CFLAGS = -g -Wall

# Source file collection
SRCS := $(shell find $(SRC_DIRS) -name '*.c' -or -name '*.s' -or -name '*.S')

# Object file mapping
OBJS := $(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))
OBJS := $(patsubst %.s,$(BUILD_DIR)/%.o,$(OBJS))
OBJS := $(patsubst %.S,$(BUILD_DIR)/%.o,$(OBJS))

# Include path
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

# Auto-generate dependency files
CPPFLAGS := $(INC_FLAGS) -MMD -MP

# Link 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 without preprocessing
$(BUILD_DIR)/%.o: %.s
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# Assembly code requiring preprocessing
$(BUILD_DIR)/%.o: %.S
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# 清理
.PHONY: clean deploy
clean:
rm -rf $(BUILD_DIR)
deploy:
cp $(BUILD_DIR)/$(TARGET_EXEC) ~/tftp

# 自动依赖包含
-include $(OBJS:.o=.d)

测试输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~ # 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 ok
read 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

文件私有数据

通常在驱动开发中会为设备自定义设备结构体将设备结构体设置为文件私有数据。设备结构体用来存放硬件相关信息,如设备号、类、设备名称等。

Linux 中并没有明确规定要必须要使用文件私有数据,但是在 linux 驱动源码中随处可见文件私有数据,因此可以认为使用文件私有数据是 Linux 驱动遵循的“潜规则”,实际上文件私有数据也体现了 Linux 面向对象的思想。

文件私有数据就是将私有数据 private_data 指向设备结构体。

1
2
3
4
5
6
struct device_test dev1;

static int cdev_test_open(struct inode *inode, struct file *file){
file->private_data = &dev1;
return 0;
}

然后在 read, write 等函数中通过 private_data 访问设备结构体

1
2
3
4
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;
}

用全局变量也可以实现这样的功能,但是随着驱动代码复杂度增多,使用 private_data 更利于管理

示例

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/string.h>

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));

// Apply for 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:

1
2
3
4
5
6
7
8
9
$ 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 called
Hello World from kernel
[ 15.000469] file_private_test_release is called

Scenario for using file private data

In Linux, the major device number is used to represent a specific type of driver, while the minor device number represents individual devices under that driver.

If our driver nowneeds to support devices with the same major device number but different minor device numbers, our driver can be written using file private data.

container_of macro

include/linux/kernel.h

1
2
3
4
5
6
#define container_of(ptr, type, member) ({				\
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
  • ptrPointer to a member within a structure.

  • typeTarget structure type.

  • memberName of the member in the structure.

  • BUILD_BUG_ON_MSG(...)Compile-time type check to ensure ptr matches the structure member type, 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)Standard macro to calculate the byte offset of a member within a structure.

Example

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#include <linux/module.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define KBUF_SIZE 32
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 device, which is an encapsulation of character devices and is essentially 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:

  1. Saving major device numbers: The major device number of miscellaneous devices is fixed at 10, so when multiple miscellaneous device drivers are registered in the system, they only need to be distinguished by minor device numbers. In contrast, character devices, whether dynamically or statically allocated, consume a major device number, leading to waste of major device numbers.
  2. Simple to use:**The miscellaneous device driver itself includes operations for creating device nodes, eliminating the need for additional use of the class_create() function and device_create() function.**Implement automatic device node creation. Simply fill in the members of the file_operations structure in the driver.

Definition

include/linux/miscdevice.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 miscellaneous device, generally only need to fill in the three member variables: minor, name, and fops in the miscdevice structure.

  • minorThe minor device number for the miscellaneous device can be selected from the predefined minor device numbers in the kernel sourceinclude/linux/miscdevice.hfile, or you can define your own minor device number (as long as it is not used by other devices). Typically, this parameter is set toMISC_DYNAMIC_MINOR, which means the minor device number is automatically allocated.
  • nameIndicates the name of the miscellaneous device. That is, after the miscellaneous 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, mount the miscdevice structure to the misc_list, and initialize structures related to the Linux device model. This serves to register the miscellaneous device.

  • Parameter meaning

    • misc: Pointer to the miscellaneous device structure
  • Function return value: Returns 0 on success, negative on failure

misc_deregister

1
extern void misc_deregister(struct miscdevice *misc);
  • Function purpose: Deletes miscdevice from mist_list, thereby unloading the miscellaneous device.
  • Parameter meaning
    • misc: Pointer to the miscellaneous device structure

Example

1
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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>

static struct file_operations fops = {
.owner = THIS_MODULE,
};
static struct miscdevice miscdev={
.minor=MISC_DYNAMIC_MINOR,// Dynamically allocate 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 release

Reverse release: Later allocated ones are released first

1
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
32
33
34
35
36
37
38
39
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’, meaninglater allocated ones are released first
  • PTR_ERR()+IS_ERR()is used to determineclass_create()anddevice_create()error.
  • PTR_ERR()Convert the error pointer to an error code and return the error code.
  • gotoCan avoid writing multiple times.ifCheck and clean up the code to make it concise.

IS_ERR()

If a function returns a pointer type, it will return a NULL pointer in case of an error, but returning NULL does not reveal 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 (i.e., null pointer)
  • Error pointer (i.e., 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 is associated with kernel-defined error codes, so error codes can be used to indicate the corresponding error condition. 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 IS_ERR()、PTR_ERR(), ERR_PTR() functions, etc., and the source code of these functions is ininclude/linux/err.hin the file.

include/linux/err.h

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_ERR_H
#define _LINUX_ERR_H

#include <linux/compiler.h>
#include <linux/types.h>

#include <asm/errno.h>

/*
* 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.
*/
#define MAX_ERRNO 4095

#ifndef __ASSEMBLY__

#define IS_ERR_VALUE(x) unlikely((unsigned long)(void *)(x) >= (unsigned long)-MAX_ERRNO)

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;
}

#endif

#endif /* _LINUX_ERR_H */

PTR_ERR()

Convert error pointer to error code

include/linux/err.h

1
2
3
4
static inline long __must_check PTR_ERR(__force const void *ptr)
{
return (long) ptr;
}

ERR_PTR()

Convert error code to pointer

1
2
3
4
static inline void * __must_check ERR_PTR(long error)
{
return (void *) error;
}

IS_ERR_OR_NULL()

Determine if it is an error or NULL

1
2
3
4
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

1
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
32
33
34
35
36
37
38
39
40
41
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */

#endif

Chinese translation:

1
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
32
33
34
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */

Example:

1
2
3
4
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:

1
2
3
4
5
6
#include <errno.h> /* Access errno global variable */
#include <string.h>

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 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 indicating success.

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 a matching device is successfully found; otherwise, it returns 0.

RK3568 Lighting Up the LED

Schematic

The topeet RK3568 Working LED schematic is as follows

topeet RK3568 Working LED schematic
topeet RK3568 Working LED schematic

It can be seen that the LED is connected toGPIO0_B7this GPIO pin. When GPIO0_B7 is at a high level, transistor Q16 conducts, and LED9 lights up. When GPIO0_B7 is at a low level, transistor Q16 is cut off, and LED9 does not light up.

Query register address

GPIO0B pin multiplexing

First, we need to set the pin multiplexing of GPIO0B, which can be found from PMU_GRF

GPIO0B multiplexing register address offset
GPIO0B multiplexing register address offset

Searching for GPIO0B7 shows that its corresponding register isPMU_GRF_GPIO0B_IOMUX_Hthe setting of bits 14:12

GPIOB7 pin multiplexing
GPIOB7 pin multiplexing

The PMU_GRF register address can be seen below as 0xFDC20000

PMU_GRF
PMU_GRF

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 register

From the Address Mapping in the TRM, it can be seen that the GPIO0 address is 0xFDD60000

Address Mapping
Address Mapping

Introduction to the GPIO chapter in the RK3568 TRM:

3568 TRM
3568 TRM

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

namely

  • GPIO_SWPORT_DDR_L/Hdetermineswhether the pin is input or output
  • GPIO_SWPORT_DR_L/Hdeterminesthe output level when the pin is output
  • GPIO_EXT_PORTreflectsthe current actual level of the pin (read-only)

Data registers and direction registers are divided into L and H

  • GPIOA and GPIOB addresses fall on L
  • GPIOC and GPIOD addresses fall on H

Also:

five GPIOs's register group have five different base addresses
five GPIOs's register group have five different base addresses

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.

Register offset address
Register offset address

GPIO_EXT_PORT
GPIO_EXT_PORT

GPIO has four groups: GPIOA, GPIOB, GPIOC, GPIOD. Each group is further distinguished by numbers 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

GPIO_SWPORT_DDR_L
GPIO_SWPORT_DDR_L

It can be seen that bits 31:16 are writable, so GPIOB7 should be 16+8+8-1=31, i.e.,bit 31 sets write access

And 0+8+8-1=15, i.e.,bit 15 corresponds to setting the direction of GPIO0B7 as input or output

Data register

GPIO_SWPORT_DR_L
GPIO_SWPORT_DR_L

Similarly, 16+8+8-1=31, i.e.,bit 31 sets write access

And 0+8+8-1=15, that isbit 15 corresponds to setting GPIO0B7 output high or low level

Summary

  • Multiplexing relationship registerhas a base address of 0xFDC20000
    • GPIO0B_IOMUX_H offset address is 000C, so the address to operate is base address + offset address = 0xFDC2000C
      • GPIO0B_IOMUX_H’s bit 28 to bit 30 are bit write access, 0 is disable, 1 is enable
      • GPIO0B_IOMUX_H’s bit 12 to bit 14 set to 0 is GPIO0_B7, 1 is PWM0_M0, 2 is CPU_AVS
  • GPIO0’s base address is 0xFDD60000
    • Direction registerAddress = base address + offset address = 0xFDD60008
      • bit 31 corresponds to GPIO0_B7 direction write access, 0 is disable, 1 is enable
      • bit 15 corresponds to GPIO0_B7 direction, 0 is input, 1 is output
    • Data RegisterAddress = Base Address + Offset Address = 0xFDD60000
      • Bit 31 corresponds to the write access of GPIO0_B7 data, 0 for disable, 1 for enable
      • Bit 15 corresponds to the data of GPIO0_B7, 0 for output low, 1 for output high

Test using the io command

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
su root
# Multiplexing Relationship Register
sudo io -4 -w 0xFDC2000C 0x70001000

# Verify if write is successful, read
sudo io -4 -r 0xFDC2000C

# Set Output
sudo io -4 -w 0xFDD60008 0x80008000

# Verify
sudo io -4 -r 0xFDD60008


# Read Current Value
sudo io -4 -r 0xFDD60000

# Turn On Light
sudo io -4 -w 0xFDD60000 0x8000C000

# Turn Off Light
sudo io -4 -w 0xFDD60000 0x80004000

Example

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#include <linux/module.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/uaccess.h>

/* Multiplexing Register */
#define PMU_GRF_BASE 0xFDC20000
#define GPIO0B_IOMUX_OFFSET 0xC
#define GPIO0B_IOMUX PMU_GRF_BASE + GPIO0B_IOMUX_OFFSET

/* Data Register and Direction Register */
#define GPIO0_BASE 0xFDD60000
#define GPIO_SWPORT_DDR_L_OFFSET 0x8
#define GPIO_SWPORT_DR_L_OFFSET 0x0
#define GPIO_EXT_PORT_OFFSET 0x70
// Direction Register
#define GPIO0_SWPORT_DDR_L GPIO0_BASE + GPIO_SWPORT_DDR_L_OFFSET
// Data Register
#define GPIO0_SWPORT_DR_L GPIO0_BASE + GPIO_SWPORT_DR_L_OFFSET
// External Input Register, Read-Only
#define GPIO0_EXT_PORT GPIO0_BASE + GPIO_EXT_PORT_OFFSET

struct 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);
// Close
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:

1
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
32
33
34
35
36
37
38
39
40
41
42
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

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);
}