Abstract: Previously, we introduced the method of lighting up an LED using bare metal. Today, we will light up an LED using driver development and compare the two methods.
1. First, Let’s Look at the Schematic
First, check the schematic to see which IO port the LED on our board is connected to.


From the schematic, we can see that the LED is connected to the third pin of GPIO1 on the chip, which is GPIO1_IO03.
2. GPIO Operation Methods for IMX6UL
First, let’s understand three terms:
-
CCM: Clock Controller Module -
IOMUXC: IOMUX Controller -
GPIO: General-purpose input/output
2.1 GPIO Module Structure
According to the chip manual “Chapter 26: General Purpose Input/Output (GPIO)”, we know that IMX6UL has a total of 5 GPIO groups (GPIO1 to GPIO5), and each group can have up to 32 pins, although there may not actually be that many.
GPIO1 has 32 pins: GPIO1_IO0~GPIO1_IO31;
GPIO2 has 22 pins: GPIO2_IO0~GPIO2_IO21;
GPIO3 has 29 pins: GPIO3_IO0~GPIO3_IO28;
GPIO4 has 29 pins: GPIO4_IO0~GPIO4_IO28;
GPIO5 has 12 pins: GPIO5_IO0~GPIO5_IO11;
We know that IMX6ULL has many IO pins, but not every pin can be used as GPIO; it can be reused for other modes, such as the I2C clock line I2C2_SCL. Therefore, to use a certain IO as GPIO, it needs to be multiplexed, which is handled by the IOMUXC_SW_MUX register in Linux. Additionally, we need to enable the clock for this GPIO, referred to as CCM in Linux, and set its IO speed, pull-up and pull-down resistors, drive strength, and slew rate (the time it takes for the IO level to change, e.g., how long it takes to go from 0 to 1; the shorter the time, the steeper the waveform, indicating a higher slew rate) using IOMUXC_SW_PAD in Linux.
Thus, if we want to use a certain GPIO group, for example, GPIO1_IO03, we first need to enable the clock for GPIO1, set GPIO1_IO03 to GPIO mode rather than IIC mode, configure the mode, speed, pull-up and pull-down resistors, slew rate, etc. Then set GPIO1_IO03 to output mode. Finally, we can write 0 or 1 to the data register (DR) of GPIO1_IO03 to control the LED’s on/off state.
2.2 Enabling the Clock
According to the chip manual, to enable the clock for GPIO1_IO03, we need to configure the CG13 bit of the CCGR1 register.


We also know that the address of this register is 20C406CH, so we can define a macro.
#define CCM_CCGR1_BASE (0X020C406C)// This register is used to enable the clock for GPIO1
/* 1. Enable GPIO1 clock */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* Clear previous settings */
val |= (3 << 26); /* Set new value */
writel(val, IMX6U_CCM_CCGR1);
2.3 IOMUXC Pin Multiplexing and Mode Configuration
Reference: Chip manual “Chapter 32: IOMUX Controller (IOMUXC)”. For a specific pin or group of pins, there are two registers in IOMUXC to set it.
IOMUXC_SW_MUX_CTL_PAD_pad-name
IOMUXC_SW_MUX_CTL_PAD_<padname> : Mux pad xxx, select the function of a specific pad
IOMUXC_SW_MUX_CTL_GRP_<group name=""> : Mux grp xxx, select the function of a group of pins
</group></padname>
Each pin or a predefined group of pins has 8 selectable modes (alternate (ALT) MUX_MODE).

For example, to set GPIO1_IO03 to GPIO mode, we need to set bits [0..3] of this register to 0101, which is 5.

