Many people follow the learning path from microcontrollers to RTOS, and then to Linux, which is a very good approach, allowing for gradual progression. Because you have learned microcontrollers, understanding RTOS will be much easier. Microcontroller + RTOS can also lead to good job opportunities in the market. Once you learn RTOS, you will find that Linux driver development is quite similar to RT-Thread driver programming, as RT-Thread drivers are likely modeled after Linux drivers. Therefore, if you are currently learning RT-Thread, transitioning to Linux driver development will be very manageable.
Of course, before diving into driver development, you should learn about the Ubuntu operating system, ARM bare-metal, and Linux system porting to prepare for embedded Linux driver development.Without further ado, let’s start with a hello driver program.
In Linux, drivers are classified into three main categories:
-
Character Device Drivers -
Character device drivers are the most extensive type of driver, as there are the most character devices, ranging from the simplest LED blinking to I2C, SPI, audio, etc. -
Block Device Drivers -
Block device and network device drivers are more complex than character device drivers, which is why semiconductor manufacturers generally provide these for us, and in most cases, they are ready to use. -
The so-called block device drivers are for storage devices, such as EMMC, NAND, SD cards, and USB drives, because these storage devices are based on storage blocks, hence the name block devices. -
Network Device Drivers -
Network device drivers are straightforward; whether wired or wireless, they fall under the category of network device drivers. A device can belong to multiple driver types; for example, a USB WiFi device uses a USB interface, so it is a character device, but it can also connect to the internet, thus also qualifying as a network device driver.
I am using Linux kernel version 4.1.15, which supports Device Tree.The development board is the Linux-MINI board provided by ZhenDianYuanZi, and using boards from other manufacturers will have no effect.

1. Introduction to Character Device Drivers
So how do applications in Linux call driver programs? The call process from Linux applications to driver programs is illustrated below:

Call flow from Linux applications to driver programs
<span>/dev</span>
directory, and applications can perform operations on this file named <span>/dev/xxx</span>
(where xxx is the specific driver file name) to interact with the hardware.<span>/dev/led</span>
, which is the driver file for an LED. The application uses the open function to open the file <span>/dev/led</span>
, and after use, the close function is called to close <span>/dev/led</span>
. The open and close functions are for opening and closing the LED driver. To turn on or off the LED, the write function is used to send data to the driver, which indicates whether to turn the LED on or off. If you want to get the status of the LED, the read function is used to read the corresponding status from the driver.
Flow of the open function call



Linux Kernel
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 (*iterate) (struct file *, struct dir_context *);
unsigned int (*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 *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct inode *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, 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 pipe_inode_info *, loff_t *, struct file *, 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
};

-
Line 1589,owner is a pointer to the module that owns this structure, usually set to <span>THIS_MODULE</span>
. -
Line 1590, the llseek function is used to modify the current read/write position of the file. -
Line 1591, the read function is used to read from the device file. -
Line 1592, the write function is used to write (send) data to the device file. -
Line 1596, poll is a polling function used to check whether the device can perform non-blocking read/write operations. -
Line 1597, the unlocked_ioctl function provides control functions for the device, corresponding to the ioctl function in the application. -
Line 1598, the compat_ioctl function has the same functionality as the unlocked_ioctl function, except that on 64-bit systems, it will be called by 32-bit applications. On 32-bit systems, 32-bit applications will call unlocked_ioctl. -
Line 1599, the mmap function is used to map the device’s memory into the process space (i.e., user space). Generally, framebuffer devices use this function; for example, the video memory of an LCD driver can be mapped into user space, allowing the application to directly manipulate the video memory, thereby avoiding the need to copy data back and forth between user space and kernel space. -
Line 1601, the open function is used to open the device file. -
Line 1603, the release function is used to release (close) the device file, corresponding to the close function in the application. -
Line 1604, the fasync function is used to flush pending data, which refreshes the buffered data to disk. -
Line 1605, the aio_fsync function has similar functionality to the fasync function, except that aio_fsync is used for asynchronously flushing pending data.
2. Character Device Driver Development
When learning bare-metal or STM32 development, driver development involves initializing the corresponding peripheral registers, which is undoubtedly the case in Linux driver development as well. However, in Linux driver development, we need to write drivers according to the specified framework, so the key to learning Linux driver development is to understand its driver framework.
2.1 How Files Opened by APP are Represented in the Kernel
<span>struct file</span>
in the kernel.
int open(const char *pathname, int flags, mode_t mode);
When reading and writing files, the current offset of the file is also stored in the <span>struct file</span>
structure’s <span>f_pos</span>
member.

<span>struct file</span>
in the kernel. Note the structure within this structure:<span>struct file_operations *f_op</span>
, which is provided by the driver program.
Driver program’s struct file

