本系列教程的上一篇: linux驱动编写--1--点亮led
目标:编写一个驱动程序,实现上一篇没写的 “接口”。并编写一个测试程序,透过驱动来控制led闪烁。
硬件:microchip推出的SAMA5D3 X PLAINED开发板
首先,我们复制上一篇文章的程序,在其基础上进行修改。
驱动接口
Linux下将设备分为3类,字符设备,块设备,网络设备,对应不同的驱动编写方式。块设备是以数据块作为传输基本单位的,一般是磁盘,Flash等存储类设备。网络设备顾名思义就是wifi网卡相关的设备。字符设备是数量最多的设备,基本上不是内存,也不搞网络的设备都被分类到这。
我们今天要控制的led,也属于字符设备。这一类设备的控制流程是,驱动需要在linux的路径/dev下注册一个设备节点(一个文件)。应用程序会通过文件操作函数,open打开该节点,write往里写入,read读取数据。驱动里需要将对led等设备的操作,封装成文件操作函数的形式。
Linux下一切都是文件。
首先我们打开 linux/fs.h这个头文件,在里面找到名为file_operations这个结构体,这些函数指针就是我们需要实现的接口函数标准。没有规定必须实现哪些,只要实现你想用的就行。
1增加头文件
在代码中添加以下头文件, 注意,这里是在上一篇的程序文件中进行修改。
#include <linux/cdev.h>
#include<linux/fs.h>
2创建write函数
我们先根据file_operations结构体中指向write函数的指针,来定义一个自己的write函数 。
后面我们将其使用系统api进行登记,然后在应用程序使用write函数往设备写入数据时,系统便会调用这条函数。
static ssize_t dev_write (struct file *filp, const char __user *user_buf, size_t count,loff_t *offt)
{
return 0;
}
3 添加控制led亮灭的程序
有没有注意到,驱动中write函数的形参,跟C语言标准库的write函数一模一样。我们在应用程序中,会使用write(file, "hello", 5);来写入文件,而这些参数会被一一传递过来,没有中间商会修改其内容。
我们往write函数内添加以下代码:
static char buf[10]; //定义一个缓冲区数组
int retvalue = 0 ; //存储函数返回值
retvalue = copy_from_user(buf, user_buf, count); //将数据从用户空间复制到内核空间
if(buf[0] == '1')
write_addr(PE_ODR,1<<24);
if(buf[0] == '0')
write_addr(PE_OER,1<<24);
注意使用copy_from_user。应用程序所处的用户内存空间有分页设计,传过来的指针,如果在内核空间内访问会因为所处的页不一样而报错。
我们这里是简单粗暴的判断传过来的第一位数据,后面应用程序只要使用write(dev_file, "1", 1);就可以点亮led。
write_addr()是上一篇文章内就写好的,用于操作底层寄存器的函数,这里给寄存器赋值以控制io输出。
4 登记所有操作函数
定义一个file_operations结构体,指向我们定义好的对应操作函数。后面我们会将这个结构体用api向系统登记。
static struct file_operations led_operations ={
.owner = THIS_MODULE ,
.write = dev_write
};
驱动初始化
上一篇教程的初始化部分,我们啥都没做,这里我们补上未做的那些事。
驱动初始化时,需要做3件事
- 向系统申请一个设备号,
- 将设备号和文件操作函数绑定
- 创建一个设备节点并与设备号绑定(应用程序要操作的那个文件)
0 添加存储信息结构体
首先我们在代码前面添加以下内容:声明两个宏定义,一个是要申请设备号的个数,第二个是要注册的设备名字。然后定义一个结构体,用于后面存储一些申请的信息。
#define CHR_CNT 1 /* 设备号个数 */
#define CHR_NAME "LED_ORANGE2C" /* 设备名字 */
/* 字符串设备信息记录结构体 */
struct {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
}ledchr;
1 申请设备号
主要使用下面这条系统api,定义于fs\char_dev.c
我们在初始化函数led_init内添加以下代码(此处在上一篇教程编写的代码文件下修改,不赘述相同知识点)
/* 1、申请设备号 */
alloc_chrdev_region(&ledchr.devid, 0, CHR_CNT, CHR_NAME); /* 申请设备号 */
ledchr.major = MAJOR(ledchr.devid); /* 获取分配号的主设备号 */
ledchr.minor = MINOR(ledchr.devid); /* 获取分配号的次设备号 */
printk("主设备号=%d\n次设备号minor=%d\r\n",ledchr.major, ledchr.minor);
在加载驱动后,就会申请设备号,并将其显示到屏幕上
2 创建字符设备
这一步包括两条api:1使用file_operations结构体创建一个字符设备,2将该字符设备与设备号绑定。这俩的定义都位于fs\char_dev.c
我们添加以下代码
/* 2、创建字符设备 */
ledchr.cdev.owner = THIS_MODULE;
cdev_init(&ledchr.cdev, &led_operations); //创建一个cdev
cdev_add(&ledchr.cdev, ledchr.devid, CHR_CNT); //将cdev与设备号关联
3 创建设备节点
这一步使用两条系统api:1传入设备名称创建一个设备类,2使用这个类和设备号向系统申请创建一个设备节点
这里的类不是指c++的类,而是linux自创的一种东西。本函数不仅仅是返回一个创建好的设备类,还会在内核中注册它。本函数定义位于include\linux\device\class.h
以下api用于创建设备节点,定义位于drivers\base\core.c
我们添加以下代码,至此,初始化部分结束
/* 3、创建设备节点 */
ledchr.class = class_create(THIS_MODULE, CHR_NAME);
ledchr.device = device_create(ledchr.class, NULL, ledchr.devid, NULL, CHR_NAME);
模块卸载
初始化写完了,卸载部分也得补充一下。
1 注销设备号
使用这条api,定义位于fs\char_dev.c
在卸载函数 led_exit中添加如下代码
unregister_chrdev_region(ledchr.devid, CHR_CNT); //注销设备号
2 注销字符设备
使用这条api,定义位于fs\char_dev.c
在卸载函数 led_exit中添加如下代码:
cdev_del(&ledchr.cdev); // 删除cdev
3 删除设备类与设备节点
删除设备类的api,定义于drivers\base\class.c
删除设备节点的api,定义于drivers\base\core.c
在卸载函数 led_exit中添加如下代码
device_destroy(ledchr.class, ledchr.devid); //注销设备节点
class_destroy(ledchr.class); //删除class
至此,驱动部分已经编写完毕了,我们就用上一篇所编写的makefile进行编译生成.ko文件
以上步骤结束后的完整代码
/*
本文件的功能,编写一个驱动,并提供操作接口write,写1点亮led,写0熄灭
*/
#include<linux/module.h> //驱动模块初始化相关
#include<asm/io.h> //进行底层io操作相关
#include <linux/cdev.h>
#include<linux/fs.h>
#define CHR_CNT 1 /* 设备号个数 */
#define CHR_NAME "LED_ORANGE2C" /* 设备名字 */
/*底层寄存器*/
#define PE_OER ((uint32_t *)0xFFFFFA10)
#define PE_ODR ((uint32_t *)0xFFFFFA14)
/* 字符串设备信息记录结构体 */
struct {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
}ledchr;
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 ssize_t dev_write (struct file *filp, const char __user *user_buf, size_t count, loff_t *offt)
{
static char buf[10]; //定义一个缓冲区数组
int retvalue = 0 ; //存储函数返回值
retvalue = copy_from_user(buf, user_buf, count); //将数据从用户空间复制到内核空间
if(buf[0] == '1')
write_addr(PE_ODR,1<<24);
if(buf[0] == '0')
write_addr(PE_OER,1<<24);
return 0;
}
static struct file_operations led_operations ={
.owner = THIS_MODULE ,
.write = dev_write
};
static int __init led_init(void)
{
/* 1、申请设备号 */
alloc_chrdev_region(&ledchr.devid, 0, CHR_CNT, CHR_NAME); /* 申请设备号 */
ledchr.major = MAJOR(ledchr.devid); /* 获取分配号的主设备号 */
ledchr.minor = MINOR(ledchr.devid); /* 获取分配号的次设备号 */
printk("主设备号=%d\n次设备号minor=%d\r\n",ledchr.major, ledchr.minor);
/* 2、创建字符设备 */
ledchr.cdev.owner = THIS_MODULE;
cdev_init(&ledchr.cdev, &led_operations); //创建一个cdev
cdev_add(&ledchr.cdev, ledchr.devid, CHR_CNT); //将cdev与设备号关联
/* 3、创建设备节点 */
ledchr.class = class_create(THIS_MODULE, CHR_NAME);
ledchr.device = device_create(ledchr.class, NULL, ledchr.devid, NULL, CHR_NAME);
write_addr(PE_ODR,1<<24);
printk("加载驱动\r\n");
return 0;
}
static void __exit led_exit(void)
{
unregister_chrdev_region(ledchr.devid, CHR_CNT); //注销设备号
cdev_del(&ledchr.cdev); // 删除cdev
device_destroy(ledchr.class, ledchr.devid); //注销设备节点
class_destroy(ledchr.class); //删除class
write_addr(PE_OER,1<<24);
printk("驱动卸载\r\n");
}
module_init(led_init); //登记模块加载时要执行的函数
module_exit(led_exit); //登记模块卸载时要执行的函数
MODULE_LICENSE("GPL");
查看驱动信息
首先我们使用insmod命令加载驱动,显示信息如下,我们申请到的主设备号是247
我们使用以下命令,查看系统加载的所有设备
cat /proc/devices
可以看到设备号247是我们申请的LED_ORANGE2C
接下来我们切换系统路径至/dev
可以看到第一个文件就是我们刚刚驱动所申请的设备节点,所有设备都会在这创建一个节点,这些不是真的文件,对他们进行open write read等操作时,会被系统转换为调用驱动中的对应函数。我们等下会编写一个测试程序,打开这个文件,并用write往里写东西。
测试
1 编写测试程序
程序如下,主要思路就是打开/dev路径下的设备节点,往里写数据。我们刚刚在驱动内编写了if判断,如果传入数据的第一个字符是’1’就点亮led,如果是’0’就熄灭。
使用sleep函数作为延时,这样就完成了一个控制led闪烁的程序
#include <stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <unistd.h>
char *devname = "/dev/LED_ORANGE2C"; //设备节点
int main()
{
int fd;
fd = open( devname, O_RDWR); //以可读写模式打开设备
if(fd < 0){
printf("打开错误\r\n");
return 0;
}
for( char i=0; i<3; i++ ) //控制led闪烁
{
write(fd, "1", 1);
printf("点亮\r\n");
sleep(5);
write(fd, "0", 1);
printf("熄灭\r\n");
sleep(5);
}
close(fd); //打开文件后要关闭文件,(虽然我们没写对应的close函数
return 0;
}
2 编译测试程序
在电脑上使用以下命令进行编译,含义是使用gcc交叉编译器,将test.c 编译为可执行文件test。 最后加上-static表示将所用的库都编译进去而不是动态链接,防止因为嵌入式linux中缺少对应库而运行出错。
arm-linux-gnueabi-gcc test.c -o test -static
3 运行测试程序
我们把编译后的可执行文件传输到嵌入式linux中,并在嵌入式linux中执行,结果如下
本篇教程结束,欢迎阅读本系列的下一篇: 编写中,先占位