We also see the address of this register is <span>Address: 20E_0000h base + 68h offset = 20E_0068h</span>
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)// This register is used to multiplex GPIO1_IO03 as GPIO
/* 2. Set the multiplexing function of GPIO1_IO03 to GPIO */
writel(5, SW_MUX_GPIO1_IO03);
IOMUXC_SW_PAD_CTL_PAD_
IOMUXC_SW_PAD_CTL_PAD_<pad_name>: pad pad xxx, set parameters for a specific pad
IOMUXC_SW_PAD_CTL_GRP_<group name="">: pad grp xxx, set parameters for a group of pins
</group></pad_name>

For example:

2.4 Internal GPIO Module
The block diagram is as follows:

We only need to focus on three registers:
① GPIOx_GDIR: Sets the pin direction, each bit corresponds to a pin, 1-output, 0-input

② GPIOx_DR: Sets the output level of the pin, each bit corresponds to a pin, 1-high level, 0-low level

③ GPIOx_PSR: Reads the level of the pin, each bit corresponds to a pin, 1-high level, 0-low level

3. How to Program?
3.1 Read GPIO
① Set a bit in the CCM_CCGRx register to enable the corresponding GPIO module, which is enabled by default. ② Set IOMUX to select the pin for GPIO. ③ Set a bit in GPIOx_GDIR to 0, setting that pin as input. ④ Read GPIOx_DR or GPIOx_PSR to get the value of that bit (reading GPIOx_DR returns the value of GPIOx_PSR).
3.2 Write GPIO
① Set a bit in the CCM_CCGRx register to enable the corresponding GPIO module, which is enabled by default. ② Set IOMUX to select the pin for GPIO. ③ Set a bit in GPIOx_GDIR to 1, setting that pin as output. ④ Write the bit value to GPIOx_DR.
Note: You can set the loopback function for that pin, allowing you to read the actual level from GPIOx_PSR; reading back from GPIOx_DR only returns the last set value and does not reflect the actual pin level, which could be affected by hardware faults causing the pin to short to ground. Setting GPIOx_DR to output high will not have any effect if that is the case.
With the knowledge above, we basically understand the process of lighting up the LED.
4. GPIO Register Operation Methods
Principle: Do not affect other bits.
4.1 Direct Read and Write
Read, modify the corresponding bit, and write:
-
To set bit n
val = data_reg;// Read
val = val | (1<<n);// Modify
data_reg = val;// Write
-
To clear bit n
val = data_reg;// Read
val = val & ~(1<<n);// Modify
data_reg = val;// Write
4.2 Set-and-Clear Protocol
set_reg, clr_reg, and data_reg correspond to the same physical register:
-
To set bit n: set_reg = (1<<n); -
To clear bit n: clr_reg = (1<<n);
5. Routine for Writing Driver Programs

-
1. Determine the major device number, or let the kernel allocate it. -
2. Define your own <span>file_operations</span><span> structure.</span>
-
3. Implement the corresponding <span>drv_open/drv_read/drv_write</span><span> functions, and fill in the </span> <code><span>file_operations</span><span> structure.</span>
-
4. Inform the kernel of the <span>file_operations</span><span> structure: </span> <code><span>register_chrdev</span><span>.</span>
-
5. Who registers the driver? There should be an entry function: the entry function is called when the driver is installed. -
6. If there is an entry function, there should be an exit function: the exit function calls unregister_chrdev when the driver is uninstalled. -
7. Other improvements: provide device information, automatically create device nodes: class_create, device_create.
How does the driver operate the hardware?
-
By using ioremap to map the physical address of the register to get the virtual address, and read/write using the virtual address.
How does the driver transfer data with the APP?
-
Using the functions <span>copy_to_user</span><span> and </span> <code><span>copy_from_user</span><span>.</span>
6. Address Mapping
Before writing the driver, we need to understand the magic of MMU, MMU stands for Memory Management Unit. In older versions of Linux, the processor was required to have an MMU, but now the Linux kernel supports processors without an MMU. The main functions of the MMU are as follows:
-
① Complete mapping from virtual space to physical space. -
② Memory protection, setting access permissions for memory, and setting buffering characteristics for virtual storage space.
We will focus on the first point, which is the mapping from virtual space to physical space, also known as address mapping. First, let’s understand two address concepts: Virtual Address (VA) and Physical Address (PA).
For a 32-bit processor, the virtual address range is 2^32=4GB. Our development board has 512MB of DDR3, and this 512MB of memory is physical memory, which can be mapped to the entire 4GB of virtual space through the MMU.