Driver program’s open/read/write
<span>struct file_operations</span>
is as follows, as mentioned earlier.
2.2 Steps to Write a Driver Program
-
1. Determine the major device number, or let the kernel allocate it. -
2. Define your own <span>file_operations</span>
structure. -
3. Implement the corresponding <span>drv_open/drv_read/drv_write</span>
functions, and fill in the<span>file_operations</span>
structure. -
4. Inform the kernel of the <span>file_operations</span>
structure by calling<span>register_chrdev</span>
. -
5. Who registers the driver program? There must be an entry function: this function will be called when the driver program is installed. -
6. If there is an entry function, there should also be an exit function: the exit function will call unregister_chrdev when the driver program is unloaded. -
7. Other improvements: provide device information and automatically create device nodes: class_create, device_create.
2.3 Writing the Test Program
The driver must be tested after writing; usually, a simple test APP is created that runs in user space. The test APP is straightforward, allowing you to execute read or write operations on the hello_drv device by inputting corresponding commands.
hello_drv.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
/* 1. Determine the major device number*/
static int major = 200;
static char kernel_buf[1024];
static struct class *hello_class;
#define MIN(a, b) (a < b ? a : b)
/* 3. Implement the corresponding open/read/write functions and fill in the file_operations structure */
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
/* 2. Define your own file_operations structure*/
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
/* 4. Inform the kernel of the file_operations structure: register the driver program */
/* 5. Who registers the driver program? There must be an entry function: this function will be called when the driver program is installed */
static int __init hello_init(void)
{
int retvalue;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
retvalue = register_chrdev(major, "hello_drv", &hello_drv); /* /dev/hello */
if(retvalue < 0){
printk("chrdevbase driver register failed\r\n");
}
printk("chrdevbase init!\r\n");
return 0;
}
/* 6. If there is an entry function, there should also be an exit function: this function will be called when the driver program is unloaded*/
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello_drv");
}
/* 7. Other improvements: provide device information and automatically create device nodes */
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhiguoxin");
2.4 Writing the Test Program
Once the driver is written, it needs to be tested. Generally, a simple test APP is created that runs in user space. The test APP is straightforward, allowing you to execute read or write operations on the hello_drv device by inputting corresponding commands.
hello_drv_test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
app test
./hello_drv_test -w www.zhiguoxin.cn
./hello_drv_test -r
*/
int main(int argc, char **argv)
{
int fd;
char buf[1024];
int len;
/* 1. Check parameters */
if (argc < 2)
{
printf("Usage: %s -w <string>\n", argv[0]);
printf(" %s -r\n", argv[0]);
return -1;
}
/* 2. Open file */
fd = open("/dev/hello", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/hello\n");
return -1;
}
/* 3. Write or read file */
if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
{
len = strlen(argv[2]) + 1;
len = len < 1024 ? len : 1024;
write(fd, argv[2], len);
}
else
{
len = read(fd, buf, 1024);
buf[1023] = '\0';
printf("APP read : %s\n", buf);
}
close(fd);
return 0;
}
2.5 Writing the Makefile
KERNELDIR := /home/zhiguoxin/linux/IMX6ULL/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := hello_drv.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
$(CROSS_COMPILE)arm-linux-gnueabihf-gcc -o hello_drv_test hello_drv_test.c
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
-
Line 1, KERNELDIR indicates the Linux kernel source directory used by the development board; use an absolute path, and fill it in according to your actual situation.
-
Line 2, CURRENT_PATH indicates the current path, obtained directly by running the <span>pwd</span>
command. -
Line 3, obj-m indicates that the <span>hello_drv.c</span>
file will be compiled into the<span>hello_drv.ko</span>
module. -
Line 8, the specific compilation command, where modules indicates compiling the module, -C indicates changing the current working directory to the specified directory, which is the KERNELDIR directory. M indicates the module source directory; by adding M=dir to the <span>make modules</span>
command, the program will automatically go to the specified dir directory to read the module source and compile it into a<span>.ko</span>
file. -
Line 9, using the cross-compilation toolchain to compile
<span>hello_drv_test.c</span>
into an executable file that can run on the ARM board.
<span>make</span>
command to compile the driver module, and the compilation process is illustrated below:
Sometimes you might encounter the following error:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
After successful compilation, a file named <span>hello_drv.ko</span>
will be generated; this file is the driver module for the <span>hello_drv</span>
device. Thus, the driver for the <span>hello_drv</span>
device has been successfully compiled.

2.6 Running Tests
2.6.1 Uploading the Program to the Development Board for Execution
After the development board starts, copy the relevant files to the development board by mounting the Ubuntu directory via NFS. In simple terms, this means accessing files on the Ubuntu virtual machine directly from the development board over the network, treating them as local files.
Since my code is located in <span>/home/zhiguoxin/myproject/alientek_drv_development_source</span>
, we will share this directory as an NFS shared folder.


