My First Driver Program on ARM Board

Abstract:There are two directions in embedded systems: one is embedded software development (MCU direction), and the other is embedded software development (Linux direction). Among them, the MCU direction is basically bare-metal development and RTOS development, while the Linux development direction is further divided into driver development and application development. Compared to driver development, application development is relatively simpler because working with drivers requires interaction with the Linux kernel. Ordinary microcontroller development is akin to application development, with little difference from Linux development, as microcontroller programming often involves using libraries written by others, just as Linux applications interact with drivers written by others.

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.My First Driver Program on ARM Board

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.

My First Driver Program on ARM Board

1. Introduction to Character Device Drivers

Character devices are the most basic type of device driver in Linux, where character devices operate on a byte-by-byte basis, performing read and write operations in a sequential manner. For example, the most common character devices include LED blinking, buttons, I2C, SPI, LCD, etc., and the drivers for these devices are called character device drivers.

So how do applications in Linux call driver programs? The call process from Linux applications to driver programs is illustrated below:

My First Driver Program on ARM Board

Call flow from Linux applications to driver programs

In Linux, everything is treated as a file. Once a driver is successfully loaded, a corresponding file will be generated in the <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.
Anyone writing drivers must understand the Linux kernel because driver programs are written based on kernel functions. Application developers do not need to understand the Linux kernel, only the driver functions.
For instance, if there is a driver file named <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.
Applications run in user space, while Linux drivers are part of the kernel, thusdrivers run in kernel space. When we want to perform operations on the kernel from user space, such as using the open function to open the /dev/led driver, sinceuser space cannot directly manipulate the kernel, we must use a method called ‘system call’ to transition from user space ‘into’ kernel space, enabling us to operate on the lower-level driver.
Functions like open, close, write, and read are provided by the C library, and in Linux systems, system calls are part of the C library. When we call the open function, the process is as follows:
My First Driver Program on ARM Board

Flow of the open function call

Regarding the C library and how to ‘trap’ into kernel space via system calls, we don’t need to worry about that; we focus on the application and the specific driver. Functions used by applications have corresponding functions in the driver program, for instance, if the application calls the open function, there must also be a function named open in the driver program. Every system call has a corresponding driver function in the driver.
In the Linux kernel file include/linux/fs.h, there is a structure called file_operations, which is a collection of Linux kernel driver operation functions. We can download the Linux kernel files and open them using source insight to take a look. The contents are as follows:
Click here to download the Linux kernel source code:
My First Driver Program on ARM Board
My First Driver Program on ARM Board
My First Driver Program on ARM Board

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
};
My First Driver Program on ARM Board
  • 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

When an APP uses the open function to open a file, it receives an integer known as a file handle. For each file handle in the APP, there is a corresponding <span>struct file</span> in the kernel.
My First Driver Program on ARM Board
struct file
When we open a file using open, the parameters such as flags and mode are recorded in the corresponding struct file in the kernel (f_flags, f_mode):
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.

My First Driver Program on ARM Board
open->struct file
When opening a character device node, there is also a corresponding <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.
My First Driver Program on ARM Board

Driver program’s struct file

My First Driver Program on ARM Board

Driver program’s open/read/write

The definition of the structure <span>struct file_operations</span> is as follows, as mentioned earlier.
My First Driver Program on ARM Board

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;
}
The code here is simple, so there’s no need to elaborate; it involves knowledge of Linux application development.

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.

Once the Makefile is written, enter the <span>make</span> command to compile the driver module, and the compilation process is illustrated below:
My First Driver Program on ARM Board

Sometimes you might encounter the following error:

My First Driver Program on ARM Board
This error is caused by the Linux source code in Ubuntu not being compiled. Use the following commands to compile the source code:
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.

My First Driver Program on ARM Board

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.

My First Driver Program on ARM Board
My First Driver Program on ARM Board

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.

My First Driver Program on ARM Board
File System Directory Overview

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
This mounts the <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.
Of course, the <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.
My First Driver Program on ARM 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
My First Driver Program on ARM Board
If the module loads successfully, there will be no prompts; if it fails, there will be an error message, usually due to a version mismatch between your module and the ARM board’s version.
Inputting the <span>lsmod</span> command will show the currently existing modules in the system.
lsmod
My First Driver Program on ARM Board

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
My First Driver Program on ARM Board

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
My First Driver Program on ARM Board

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
My First Driver Program on ARM Board

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:

My First Driver Program on ARM Board
It can be seen that there are currently no modules in the system, and the <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.

My First Driver Program on ARM Board

END

Source: Guoguo Xiaoshidi
Copyright belongs to the original author; please contact for deletion if there is any infringement.
Recommended Reading
Example of Layered Isolation in Embedded Software
He Brought the Linux System Back to China
Several Tricks of State Machines in Embedded Systems
→ Follow to avoid getting lost ←

Leave a Comment