Physical memory is only 512MB, but virtual memory is 4GB, so multiple virtual addresses can map to the same physical address. The processor handles the issue of the virtual address range being larger than the physical address range.
When the Linux kernel starts, it initializes the MMU, sets up the memory mapping, and after that, the CPU accesses virtual addresses. For example, the address of the multiplexing register for the GPIO1_IO03 pin of I.MX6ULL is <span>0X020E0068</span>
. If the MMU is not enabled, we can directly write data to the register address 0X020E0068 to configure the GPIO1_IO03 multiplexing function. Now that the MMU is enabled and memory mapping is set up, we cannot directly write data to the address 0X020E0068. We must obtain the virtual address corresponding to the physical address 0X020E0068 in the Linux system, which involves the conversion between physical memory and virtual memory, requiring the use of two functions: ioremap and iounmap.
6.1 ioremap Function
The ioremap function is used to obtain the virtual address space corresponding to the specified physical address space, defined in <span>arch/arm/include/asm/io.h</span>
, defined as follows:
#include<asm/io.h>
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype,__builtin_return_address(0));
}
ioremap is a macro with two parameters: cookie and size, and the actual function is __arm_ioremap, which has three parameters and a return value. The meanings of these parameters and the return value are as follows:
-
phys_addr: The starting physical address to be mapped. -
size: The size of the memory space to be mapped. -
mtype: The type of ioremap, which can be MT_DEVICE, MT_DEVICE_NONSHARED, MT_DEVICE_CACHED, and MT_DEVICE_WC. The ioremap function selects MT_DEVICE. -
Return value: A pointer of type __iomem, pointing to the starting address of the mapped virtual space.
If we want to obtain the virtual address corresponding to the I.MX6ULL’s <span>IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03</span>
register, we can use the following code:
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
The macro <span>SW_MUX_GPIO1_IO03_BASE</span>
is the physical address of the register, and <span>SW_MUX_GPIO1_IO03</span>
is the mapped virtual address. For I.MX6ULL, a register is 4 bytes (32 bits), so the length of the mapped memory is 4. After mapping, we can directly read and write to <span>SW_MUX_GPIO1_IO03</span>
.
In fact, it is mapped by pages (4096 bytes), meaning it is mapped in whole pages. So although we are mapping 4 bytes, we are actually mapping 4096 bytes.
6.2 iounmap Function
When unloading the driver, we need to use the iounmap function to release the mapping made by the ioremap function. The prototype of the iounmap function is as follows:
void iounmap (volatile void __iomem *addr)
<span>iounmap</span>
has only one parameter <span>addr</span>
, which is the starting address of the virtual address space to be unmapped. If we want to cancel the mapping of the <span>IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03</span>
register address, we can use the following code:
iounmap(SW_MUX_GPIO1_IO03);
6.3 Using volatile
① The compiler is smart and will help us with optimizations. For example:
int a;
a = 0; // This statement can be optimized away without affecting a's result
a = 1;
② Sometimes the compiler makes assumptions, for example:
int *p = ioremap(xxxx, 4); // GPIO register address
*p = 0; // Turn on the light, but this statement gets optimized away
*p = 1; // Turn off the light
③ In such cases, to prevent the compiler from optimizing, we need to add volatile to inform it that this is error-prone and should not be optimized:
volatile int *p = ioremap(xxxx, 4); // GPIO register address
*p = 0; // Turn on the light, this statement will not be optimized away
*p = 1; // Turn off the light
7. I/O Memory Access Functions
The I/O here refers to input/output, and is not the GPIO pins we learned about in microcontrollers. This involves two concepts: I/O ports and I/O memory.
When external registers or memory are mapped to IO space, they are called I/O ports. When external registers or memory are mapped to memory space, they are called I/O memory.
However, for ARM, there is no concept of I/O space, so under the ARM architecture, there is only I/O memory (which can be directly understood as memory). After using the ioremap function to map the physical address of the register to the virtual address, we can directly access these addresses through pointers, but the Linux kernel does not recommend doing this. Instead, it recommends using a set of operation functions to read and write to the mapped memory.
What does this mean?
Simply put, I know that the clock register address for GPIO1_IO03 is 0X020C406C, but you cannot directly manipulate it.
#define CCM_CCGR1_BASE (0X020C406C)
0X020C406C is its actual physical address, but when the Linux kernel starts, it initializes the MMU, sets up memory mapping, and after that, the CPU accesses virtual addresses. Thus, we cannot operate on the actual physical address.
What should we do? No worries, Linux provides the ioremap memory mapping function. I know the actual physical address, and as long as I use this function, I automatically get the virtual address corresponding to this physical address.
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
Now we have obtained the virtual address IMX6U_CCM_CCGR1 corresponding to 0X020C406C, but we still cannot directly operate on this virtual address. After using the ioremap function to map the physical address of the register to the virtual address, we should not directly access these addresses. Instead, the Linux kernel recommends using a set of operation functions to read and write to the mapped memory.
So, even though I know the clock register address for GPIO1_IO03 is 0X020C406C, I cannot directly manipulate it.
#define CCM_CCGR1_BASE (0X020C406C)
Now we have obtained the virtual address IMX6U_CCM_CCGR1 corresponding to 0X020C406C, but we still cannot directly operate on this virtual address. After using the ioremap function to map the physical address of the register to the virtual address, we should not directly access these addresses. Instead, the Linux kernel recommends using a set of operation functions to read and write to the mapped memory.
So, even though I know the clock register address for GPIO1_IO03 is 0X020C406C, I cannot directly manipulate it.
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
Now we have obtained the virtual address IMX6U_CCM_CCGR1 corresponding to 0X020C406C, but we still cannot directly operate on this virtual address. After using the ioremap function to map the physical address of the register to the virtual address, we should not directly access these addresses. Instead, the Linux kernel recommends using a set of operation functions to read and write to the mapped memory.
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* Clear previous settings */
val |= (3 << 26); /* Set new value */
writel(val, IMX6U_CCM_CCGR1);
The specific read and write operation functions are as follows:
1. Read Operation Functions
There are several read operation functions:
u8 readb(const volatile void __iomem *addr)// Read 8bit
u16 readw(const volatile void __iomem *addr)// Read 16bit
u32 readl(const volatile void __iomem *addr)// Read 32bit
The functions readb, readw, and readl correspond to 8bit, 16bit, and 32bit read operations, respectively. The parameter addr is the memory address to read, and the return value is the data read.
2. Write Operation Functions
There are several write operation functions:
void writeb(u8 value, volatile void __iomem *addr)// Write 8bit
void writew(u16 value, volatile void __iomem *addr)// Write 16bit
void writel(u32 value, volatile void __iomem *addr)// Write 32bit
The functions writeb, writew, and writel correspond to 8bit, 16bit, and 32bit write operations, respectively. The parameters value is the value to write, and addr is the address to write to.
8. Program Writing
8.1 Writing the Driver Program
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/*
We need to configure a certain GPIO pin
1. First, enable the clock for this GPIO
2. Then multiplex this GPIO for GPIO functionality
3. Set the parameters for this GPIO
4. Set this GPIO as input or output
5. Write data to this GPIO's data register
*/
#define LED_MAJOR 200 /* Major device number */
#define LED_NAME "led" /* Device name */
#define LEDOFF 0 /* Turn off LED */
#define LEDON 1 /* Turn on LED */
/* Register physical addresses */
#define CCM_CCGR1_BASE (0X020C406C)// This register is used to open the clock for GPIO1
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)// This register is used to multiplex GPIO1_IO03 as GPIO
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)// This register is used to configure GPIO1_IO03's speed, drive strength, slew rate, etc.
#define GPIO1_DR_BASE (0X0209C000)// This register is the data register for GPIO1_IO03
#define GPIO1_GDIR_BASE (0X0209C004)// This register is used to set the direction of GPIO1_IO03, input or output
/* Mapped register virtual address pointers */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
/*
* @description : LED on/off
* @param - sta : LEDON(0) to turn on LED, LEDOFF(1) to turn off LED
* @return : None
*/
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
}
}
/*
* @description : Open device
* @param - inode : Pass the inode to the driver
* @param - filp : Device file, the file structure has a member variable called private_data
* Usually, during open, we point private_data to the device structure.
* @return : 0 on success; others on failure
*/
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* @description : Read data from the device
* @param - filp : The device file to be opened (file descriptor)
* @param - buf : The data buffer returned to user space
* @param - cnt : The length of data to read
* @param - offt : Offset relative to the start of the file
* @return : Number of bytes read, negative value indicates read failure
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : Write data to the device
* @param - filp : Device file, represents the opened file descriptor
* @param - buf : The data to be written to the device
* @param - cnt : The length of data to be written
* @param - offt : Offset relative to the start of the file
* @return : Number of bytes written, negative value indicates write failure
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /* Get the status value */
if(ledstat == LEDON) {
led_switch(LEDON); /* Turn on LED */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* Turn off LED */
}
return 0;
}
/*
* @description : Close/release device
* @param - filp : The device file to be closed (file descriptor)
* @return : 0 on success; others on failure
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* Device operation functions */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/*
* @description : Driver entry function
* @param : None
* @return : None
*/
static int __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
/* Initialize LED */
/* 1. Register address mapping */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
/* 2. Enable GPIO1 clock */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* Clear previous settings */
val |= (3 << 26); /* Set new value */
writel(val, IMX6U_CCM_CCGR1);
/* 3. Set GPIO1_IO03 multiplexing function to GPIO */
writel(5, SW_MUX_GPIO1_IO03);
/* Register SW_PAD_GPIO1_IO03 to set IO properties */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* Clear previous settings */
val |= (1 << 3); /* Set as output */
writel(val, GPIO1_GDIR);
/* 5. Default turn off LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 6. Register character device driver */
retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}
return 0;
}
/*
* @description : Driver exit function
* @param : None
* @return : None
*/
static void __exit led_exit(void)
{
/* Unmap */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/* Unregister character device driver */
unregister_chrdev(LED_MAJOR, LED_NAME);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhiguoxin");
With the explanation above, the code is quite simple, just follow the 7 steps to operate.
8.2 Writing the Test Program
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Usage:
./ledtest /dev/led 0 Turn off LED
./ledtest /dev/led 1 Turn on LED
***************************************************************/
#define LEDOFF 0
#define LEDON 1
/*
* @description : Main program
* @param - argc : Number of elements in argv array
* @param - argv : Specific parameters
* @return : 0 on success; others on failure
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* Open led driver */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]); /* Operation to perform: turn on or off */
/* Write data to /dev/led file */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
retvalue = close(fd); /* Close file */
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
The test program is simple and does not need much explanation.
8.3 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 := led.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
$(CROSS_COMPILE)arm-linux-gnueabihf-gcc -o ledApp ledApp.c
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
-
Line 1, KERNELDIR indicates the Linux kernel source directory used by the development board, using an absolute path. Everyone should fill it in according to their actual situation. -
Line 2, CURRENT_PATH indicates the current path, obtained by running the <span>pwd</span>
command. -
Line 3, obj-m indicates that the <span>led.c</span>
file will be compiled into the<span>led.ko</span>
module. -
Line 8, the specific compilation command, where modules indicates to compile the module, -C indicates to switch the current working directory to the specified directory, which is the KERNELDIR directory. M indicates the module source code directory, <span>make modules</span>
command with M=dir makes the program automatically read the module’s source code from the specified dir directory and compile it into a<span>.ko</span>
file. -
Line 9, using the cross-compilation toolchain to compile <span>ledApp.c</span>
into an executable<span>ledApp</span>
that can run on the ARM board.
After writing the Makefile, enter <span>make</span>
to compile the driver module. The compilation process is shown in the figure:

