Linux字符设备驱动之LED驱动

注:本文的参考文档《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.3.pdf》,只用于学习记录。

1. 实验环境

硬件平台:Jz2440 开发板
linux内核:linux-4.15

2. Linux 下 LED 灯驱动原理

    Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以LED驱动最终也要落实对IO相关寄存器的配置,与裸机不同的是,Linux下编写驱动需要符合Linux的驱动框架。

2.1 查看原理图

    对LED进行操作我们需要知道LED接到SOC的哪一个IO上,是高电平点亮LED还是低电平点亮LED,JZ2440开发板的LED部分的原理图下图所示:
在这里插入图片描述 在这里插入图片描述
从LED的原理图可知:
(1) LED D10、D11、D12 IO输出低电平点亮、输出高电平熄灭;
(2) LED D10、D11、D12 连接SOC的IO分别是GPF4、GPF5、GPF6;

2.2 查看SOC芯片手册配置IO寄存器

S3C2440芯片手册GPF的相关寄存器信息如下:
在这里插入图片描述
从上图可知:
(1) 我们需要配置的寄存器有GPFCON、GPFDAT
(2) GPFCON、GPFDAT寄存器对应的物理地址分别是0x56000050、0x56000054
(3) 把GPF4~GPF6配置为输出,需要把GPFCON寄存器的bit[8:13] = 010101
(4) 可以通过对GPFDAT的 bit[4:6] 写值来控制GPF4~GPF6的输出状态;

2.3 地址映射

老版本Linux中要求处理器有MMU,但是现在Linux 内核已经支持无 MMU 的处理器了,MMU的主要功能如下:
(1) 完成虚拟空间到物理空间的映射;
(2) 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
注:Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟 地 址 。
在linux系统中,要完成物理内存和虚拟内存之间的转换,设计到两个函数:ioremapiounmap

2.3.1 ioremap函数

ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h 文件中,定义如下:

void __iomem *ioremap(resource_size_t res_cookie, size_t size);
#define ioremap ioremap

/*ioremap的实现在arch/arm/mm/ioremap.c*/
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{
    
    
	return arch_ioremap_caller(res_cookie, size, MT_DEVICE,
				   __builtin_return_address(0));
}

ioremap 是一个宏,也是一个函数名,它由两参数 有两个参数和一个返回值,它们的含义如下:
res_cookie:要映射给的物理起始地址,在include/linux/types.h 有定义:typedef phys_addr_t resource_size_t;
size:要映射的内存空间大小;
返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址,对于32位的处理器,映射的内存长度为 4字节;

2.3.2 iounmap函数

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射, iounmap 函数原型如下:(也在arch/arm/include/asm/io.h 文件)

void iounmap(volatile void __iomem *iomem_cookie);
#define iounmap iounmap

iounmap 只有一个参数 iomem_cookie,此参数就是要取消映射的虚拟地址空间首地址。

2.4 I/O 内存访问函数

    这里说的 I/O 是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO 引脚。这里涉及到两个概念: I/O 端口和 I/O 内存当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口当外部寄存器或内存映射到内存空间时,称为 I/O 内存。 使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

2.4.1 读操作函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

readbreadwreadl 这三个函数分别对应 8bit16bit32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。

2.4.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)

writebwritewwritel 这三个函数分别对应 8bit16bit32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。

3. LED驱动程序编写

(1) 编写led_drv.c 驱动程序,代码如下:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/uaccess.h>
#include <linux/init.h>

#include <asm/mach/map.h>
#include <asm/io.h>


#define DEVICE_NAME    "led"
#define GPF_BASE_ADDR  0x56000050
#define LED_ON         1
#define LED_OFF        0

static struct led_dev_t{
    
    
	struct cdev cdev;/*定义字符设备*/
	int major;       /*主设备号*/
	struct class *class;  /*类*/
	struct device *device;/*设备*/

	unsigned int __iomem *gpfcon;  /*gpfcon 寄存器*/
	unsigned int __iomem *gpfdat;  /*gpfdat 寄存器*/
}led_dev;


static void led_switch(u32 on_off)
{
    
    
	u32 val;
	
	if(on_off == 1){
    
    
		val = readl(led_dev.gpfdat);
		val &= ~((1<<4) | (1<<5) | (1<<6));
	    writel(val,led_dev.gpfdat);
	}else{
    
    
		val = readl(led_dev.gpfdat);
		val |= ((1<<4) | (1<<5) | (1<<6));
	    writel(val,led_dev.gpfdat);
	}
}

