Abstract: Yesterday, I introduced the method of lighting up an LED using bare metal programming. Today, we will light up an LED using driver development to see the differences between the two methods.
1. Let’s Look at the Schematic
First, let’s 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 the chip’s GPIO1, 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 groups of GPIOs (GPIO1 to GPIO5), with each group having a maximum of 32 pins, although there may not be that many available.
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 multiplexed for other functions, such as I2C clock line I2C2_SCL, among others. Therefore, to use a specific IO as GPIO, we need to multiplex it using the register responsible for multiplexing in Linux, IOMUXC_SW_MUX. We also need to enable the clock for this GPIO in Linux, referred to as CCM, and set its IO speed, pull-up/pull-down resistors, drive strength, slew rate (the time required for the IO level to change, such as how long it takes to switch from 0 to 1; the shorter the time, the steeper the waveform, indicating a higher slew rate), etc. In Linux, this is done using IOMUXC_SW_PAD.
Thus, to use a specific GPIO group, such as GPIO1_IO03, we first need to enable the clock for GPIO1, set GPIO1_IO03 to GPIO mode instead of IIC mode, configure the mode, speed, pull-up/pull-down resistors, slew rate, etc. 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 bit CG13 of the register CCGR1.


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, IOMUXC has two registers for configuration.
IOMUXC_SW_MUX_CTL_PAD_pad-name
IOMUXC_SW_MUX_CTL_PAD_<PADNAME>: Mux pad xxx, select a pad's function
IOMUXC_SW_MUX_CTL_GRP_<GROUP NAME>: Mux grp xxx, select a group of pins' function
Each pin or group of predefined pins has 8 selectable modes (alternate (ALT) MUX_MODE).

For instance, 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 that the address of this register is Address: 20E_0000h base + 68h offset = 20E_0068h
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)// This register is used to multiplex GPIO1_IO03 as GPIO
/* 2. Set the multiplexing function for GPIO1_IO03 to GPIO
* Finally, set the IO attributes. */
writel(5, SW_MUX_GPIO1_IO03);
IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>
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

For example:

2.4 Internal GPIO Module
The block diagram is as follows:

We only need to care about three registers:
① GPIOx_GDIR: Set pin direction, each bit corresponds to a pin, 1-output, 0-input

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

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

3. How to Program?
3.1 Read GPIO
① Set the corresponding bit in the CCM_CCGRx register to enable the GPIO module, which is enabled by default. ② Set the IOMUX to select the pin for GPIO. ③ Set the corresponding bit in GPIOx_GDIR to 0 to configure the pin as input. ④ Read GPIOx_DR or GPIOx_PSR to obtain the value of the corresponding bit (reading GPIOx_DR returns the value of GPIOx_PSR).
3.2 Write GPIO
① Set the corresponding bit in the CCM_CCGRx register to enable the GPIO module, which is enabled by default. ② Set the IOMUX to select the pin for GPIO. ③ Set the corresponding bit in GPIOx_GDIR to 1 to configure the pin as output. ④ Write the value to the corresponding bit in GPIOx_DR.
Note: You can set the loopback function for this pin, allowing you to read the actual level from GPIOx_PSR; however, reading from GPIOx_DR will only return the last set value and does not reflect the real level of the pin, which could be affected by hardware failures, such as a short circuit to ground. Setting GPIOx_DR to output high will not work in that case.
With the knowledge above, we have a basic understanding of the process to light 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, which can also be allocated by the kernel. -
2. Define your own <span>file_operations</span><span> structure.</span>
-
3. Implement corresponding <span>drv_open/drv_read/drv_write</span><span> functions, filling 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 must be an entry function: When installing the driver, this entry function will be called. -
6. With an entry function, there should be an exit function: When uninstalling the driver, the exit function calls unregister_chrdev. -
7. Other improvements: Provide device information, automatically create device nodes: class_create, device_create.
How does the driver operate hardware?
-
By using ioremap to map the physical address of registers to obtain virtual addresses for read/write operations.
How does the driver transfer data with the APP?
-
Using <span>copy_to_user</span><span> and </span><code><span>copy_from_user</span><span> functions.</span>
6. Address Mapping
Before writing a driver, we need to understand the MMU (Memory Management Unit). The MMU is responsible for mapping virtual addresses to physical addresses. In older versions of Linux, processors were required to have an MMU, but now the Linux kernel supports processors without an MMU. The main functions of the MMU are:
-
① Complete mapping from virtual space to physical space. -
② Memory protection, setting access permissions for storage, and setting buffering characteristics for virtual storage space.
We 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 concepts of addresses: Virtual Address (VA) and Physical Address (PA).
For a 32-bit processor, the virtual address range is 2^32=4GB, while our development board has 512MB of DDR3, which is the physical memory. The MMU can map this 512MB of memory to the entire 4GB of virtual space.