9. Running Tests
9.1 Uploading Programs to the Development Board for Execution
After the development board starts, use the NFS mount method to copy the corresponding files to the development board. In simple terms, this means accessing files on the Ubuntu virtual machine from the development board via the network as if they were local files.
Since my code is stored in <span>/home/zhiguoxin/myproject/alientek_drv_development_source</span>
, we will use this directory as the NFS shared folder.


The Ubuntu IP is 192.168.10.100, usually mounted in the development board’s mnt directory, which is specifically 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
Execute the following command on the development board to achieve the mount:
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 on the development board to the Ubuntu directory <span>/home/zhiguoxin/myproject/alientek_drv_development_source</span>
. This way, we can modify files under Ubuntu and directly execute the executable files on the development board.
Of course, my <span>/home/zhiguoxin/myproject/</span>
and <span>windows</span>
have a shared directory, so I can also modify files directly on <span>windows</span>
and then synchronize files between Ubuntu and the development board.

9.2 Loading the Driver Module
The driver module <span>led.ko</span>
and the executable file <span>ledApp</span>
are ready. Next, we will run the tests. Here, I use the mounted method to mount the server’s project folder to the ARM board’s mnt directory. Enter the <span>/mnt/02_led</span>
directory and input the following command to load the <span>led.ko</span>
driver file:
insmod led.ko

