目标: 编写一个最基本的驱动,加载时点亮led,卸载时熄灭led。
硬件:microchip推出的SAMA5D3 X PLAINED开发板
知识点:
1. 驱动的框架
2. 使用内存映射
3. 编译驱动
4. 加载驱动
驱动框架
这里就不赘述什么是驱动,驱动和应用的关系了。
驱动需要在一台安装了linux的电脑内编写(你用来编译linux镜像的那台电脑),必备arm-gcc交叉编译器,以及linux源码项目(就是那个make之后会编译出一个linux镜像的项目)。
首先,我们先创建一个.c文件,这里就起名为led_drv.c。这里我们先瞄一眼最后的完整代码
#include<linux/module.h> //驱动模块初始化相关
#include<asm/io.h> //进行底层io操作相关
#define PE_OER ((uint32_t *)0xFFFFFA10)
#define PE_ODR ((uint32_t *)0xFFFFFA14)
static void __iomem *ADDR_ONECE; //给函数临时存储虚拟地址使用
static void write_addr( uint32_t *p , uint32_t value )
{
ADDR_ONECE = ioremap( (resource_size_t) p, 4);
writel(value,ADDR_ONECE);
iounmap( ADDR_ONECE );
}
static int __init led_init(void)
{
write_addr(PE_ODR,1<<24);
printk("加载驱动\r\n");
return 0;
}
static void __exit led_exit(void)
{
write_addr(PE_OER,1<<24);
printk("驱动卸载\r\n");
}
module_init(led_init); //登记模块加载时要执行的函数
module_exit(led_exit); //登记模块卸载时要执行的函数
MODULE_LICENSE("GPL");
1.头文件
驱动的默认头文件路径,是linux源码项目内的include文件夹下。后面你会经常打开这个路径,查看头文件具体内容的。
本篇教程实现的功能较为简单,只需要使用两个头文件,一个是驱动必备的基础头文件,一个是操作寄存器所需要使用到的。
#include<linux/module.h> //驱动模块初始化相关
#include<asm/io.h> //进行底层io操作相关
2. init及exit函数
驱动里没有main函数,取而代之的是一条会在驱动被加载时被调出来运行的入口函数,一条在卸载时运行的出口函数
这两条函数不限定函数名,只要自己定义后使用module_init和module_exit函数登记,就会在加载和卸载驱动时调出他俩来执行,如下:
接下来我们会把点亮和熄灭led的程序写进这俩函数内
static int __init led_init(void)
{
}
static void __exit led_exit(void)
{
}
module_init(led_init); //登记模块加载时要执行的函数
module_exit(led_exit); //登记模块卸载时要执行的函数
用__init修饰函数,则本函数所占用的内存会在初始化结束后被释放掉。
3. 添加开源协议声明
在文件最后添加以下函数,声明使用GPL开源许可。
MODULE_LICENSE("GPL");
内存映射
控制led,只需要通过给寄存器赋值,控制引脚输出高电平即可。
由于mmu的存在(Memory Manage Unit 内存管理单元,可以实现虚拟内存,内存访问权限管理等功能,必须带有mmu才能装linux系统),如果像写单片机时那样直接对寄存器赋值,会失败,需要让mmu做内存映射。
1. 寻找控制io电平的寄存器
阅读芯片手册,学习gpio的控制流程及相关寄存器功能。这里的目的是为了展示怎么使用内存映射,实际应用中linux为很多常见操作都提供了一套标准操作流程,比如配置引脚功能,控制led,管理输入等,后续会再学习。
最终查询到我所使用的ATSAMA5D36芯片有如下两个寄存器,给OER寄存器赋1,则会让对应引脚输出高电平,给ODR寄存器赋1,则会让引脚输出低电平。
#define PE_OER ((uint32_t *)0xFFFFFA10)
#define PE_ODR ((uint32_t *)0xFFFFFA14)
2. 内存映射的API
定义于arch\arm\include\asm\io.h
地址映射使用ioremap函数,需要传入需要传入两个参数,1是要转换的物理地址起始,2要转换从该地址起始的多少字节。会返回转换后的虚拟地址起始位置
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
释放虚拟地址使用iounmap函数,只要传入要释放的虚拟地址即可。
void iounmap(volatile void __iomem *iomem_cookie);
读取内存总共有3条api,只要传入虚拟地址起始,就会返回其存储的值。bwl分别对应将该地址视为8 16 32位变量进行读取。
#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c) ({ u16 __v = readw_relaxed(c); __iormb(); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })
写内存也有3条api,传入两个参数,v是要修改的值,c是虚拟地址。bwl分别对应将该地址视为8 16 32位变量进行修改
#define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })
3. 添加led控制代码
先定义一个用于修改内存的函数,传入参数是地址和要赋的值。第一行是获取映射后的虚拟地址,第二行进行赋值,第三行释放该虚拟地址。
static void __iomem *ADDR_ONECE; //给函数临时存储虚拟地址使用
static void write_addr( uint32_t *p , uint32_t value )
{
ADDR_ONECE = ioremap( (resource_size_t) p, 4);
writel(value,ADDR_ONECE);
iounmap( ADDR_ONECE );
}
然后往led_init内添加以下代码,调用上面的函数,将odr寄存器赋值为1<<24,即可点亮led
write_addr(PE_ODR,1<<24);
往led_exit内添加以下代码,将oer寄存器赋值为1<<24,就会熄灭led
write_addr(PE_OER,1<<24);
完整驱动代码
完整代码如下,额外加了两句printk用于输出提示信息。运行于内核中的驱动不能使用printf函数,需要使用printk。
#include<linux/module.h> //驱动模块初始化相关
#include<asm/io.h> //进行底层io操作相关
#define PE_OER ((uint32_t *)0xFFFFFA10)
#define PE_ODR ((uint32_t *)0xFFFFFA14)
static void __iomem *ADDR_ONECE; //给函数临时存储虚拟地址使用
static void write_addr( uint32_t *p , uint32_t value )
{
ADDR_ONECE = ioremap( (resource_size_t) p, 4);
writel(value,ADDR_ONECE);
iounmap( ADDR_ONECE );
}
static int __init led_init(void)
{
write_addr(PE_ODR,1<<24);
printk("加载驱动\r\n");
return 0;
}
static void __exit led_exit(void)
{
write_addr(PE_OER,1<<24);
printk("驱动卸载\r\n");
}
module_init(led_init); //登记模块加载时要执行的函数
module_exit(led_exit); //登记模块卸载时要执行的函数
MODULE_LICENSE("GPL");
编译
1. 编写makefile
首先创建一个文件,文件命名为Makefile,在里面添加修改以下内容:
第一行的含义是将led_drv.c编译后的.o文件添加进行生成,需要修改为你的驱动程序的名称
第二行是定义了linux源码项目在电脑中的路径,需要按实际情况修改
obj-m := led_drv.o
KERNEL_PATH := /home/cb/work/linux-at91/
PWD := $(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
2. 编译
首先命令行切换到我们刚刚编写的drv.c所在的目录,里面就如下两个文件
我们是在电脑上编译的,需要运行以下命令,声明使用的交叉编译器
export CROSS_COMPILE=arm-linux-gnueabi-
然后运行以下命令,开始编译
make ARCH=arm
编译结束,会得到一个和驱动程序同名,但后缀为.ko的文件,这就是我们需要的目标文件。如这里是led_drv.ko
测试
0. 将驱动存进linux开发板
我采用的是在电脑端搭建一个nfs服务器,然后在linux开发板上使用命令,将nfs服务器挂载到开发板的指定文件夹内,实现共享文件夹。
1. 加载驱动
在板子上使用insmod 命令来加载驱动
insmod led_drv.ko
板子上的红灯即是我们点亮的,屏幕上也出现了我们用printk输出的语句
2. 查看已加载的驱动
使用以下命令
lsmod
3. 卸载驱动
使用rmmod 命令来加载驱动
rmmod led_drv
板子上的红灯d3熄灭了,屏幕上也出现了我们用printk输出的语句
本篇教程到此结束,下一篇会介绍驱动接口函数的编写方法,并编写一个运行于linux下的程序,透过驱动来控制led闪烁
请点击下一篇教程继续学习: linux驱动编写--2--应用程序控制led闪烁