The IP of Ubuntu is 192.168.10.100, and it is usually mounted in the development board’s mnt directory, which is specifically intended for temporary mounts.

Then, use MobaXterm software to access the development board via SSH.
ubuntu ip:192.168.10.100
windows ip:192.168.10.200
development board ip:192.168.10.50
To mount the directory on the development board, execute the following command:
mount -t nfs -o nolock,vers=3 192.168.10.100:/home/zhiguoxin/myproject/alientek_drv_development_source /mnt
<span>mnt</span>
directory of the ARM board to the Ubuntu directory <span>/home/zhiguoxin/myproject/alientek_drv_development_source</span>
. This way, we can modify files in Ubuntu and directly execute executable files on the development board.<span>/home/zhiguoxin/myproject/</span>
directory and the <span>windows</span>
directory are shared, allowing me to modify files directly on <span>windows</span>
, and then synchronize files between Ubuntu and the development board.
2.6.2 Loading the Driver Module
The driver module <span>hello_drv.ko</span>
and the executable file <span>hello_drv_test</span>
are ready; the next step is to run tests. Here, I am using the mount method to mount the server’s project folder to the ARM board’s mnt directory. Enter the <span>/mnt/01_hello_drv</span>
directory and enter the following command to load the <span>hello_drv.ko</span>
driver file:
insmod hello_drv.ko

<span>lsmod</span>
command will show the currently existing modules in the system.lsmod

Currently, the system only has the <span>hello_drv</span>
module. Enter the following command to check if the <span>hello_drv</span>
device exists in the system:
cat /proc/devices

It can be seen that the <span>hello_drv</span>
device exists in the current system, with a major device number of <span>200</span>
, consistent with the major device number we set.
2.7 Creating Device Node Files
Once the driver loads successfully, a corresponding device node file must be created in the <span>/dev</span>
directory. Applications perform operations on this device node file to interact with specific devices. Enter the following command to create the <span>/dev/hello_drv</span>
device node file:
mknod /dev/hello_drv c 200 0
Here, <span>mknod</span>
is the command to create a node, <span>/dev/hello_drv </span>
is the node file to be created, <span>c</span>
indicates that this is a character device, <span>200</span>
is the major device number, and <span>0</span>
is the minor device number. After creation, the <span>/dev/hello_drv </span>
file will exist, and you can use the <span>ls /dev/chrdevbase -l</span>
command to check.
ls /dev/hello_drv -l

If the <span>hello_drv_test</span>
wants to read or write the <span>hello_drv</span>
device, it can directly perform read and write operations on <span>/dev/hello_drv</span>
. Essentially, the <span>/dev/hello_drv</span>
file is the user space representation of the <span>hello_drv</span>
device. Everything in Linux is a file, including devices, and you should now have this concept.
2.8 Testing hello_drv Device Operations
Everything is set up. Use the <span>hello_drv_test</span>
software to operate on the <span>hello_drv</span>
device to see if the read/write operations are normal. First, perform a write operation to input the string <span>www.zhiguoxin.cn</span>
into the kernel.
./hello_drv_test -w www.zhiguoxin.cn
Then read the just-written string back from the kernel.
./hello_drv_test -r

As you can see, the read/write operations are normal, indicating that the <span>hello_drv</span>
driver we wrote is functioning correctly.
2.9 Unloading the Driver Module
If you no longer use a device, you can unload its driver. For instance, enter the following command to unload the <span>hello_drv</span>
device:
rmmod hello_drv.ko
After unloading, use the <span>lsmod</span>
command to check if the <span>hello_drv</span>
module still exists:

<span>hello_drv</span>
module also does not exist, indicating that the module has been successfully unloaded. Moreover, the <span>hello_drv</span>
device is no longer present in the system.Thus, the entire driver for the <span>hello_drv</span>
device has been validated and is functioning correctly. Future character device driver experiments can essentially use this as a template for writing.
3. Conclusion
The above is about character drivers in Linux. It may seem a bit challenging for beginners; I did not explain the code here because there is not much to elaborate on. It involves practical applications of object-oriented programming and pointer functions that I often mention in microcontroller development. Therefore, for embedded systems, it is essential to solidify the fundamentals of C programming, especially structures, pointers, and linked lists. If you can thoroughly understand these three concepts, Linux driver programming will be very straightforward, as driver development equals software architecture plus hardware operations. And for software architecture, you need to be very familiar with C programming, while hardware operations involve manipulating those few registers in microcontrollers.
Of course, if you have learned RT-Thread, then learning Linux drivers will also be quite easy, as RT-Thread consists of a kernel plus drivers, while FreeRTOS is merely a kernel.

END