9.3 Creating Device Node Files
After successfully loading the driver, a corresponding device node file needs to be created in the <span>/dev</span>
directory. The application program operates on this device node file to perform operations on the specific device. Input the following command to create the <span>/dev/led</span>
device node file:
mknod /dev/led c 200 0
Where <span>mknod</span>
is the command to create the 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/led</span>
file will exist, and you can use the <span>ls /dev/led -l</span>
command to check.

9.4 LED Device Operation Test
Everything is ready. Use the <span>ledtest </span>
software to operate the <span>led</span>
device and check if it can normally turn on or off the LED.
./ledApp /dev/led 0 Turn off LED
./ledApp /dev/led 1 Turn on LED

9.5 Unloading the Driver Module
If a device is no longer in use, its driver can be unloaded. For example, input the following command to unload the <span>hello_drv</span>
device:
rmmod led.ko
After unloading, use the <span>lsmod</span>
command to check if the <span>led</span>
module still exists:

It can be seen that the system no longer has any modules, and the <span>led</span>
module does not exist, indicating that the module has been successfully unloaded. Moreover, the <span>led</span>
device no longer exists in the system.
Thus, the entire driver for the <span>led</span>
device has been validated, and the driver operates normally. Future character device driver experiments can basically use this as a template for writing.
Due to recent changes in the WeChat public platform push rules, many readers have reported not seeing updated articles in time. According to the latest rules, it is recommended to click on “Recommended Reading, Share, Collect”, etc., to become a regular reader.
Recommended Reading:
-
Foxconn sells all shares of Unisplendour overnight!
-
Huawei’s HiSilicon market share drops to zero, is Ren Zhengfei’s billion investment wasted?
-
Leaving Huawei to start a robotics company? The latest response from Zhi Hui Jun is here!
Please click 【Looking】 to give the editor a thumbs up
