Linux
下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED
灯驱动最终也是对 I.MX6ULL
的 IO
口进行配置,与裸机实验不同的是,在 Linux
下编写驱动要符合 Linux
的驱动框架。
一、地址映射
Linux
内核启动的时候会初始化 MMU
,设置好内存映射,设置好以后 CPU
访问的都是虚拟地址。
在编写驱动之前,我们需要先简单了解一下 MMU
这个神器,MMU
全称叫做 Memory Manage Unit
,也就是内存管理单元。在老版本的 Linux
中要求处理器必须有 MMU
,但是现在 Linux
内核已经支持无MMU的处理器了。MMU
主要完成的功能如下:
1、完成虚拟空间到物理空间的映射。
2、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap
和 iounmap
。
1、ioremap
ioremap
函 数用于获取指定物理地址空间对应的虚拟地 址空 间,定 义在 arch/arm/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));
}
2、iounmap
卸载驱动的时候需要使用 iounmap
函数释放掉 ioremap
函数所做的映射,iounmap
函数原型如下:
void iounmap (volatile void __iomem *addr)
二、I/O内存访问函数
这里说的 I/O
是输入/输出的意思。这里涉及到两个概念:I/O
端口和 I/O
内存。
当外部寄存器或内存映射到 IO
空间时,称为 I/O
端口。
当外部寄存器或内存映射到内存空间时,称为 I/O
内存。
但是对于 ARM
来说没有 I/O
空间这个概念,因此 ARM
体系下只有 I/O
内存(可以直接理解为内存)。使用 ioremap
函数将寄存器的物 理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux
内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
1、读操作函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb
、readw
和 readl
这三个函数分别对应 8bit
、16bit
和 32bit
读操作,参数 addr
就是要读取写内存地址,返回值就是读取到的数据。
2、写操作函数
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb
、writew
和 writel
这三个函数分别对应 8bit
、16bit
和 32bit
写操作,参数 value
是要写入的数值,addr
是要写入的地址。
三、硬件原理图分析
https://blog.csdn.net/OnlyLove_/article/details/121757151
四、实验程序编写
1、工程配置
创建工程文件 c_cpp_properties.json
,并添加头文件路径。
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
"/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
"/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu17",
"cppStandard": "gnu++14",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
2、搭建驱动框架
1、led.c
#include "linux/init.h"
#include "linux/module.h"
#include "linux/fs.h"
#include "linux/types.h"
// struct inode 声明在 linux/fs.h 中
// struct file 声明在 linux/fs.h 中
int led_open (struct inode *i, struct file *f)
{
// printk 声明在 linux/printk.h 中
printk("led open!\r\n");
return 0;
}
int led_release (struct inode *i, struct file *f)
{
printk("led release!\r\n");
return 0;
}
// ssize_t 定义在 linux/types.h 中
// __user 定义在 linux/compiler.h 中
// size_t 定义在 linux/types.h 中
// loff_t 定义在 linux/types.h 中
ssize_t led_read (struct file *f, char __user *b, size_t c, loff_t * l)
{
printk("led read!\r\n");
return 0;
}
ssize_t led_write (struct file *f, const char __user *b, size_t c, loff_t *l)
{
printk("led write!\r\n");
return 0;
}
// 声明在 linux/fs.h 头文件中
static struct file_operations fops = {
.open = led_open,
.release = led_release,
.read = led_read,
.write = led_write,
};
/* 驱动入口函数 */
static int __init led_init(void)
{
/* 入口函数具体内容 */
int retvalue = 0;
// 声明在 linux/fs.h 头文件中
retvalue = register_chrdev(200,"chrdev",&fops);
if(retvalue < 0){
/* 字符设备注册失败 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit led_exit(void)
{
/* 出口函数具体内容 */
// 声明在 linux/fs.h 头文件中
unregister_chrdev(200,"chrdev");
}
// 声明在 linux/init.h 头文件中
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(led_init);
module_exit(led_exit);
// 声明在 linux/module.h 头文件中
MODULE_LICENSE("GPL");
2、makefile
KERNELDIR := /home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := led.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
3、LED 初始化
1、映射相关寄存器地址
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
/* 映射后的寄存器虚拟地址指针 */
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;
以上通过 define
定义物理地址,通过变量保存虚拟地址。物理地址和虚拟地址映射通过 open
函数完成,代码如下:
int led_open (struct inode *i, struct file *f)
{
// printk 声明在 linux/printk.h 中
printk("led open!\r\n");
/* 寄存器地址映射 */
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);
return 0;
}
取消地址映射通过 close
函数完成,代码如下:
int led_release (struct inode *i, struct file *f)
{
printk("led release!\r\n");
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
return 0;
}
2、初始化
初始化包括配置时钟、引脚复用、引脚电平。
初始化代码在 open
函数中完成,添加初始化程序后代码如下:
int led_open (struct inode *i, struct file *f)
{
u32 val = 0;
// printk 声明在 linux/printk.h 中
printk("led open!\r\n");
/* 1、寄存器地址映射 */
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、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清楚以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
/* 3、设置GPIO1_IO03的复用功能,将其复用为
* GPIO1_IO03,最后设置IO属性。
*/
writel(5, SW_MUX_GPIO1_IO03);
/*寄存器SW_PAD_GPIO1_IO03设置IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 110 R0/6驱动能力
*bit [0]: 0 低转换率
*/
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 4、设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);
/* 5、默认关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
return 0;
}
3、控制 LED 灭亮
控制灯的灭亮,由 write
函数完成。
1、从用户空间拷贝数据
extern inline long
copy_from_user(void *to, const void __user *from, long n)
{
return __copy_tofrom_user(to, (__force void *)from, n, from);
}
/*
* to:内核空间数据存储缓存
* from:指向用户空间数据缓存区
* n:拷贝数据长度
*/
2、LED 亮灭控制
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
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);
}
}
3、write 代码
/*
* @description : 向设备写数据
* @param - f : 设备文件,表示打开的文件描述符
* @param - b : 要写给设备写入的数据
* @param - c : 要写入的数据长度
* @param - l : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
ssize_t led_write (struct file *f, const char __user *b, size_t c, loff_t *l)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
printk("led write!\r\n");
retvalue = copy_from_user(databuf, b, c);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态值 */
if(ledstat == LEDON) {
led_switch(LEDON); /* 打开LED灯 */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* 关闭LED灯 */
}
return 0;
}
4、驱动代码
#include "linux/init.h"
#include "linux/module.h"
#include "linux/fs.h"
#include "linux/types.h"
#include "asm/io.h"
#include "asm/uaccess.h"
#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
/* 映射后的寄存器虚拟地址指针 */
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打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
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);
}
}
// struct inode 声明在 linux/fs.h 中
// struct file 声明在 linux/fs.h 中
int led_open (struct inode *i, struct file *f)
{
u32 val = 0;
// printk 声明在 linux/printk.h 中
printk("led open!\r\n");
/* 1、寄存器地址映射 */
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、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清楚以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
/* 3、设置GPIO1_IO03的复用功能,将其复用为
* GPIO1_IO03,最后设置IO属性。
*/
writel(5, SW_MUX_GPIO1_IO03);
/*寄存器SW_PAD_GPIO1_IO03设置IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 110 R0/6驱动能力
*bit [0]: 0 低转换率
*/
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 4、设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);
/* 5、默认关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
return 0;
}
int led_release (struct inode *i, struct file *f)
{
printk("led release!\r\n");
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
return 0;
}
// ssize_t 定义在 linux/types.h 中
// __user 定义在 linux/compiler.h 中
// size_t 定义在 linux/types.h 中
// loff_t 定义在 linux/types.h 中
ssize_t led_read (struct file *f, char __user *b, size_t c, loff_t * l)
{
printk("led read!\r\n");
return 0;
}
/*
* @description : 向设备写数据
* @param - f : 设备文件,表示打开的文件描述符
* @param - b : 要写给设备写入的数据
* @param - c : 要写入的数据长度
* @param - l : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
ssize_t led_write (struct file *f, const char __user *b, size_t c, loff_t *l)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
printk("led write!\r\n");
retvalue = copy_from_user(databuf, b, c);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态值 */
if(ledstat == LEDON) {
led_switch(LEDON); /* 打开LED灯 */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* 关闭LED灯 */
}
return 0;
}
// 声明在 linux/fs.h 头文件中
static struct file_operations fops = {
.open = led_open,
.release = led_release,
.read = led_read,
.write = led_write,
};
/* 驱动入口函数 */
static int __init led_init(void)
{
/* 入口函数具体内容 */
int retvalue = 0;
// 声明在 linux/fs.h 头文件中
retvalue = register_chrdev(LED_MAJOR,LED_NAME,&fops);
if(retvalue < 0){
/* 字符设备注册失败 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit led_exit(void)
{
/* 出口函数具体内容 */
// 声明在 linux/fs.h 头文件中
unregister_chrdev(LED_MAJOR,LED_NAME);
}
// 声明在 linux/init.h 头文件中
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(led_init);
module_exit(led_exit);
// 声明在 linux/module.h 头文件中
MODULE_LICENSE("GPL");
5、应用程序代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "stdio.h"
int main(int argc, char *argv[])
{
int fd = 0, retvalue = 0;
char writebuf[1] = "";
fd = open(argv[1],O_RDWR);
if(fd < 0){
printf("Can't open file %s\r\n", argv[1]);
return -1;
}
writebuf[0] = atoi(argv[2]);
// 打开 led
write(fd, writebuf, 1);
retvalue = close(fd);
if(retvalue < 0){
printf("Can't close file %s\r\n", argv[1]);
return -1;
}
return 0;
}
编译应用程序指令如下:
arm-linux-gnueabihf-gcc led_app.c -o led_app
6、验证
1、加载驱动
加载驱动:
/ # ls
bin etc led_app linuxrc proc sbin tmp
dev led.ko lib mnt root sys usr
/ # insmod led.ko
/ #
/ # lsmod
Module Size Used by Tainted: G
led 2274 0
加载驱动也可以使用 modprobe
命令。
查看驱动加载情况:
/ # cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
10 misc
13 input
29 fb
81 video4linux
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
200 led
207 ttymxc
216 rfcomm
226 drm
249 ttyLP
250 iio
251 watchdog
252 ptp
253 pps
254 rtc
Block devices:
1 ramdisk
259 blkext
7 loop
8 sd
31 mtdblock
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/ #
2、创建设备文件
命令如下所示:
mknod /dev/led c 200 0
查看创建结果:
/ # ls /dev/
autofs ram11 tty36
bus ram12 tty37
console ram13 tty38
cpu_dma_latency ram14 tty39
dri ram15 tty4
fb0 ram2 tty40
full ram3 tty41
fuse ram4 tty42
hwrng ram5 tty43
i2c-0 ram6 tty44
i2c-1 ram7 tty45
input ram8 tty46
kmsg ram9 tty47
led random tty48
loop-control rtc0 tty49
loop0 snd tty5
loop1 tty tty50
loop2 tty0 tty51
loop3 tty1 tty52
loop4 tty10 tty53
loop5 tty11 tty54
loop6 tty12 tty55
loop7 tty13 tty56
mem tty14 tty57
memory_bandwidth tty15 tty58
mmcblk0 tty16 tty59
mmcblk0p1 tty17 tty6
mmcblk1 tty18 tty60
mmcblk1boot0 tty19 tty61
mmcblk1boot1 tty2 tty62
mmcblk1p1 tty20 tty63
mmcblk1p2 tty21 tty7
mmcblk1rpmb tty22 tty8
mxc_asrc tty23 tty9
network_latency tty24 ttymxc0
network_throughput tty25 ttymxc1
null tty26 ubi_ctrl
pps0 tty27 urandom
pps1 tty28 vcs
ptmx tty29 vcs1
ptp0 tty3 vcsa
ptp1 tty30 vcsa1
pts tty31 video0
pxp_device tty32 watchdog
ram0 tty33 watchdog0
ram1 tty34 zero
ram10 tty35 ⚌A-⚌
/ #
通过以上日志可以确定驱动文件加载成功。
3、运行应用程序
点亮 LED
,指令如下:
./led_app /dev/led 1
熄灭 LED
,指令如下:
./led_app /dev/led 1
4、测试
执行 app
程序日志如下:
/ # ./led_app /dev/led 1
led open!
led write!
led release!
/ # ./led_app /dev/led 0
led open!
led write!
led release!
/ #
通过观察开发板上 LED
灯状态,可以确定 LED
可以正常打开和关闭。