static int led_open(struct inode *inode, struct file *filp)
{
    
    
	u32 val;

	filp->private_data = &led_dev;
		
	val = readl(led_dev.gpfcon);
	val &= ~(3<<(4*2) | 3<<(5*2) | 3<<(6*2));
	val |= 1<<(4*2)| 1<<(5*2)| 1<<(6*2);
	writel(val, led_dev.gpfcon);

	//printk("gpfcon:0x%x\n",*led_dev.gpfcon);

	return 0;
}


static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    
    
	return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    
    	
	u32 val,ret;

	ret = copy_from_user(&val,buf,cnt);

	if(ret < 0){
    
    
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	
	if (val == 1){
    
    
		led_switch(LED_ON);
	}else{
    
    
		led_switch(LED_OFF);
	}

	return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    
    
	
	return 0;
}


static struct file_operations led_fops = {
    
    
	.owner   = THIS_MODULE,
	.open    = led_open,
	.read    = led_read,
	.write   = led_write,
	.release = led_release,

};


static int __init led_init(void)
{
    
    
	dev_t devid;
    /*1.创建设备号*/
	if(led_dev.major){
    
    
		devid = MKDEV(led_dev.major,0);
		register_chrdev_region(devid, 1, DEVICE_NAME);
	}else{
    
    
		alloc_chrdev_region(&devid, 0, 1, DEVICE_NAME);
		led_dev.major = MAJOR(devid);
	}

	/*2.初始化字符设备cdev*/
	cdev_init(&led_dev.cdev, &led_fops);

	/*3.向内核添加一个cdev*/
	cdev_add(&led_dev.cdev, devid, 1);

	/*4.创建类*/
	led_dev.class = class_create(THIS_MODULE, DEVICE_NAME);
	if(IS_ERR(led_dev.class)){
    
    
		return PTR_ERR(led_dev.class);
	}

	/*5.创建设备*/
	led_dev.device = device_create(led_dev.class, NULL, devid, NULL, DEVICE_NAME);
	if(IS_ERR(led_dev.device)){
    
    
		return PTR_ERR(led_dev.device);
	}

	/*6.初始化硬件*/
	led_dev.gpfcon = ioremap(GPF_BASE_ADDR, 8);
	led_dev.gpfdat = led_dev.gpfcon + 1;
	
	return 0;
	
}

static void __exit led_exit(void)
{
    
    
	cdev_del(&led_dev.cdev); /*删除cdev*/
	unregister_chrdev_region(MKDEV(led_dev.major,0),1); /*注销设备号*/

	device_destroy(led_dev.class, MKDEV(led_dev.major,0)); /*删除设备*/
	class_destroy(led_dev.class);

	iounmap(led_dev.gpfcon);
}


module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

(2) 编写Makefile,代码如下:

KERN_DIR = /home/book/works/linux-4.15

all:
	make -C $(KERN_DIR) M=`pwd` modules

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m  += led_drv.o

(3) 编写测试app,led_app.c 的代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

/*int main(int argc,char **argv) 参数详解
 * argc:命令行总的参数个数
 * argv:对应argc个参数,其中argv[0]是程序的全名,后面的参数对应着用户输入的参数
 * 
 *例如:应用程序:led_app on 
 *      其中:led_app是应用程序的名称,on是用户输入的参数
 *      此时:argc = 2, argv[0] = "led_app" , argv[1] = "on"
 */
int main(int argc,char **argv)
{
    
    
	int val;
    int fd;

	fd = open("/dev/led",O_RDWR);

	if(fd<0) printf("can't open!\n");
	if (argc != 2)
	{
    
    
		printf("Usage :\n");
		printf("%s <on|off>\n", argv[0]);
		return 0;
	}

	if(strcmp(argv[1],"on") == 0) val = 1;
	else  val = 0;

    write(fd,&val,sizeof(val));
	close(fd);

	return 0;
}

测试时输入命令:./led_app on,点亮LED;输入命令:./led_app off,熄灭LED。

猜你喜欢

转载自blog.csdn.net/qq_35031421/article/details/105258100