linux驱动编写--2--应用程序控制led闪烁

 本系列教程的上一篇:  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件事

  1. 向系统申请一个设备号,
  2. 将设备号和文件操作函数绑定
  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中执行,结果如下

        

        

本篇教程结束,欢迎阅读本系列的下一篇:    编写中,先占位

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值