Since the physical memory is only 512MB and the virtual memory is 4GB, multiple virtual addresses will map to the same physical address. The processor automatically handles the issue of having a larger virtual address range than the physical address range.
When the Linux kernel starts, it initializes the MMU, sets up memory mappings, and after this setup, the CPU accesses only virtual addresses. For example, the address of the multiplexing register for GPIO1_IO03 in the I.MX6ULL is<span>GPIO1_IO03</span>
is<span>0X020E0068</span>
. If MMU is not enabled, we can directly write data to the address 0X020E0068 to configure the multiplexing function of GPIO1_IO03. However, with MMU enabled and memory mapping set, we cannot directly write data to the address 0X020E0068. We must obtain the virtual address corresponding to this physical address in the Linux system, which involves using the functions: ioremap and iounmap.
6.1 ioremap Function
The ioremap function is used to obtain the virtual address space corresponding to a specified physical address space, defined in<span>arch/arm/include/asm/io.h</span><span> as follows:</span>
#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. The function __arm_ioremap actually takes effect, which has three parameters and one return value, as follows:
-
phys_addr: The physical starting address to be mapped. -
size: The size of the memory space to be mapped. -
mtype: The type of ioremap, which can choose 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 register<span>IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03</span><span> of I.MX6ULL, we can use the following code:</span>
#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, while<span>SW_MUX_GPIO1_IO03</span>
is the mapped virtual address. For I.MX6ULL, a register is 4 bytes (32 bits), so the mapped memory length is 4. After mapping, we can directly read/write to<span>SW_MUX_GPIO1_IO03</span>
.
In reality, it maps by pages (4096 bytes), meaning it maps whole pages. So even though we are mapping 4 bytes, it actually maps 4096 bytes.
6.2 iounmap Function
When unloading drivers, we need to use the iounmap function to release the mapping done 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 address mapping for the register<span>IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03</span>
, we can use the following code:
iounmap(SW_MUX_GPIO1_IO03);
6.3 Usage of volatile
① The compiler is smart and will optimize some code, for example:
int a;
a = 0; // This statement can be optimized away without affecting a's result
a = 1;
② Sometimes the compiler makes its own 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 these cases, to prevent the compiler from automatically optimizing away, we need to add volatile, telling it this is error-prone and shouldn’t be optimized:
volatile int *p = ioremap(xxxx, 4); // GPIO register address
*p = 0; // Turn on the light, this statement won't be optimized away
*p = 1; // Turn off the light
7. I/O Memory Access Functions
Here, I/O refers to input/output, not GPIO pins as learned in microcontroller studies. This involves two concepts: I/O ports and I/O memory.
When external registers or memory are mapped to IO space, it is called I/O ports. When external registers or memory are mapped to memory space, it is called I/O memory.
However, for ARM, there is no concept of I/O space; hence, 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 registers to virtual addresses, we can directly access these addresses via pointers. However, the Linux kernel does not recommend this and instead recommends using a set of operation functions to read/write to the mapped memory.
What does this mean? In simple terms: I know that the clock register address for GPIO1_IO03 is 0X020C406C, but you cannot directly operate on 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 mappings, and after this setup, the CPU accesses only virtual addresses, so we cannot operate on the actual physical address.
What to do? Don’t worry; Linux provides the ioremap memory mapping function. I know the actual physical address, and by using this function, we automatically obtain the virtual address corresponding to this physical address.
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
Now we have obtained the virtual address corresponding to 0X020C406C, which is IMX6U_CCM_CCGR1. However, we still cannot directly operate on this virtual address because after mapping the physical address of the register to the virtual address using the ioremap function, we should not directly access these addresses. Instead, the Linux kernel recommends using a set of operation functions to perform read/write operations on the mapped memory.
So, even though I know that the address of the clock register is 0X020C406C, I cannot directly manipulate it.
#define CCM_CCGR1_BASE (0X020C406C)
Now we have obtained the virtual address corresponding to it, but we still cannot directly operate on this virtual address, as the Linux kernel does not recommend this. Instead, it provides read/write functions to manipulate this virtual address. Therefore, we must follow the kernel’s recommendations. For example, if I want to manipulate certain bits of the four bytes at this address, I need to read the memory space corresponding to this address, modify it, and then write the modified data back.
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
The read operation functions are as follows:
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
readb, readw, and readl correspond to 8bit, 16bit, and 32bit read operations respectively, where addr is the memory address to read from, and the return value is the data read.
2. Write Operation Functions
The write operation functions are as follows:
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
writeb, writew, and writel correspond to 8bit, 16bit, and 32bit write operations respectively, where value is the value to be written, 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 specific 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 the data register of this GPIO
*/
#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 enable 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 configures 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 sets the direction for 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) turn on LED, LEDOFF(1) 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 : Inode passed to the driver
* @param - filp : Device file, the file structure has a member variable called private_data
* Typically, the private_data is pointed to the device structure during open.
* @return : 0 on success; other 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 open (file descriptor)
* @param - buf : Buffer to return data to user space
* @param - cnt : Length of data to read
* @param - offt : Offset relative to the file start address
* @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, indicating the opened file descriptor
* @param - buf : Data to write to the device
* @param - cnt : Length of data to write
* @param - offt : Offset relative to the file start address
* @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 : Device file to close (file descriptor)
* @return : 0 on success; other 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 the multiplexing function for GPIO1_IO03 to GPIO
* Finally, set the IO attributes.
*/
writel(5, SW_MUX_GPIO1_IO03);
/* Register SW_PAD_GPIO1_IO03 to set IO attributes
* bit 16:0 HYS off
* bit [15:14]: 00 default pull-down
* bit [13]: 0 keeper function
* bit [12]: 1 pull/keeper enabled
* bit [11]: 0 open-drain output off
* bit [7:6]: 10 speed 100Mhz
* bit [5:3]: 110 R0/6 drive strength
* bit [0]: 0 low transition rate
*/
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 4. Set GPIO1_IO03 as output */
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 explanations above, the code is straightforward and follows those seven steps.
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 method :
./ledtest /dev/led 0 Turn off LED
./ledtest /dev/led 1 Turn on LED
***************************************************************/
#define LEDOFF 0
#define LEDON 1
/*
* @description : main function
* @param - argc : Number of elements in argv array
* @param - argv : Specific parameters
* @return : 0 on success; other 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 execute: 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 quite simple and requires no further 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, which should be an absolute path. Please fill in according to your 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 should be compiled into a<span>led.ko</span>
module. -
Line 8, the specific compilation command, where the modules indicate compiling modules, -C indicates switching the current working directory to the specified directory, which is the KERNELDIR directory. M indicates the module source directory, <span>make modules</span>
command will automatically read the module source from the specified dir 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 file 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 below:

9. Running Tests
9.1 Uploading Programs to the Development Board for Execution
After the development board starts, mount the Ubuntu directory via NFS and 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 NFS over the network, as if they were local files.
Since my code is located 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
To mount, execute the following command on the development board:
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 file on the development board.
Of course, my<span>/home/zhiguoxin/myproject/</span>
and<span>windows</span>
are shared directories, and I can also modify files directly on<span>windows</span>
, then synchronize files directly between Ubuntu and the development board.

9.2 Loading the Driver Module
The driver module<span>led.ko</span>
and the<span>ledApp</span>
executable file are ready. Next is the running test. 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, we need to create a corresponding device node file in the<span>/dev</span>
directory. The application program operates on this device node file to control the specific device. Enter 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 nodes,<span>/dev/hello_drv </span>
is the node file to create,<span>c</span>
indicates 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 to see 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, you may want to unload its driver. 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 whether 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 no longer exists, indicating that the module has been unloaded successfully. Moreover, the<span>led</span>
device is also no longer present in the system.
<span>led</span>
device is completed, and the driver works correctly. Future character device driver experiments can basically use this as a template for writing.
END
→Follow to Stay Updated←