Today, we will light up an LED using driver development. Let’s see what the differences are!
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.


Alright, from the schematic we know 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, IO Multiplexing 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 the IMX6UL has a total of 5 groups of GPIO (GPIO1 to GPIO5), each group can have up to 32 pins, but there may not be that many in practice.
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 the IMX6ULL has many IO pins, but not every pin can be used as GPIO. It can be multiplexed for other modes, such as the I2C clock line I2C2_SCL and other uses. Therefore, to use a specific IO as GPIO, we need to multiplex it. In Linux, the register responsible for multiplexing is IOMUXC_SW_MUX. Additionally, we need to enable the clock for this GPIO, referred to as CCM in Linux. Similar to STM32, we also need to set the IO speed, pull-up/down resistors, drive strength, slew rate (the time required for the IO level to change, such as how long it takes to go 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.
Therefore, if we want to use a specific GPIO group, such as GPIO1_IO03, we first need to enable the clock for GPIO1, then set GPIO1_IO03 to GPIO mode instead of IIC mode. Next, we need to configure GPIO1_IO03’s mode, speed, pull-up/down resistors, slew rate, etc. Finally, we can write 0 or 1 to the DR register of GPIO1_IO03, which is the data register, to control the LED’s on/off state.
2.2 Enabling the Clock
According to the chip manual, we can see that to enable the clock for GPIO1_IO03, we need to configure the CG13 bit of the CCGR1 register.


Furthermore, I learned 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 used to configure it.
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
</group></padname>
For a specific pin or a group of preset pins, there are 8 selectable modes (alternate (ALT) MUX_MODE).

For example, if we want 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
* Finally, set the IO attributes. */
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: Set pin direction, each bit corresponds to a pin, 1-output, 0-input

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

③ GPIOx_PSR: Read 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 the corresponding bit in the CCM_CCGRx register to enable the GPIO module, which is enabled by default.② Set IOMUX to select the pin for GPIO.③ Set the corresponding bit in GPIOx_GDIR to 0, setting the pin as input.④ Read GPIOx_DR or GPIOx_PSR to get 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 IOMUX to select the pin for GPIO.③ Set the corresponding bit in GPIOx_GDIR to 1, setting 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 of the pin from GPIOx_PSR; the value read from GPIOx_DR is only the last set value and does not reflect the actual level of the pin. For example, if there is a hardware fault causing the pin to short to ground, writing a high level to GPIOx_DR will not take effect.
With the above knowledge, 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, write back
-
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, 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>
structure. -
3. Implement the corresponding <span>drv_open/drv_read/drv_write</span>
functions, and fill in the<span>file_operations</span>
structure. -
4. Tell the kernel about the <span>file_operations</span>
structure:<span>register_chrdev</span>
. -
5. Who registers the driver? There must be an entry function: When installing the driver, this entry function will be called. -
6. If there is an entry function, there should also 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 the hardware?
-
By using ioremap to map the physical address of the register to obtain the virtual address for read/write operations.
How does the driver transfer data with the APP?
-
Using <span>copy_to_user</span>
and<span>copy_from_user</span>
functions.
6. Address Mapping
Before writing the driver, we need to understand the MMU (Memory Management Unit). The MMU is responsible for mapping virtual memory to physical memory. 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:
-
① Map virtual space to physical space. -
② Memory protection, setting access permissions for memory, configuring the buffering characteristics of virtual storage space.
We will focus on the first point, which is the mapping from virtual space to physical space. 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, which is the physical memory. Through the MMU, this can be mapped 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 handles the issue of the virtual address range exceeding 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 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 register for I.MX6ULL is<span>0X020E0068</span>
. If the MMU is not enabled, we can write data directly to the address 0X020E0068 to configure the multiplexing function of GPIO1_IO03. Now that the MMU is enabled and memory mapping is set up, we can no longer write directly 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 and virtual memory and requires two 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>
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. The function __arm_ioremap is the one that actually works and has three parameters and one return value, which are 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 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 mapped virtual space’s starting address.
For example, if we want to get 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, while<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 reality, it is mapped by pages (4096 bytes), meaning it is mapped in whole pages. So although we are mapping 4 bytes, we are effectively mapping 4096 bytes.
6.2 iounmap Function
When uninstalling the driver, 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. For example, if we want to cancel the mapping of the address of the<span>IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03</span>
register, we can use the following code:
iounmap(SW_MUX_GPIO1_IO03);
6.3 Use of volatile
① The compiler is smart and can help us with optimizations, for example:
int a;
a = 0; // This line can be optimized away without affecting a's result
a = 1;
② Sometimes the compiler can be overly clever, for example:
int *p = ioremap(xxxx, 4); // GPIO register's address
*p = 0; // Turn on the light, but this line gets optimized away
*p = 1; // Turn off the light
③ To avoid automatic optimization by the compiler in the above case, we need to add volatile to tell it that this is error-prone and should not be optimized:
volatile int *p = ioremap(xxxx, 4); // GPIO register's address
*p = 0; // Turn on the light, this line will not be optimized away
*p = 1; // Turn off the light
7. I/O Memory Access Functions
Here, I/O refers to input/output, and not the GPIO pins discussed when learning microcontrollers. This involves two concepts: I/O Ports and I/O Memory.
When external registers or memory are mapped to I/O 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, 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. However, the Linux kernel does not recommend doing this, but rather recommends using a set of operation functions to read and write to the mapped memory.
What does this mean? In simpler terms, I know that the clock register address for GPIO1_IO03 is 0X020C406C, but I cannot operate it directly.
#define CCM_CCGR1_BASE (0X020C406C)
0X020C406C is its actual physical address, but when the Linux kernel starts, it initializes the MMU, sets up the memory mapping, and after that, the CPU accesses virtual addresses. Therefore, we cannot operate on the actual physical address directly. What should we do? Don’t worry, Linux provides the ioremap memory mapping function. I know the actual physical address, and by using this function, we can 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 IMX6U_CCM_CCGR1 corresponding to 0X020C406C, but we still cannot operate on this virtual address directly. Why? Because after using the ioremap function to map the physical address of the register to the virtual address, we should be able to directly access these addresses through pointers, but the Linux kernel does not recommend doing this and provides a set of read and write functions to operate on the mapped memory instead. So we have to follow its recommendations. For example, if I want to manipulate specific bits of the next four bytes at this address, I need to do it as follows: first 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. The parameter addr is the address of the memory to read, 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. The parameter 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 GPIO pin
* 1. First, enable the clock for this GPIO
* 2. Then multiplex this GPIO for GPIO functionality
* 3. Set 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 */
/* Physical address of the registers */
#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 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 sets the direction of GPIO1_IO03, input or output
/* Mapped virtual address pointers for the registers */
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 the device
* @param - inode : Passed to the driver inode
* @param - filp : Device file, the file structure has a member variable called private_data
* Generally, during open, private_data points to the device structure.
* @return : 0 on success; other on failure
*/
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* @description : Read data from 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's starting 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 device
* @param - filp : Device file, indicates 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's starting 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 the LED */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* Turn off the LED */
}
return 0;
}
/*
* @description : Close/release device
* @param - filp : The 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 GPIO1_IO03's multiplexing function to GPIO1_IO03, finally set 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 slew 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. Turn off LED by default */
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 above explanation, the code is quite simple and does not require further elaboration; just follow those 7 steps to operate.
8.2 Writing a 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 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]); /* The 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 quite simple and does not require further elaboration.
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; please 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>led.c</span>
file will be compiled into the<span>led.ko</span>
module. -
Line 8, the specific compile command, where modules indicates to compile modules, -C indicates to change the current working directory to the specified directory, which is the KERNELDIR directory. M indicates the module source directory, so when you add M=dir to the <span>make modules</span>
command, the program will automatically read the module source from the specified dir directory and compile it into<span>.ko</span>
files. -
Line 9, use the cross-compilation toolchain to compile <span>ledApp.c</span>
into an executable file that can run on the ARM board.
Once the Makefile is written, enter<span>make</span>
to compile the driver module, and the compilation process is shown in the figure below.

9. Running Tests
9.1 Uploading Programs to the Development Board
After the development board starts, we can copy the corresponding files to the development board by mounting the Ubuntu directory through NFS. In simple terms, it means accessing files on the Ubuntu virtual machine directly from the development board over the network, treating it as if it were local files.
Since my code is stored in<span>/home/zhiguoxin/myproject/alientek_drv_development_source</span>
, we will share this directory as an NFS shared folder.


The Ubuntu IP is 192.168.10.100, usually mounted in the development board’s mnt directory, which is specifically used 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’s<span>/home/zhiguoxin/myproject/alientek_drv_development_source</span>
directory. This way, we can modify files in 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, so I can also modify files directly in<span>windows</span>
, and then Ubuntu and the development board will synchronize files.

9.2 Loading the Driver Module
The driver module<span>led.ko</span>
and the executable file<span>ledApp</span>
are both ready. Next, we will run the test. Here, I mount the server’s project folder to the mnt directory on the ARM board, and enter the<span>/mnt/02_led</span>
directory to 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 complete the operations on the specific device. Enter the following command to create the device node file<span>/dev/led</span>
:
mknod /dev/led c 200 0
Where<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 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 file<span>/dev/led</span>
will exist, you can use<span>ls /dev/led-l</span>
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 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 you no longer use a certain device, you can unload its driver. For example, enter the following command to unload the<span>hello_drv</span>
device:
rmmod led.ko
After unloading, use the<span>lsmod</span>
command to see if the<span>led</span>
module still exists:

It can be seen that at this time the system has no modules, and the<span>led</span>
module does not exist, indicating that the module has been successfully unloaded. Moreover, the<span>led</span>
device is also no longer in the system.
Thus, the entire driver for the<span>led</span>
device has been validated, and the driver works correctly. Future character device driver experiments can basically use this as a template for writing.
This is a new book by Yi Kou Jun, thank you for your support!
end
Yi Kou Linux
Follow, reply【1024】to receive a wealth of Linux materials
Collection of Wonderful Articles
Recommended Articles