Linux——字符设备驱动控制LED

目录

驱动模块的加载和卸载

驱动程序Makefile编写

字符设备注册与注销

字符设备驱动模板

应用程序对驱动读写操作

iounmap函数

LED寄存器物理地址映射到虚拟地址

应用程序代码编写


Linux驱动的两种运行方式:

1、将驱动编译进Linux内核中,也就是zImage,当内核启动的说话就会自动运行驱动程序;

2、将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后要用insmod命令加载驱动模块,用rmmod命令卸载驱动模块

驱动模块的加载和卸载

模块的加载和卸载的注册函数如下:

cpp 复制代码
module_init(xxx_init);  //注册模块加载函数
module_exit(xxx_exit);  //注册模块卸载函数

编译驱动的时候需要用到linux内核源码,因此要解压缩linux内核源码,然后再编译,得到zImage和.dtb。需要使用编译后的zImage和dtb启动系统。

驱动程序Makefile编写

cpp 复制代码
KERNELDIR := /home/zzs/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga

CURRENT_PATH := $(shell pwd)

obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

驱动编译完成以后扩展名为.ko,两种命令可以加载驱动模块:insmod和modprobe

在启动Linux内核后,输入如下命令加载驱动:

cpp 复制代码
insmod drv.ko

modprobe命令相比于insmod命令,区别在于modprobe可以解决依赖关系的问题,insmod命令如果要驱动依赖于first.ko模块的模块,就有一个先加载后加载的问题,而modprobe就比较智能一些,会对模块进行依赖关系的分析,然后就所有的依赖模块都加载到内核中。

我们将编译生成的.ko模块拷贝到rootfs中,通过nfs传输到开发板中进行使用。

使用modprobe命令出现下面的错误:

原因是没有加载modules.dep,通过depmod命令进行加载,如果无法使用depmod,要通过busybox重新配置。

显示当前存在的模块命令如下:

cpp 复制代码
lsmod

驱动模块的卸载使用rmmod命令即可,因为另一种卸载驱动模块的命令modprobe -r的使用前提是所卸载模块的依赖模块已经没有被其他模块使用,否则就不能使用该命令卸载驱动模块:

cpp 复制代码
rmmod drv.ko

使用printk打印日志信息验证模块的加载和卸载:

字符设备注册与注销

字符设备的注册函数

cpp 复制代码
static inline register chrdev(unsigned int major,const char *name,const struct file_operations *fops)

字符设备的注销函数

cpp 复制代码
static inline void unregister_chrdev(unsigned int major,const char *name)

major:主设备号,Linux下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分

name:设备名字

fops:结构体file_operations类型指针,指向设备的操作函数集合变量
设备号由主设备号和次设备号组成,主设备号表示一个具体的驱动,次设备号表示使用这个驱动的各个设备。

设备号的类型为dev_t,是一个定义为u32的数据类型,也就是unsigned int,其中,高12位是主设备号,低20位是次设备号,所有主设备号的范围是0~4095,选择主设备号时不要超出这个范围。

查看当前系统所有已经使用了的设备号:

cpp 复制代码
cat /proc/devices

一般字符设备的注册在驱动模块的入口函数xxx_init中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行。

字符设备驱动模板

cpp 复制代码
/*打开设备*/
static int chrtest_open(struct inode *inode,struct file *filp)
{
	/*用户实现具体功能*/
	return 0;
}

/*从设备读取*/
static ssize_t chrtest_read(struct file *filp,char __user *buf,size_t cnt,loft_t *offt)
{
	/*用户实现具体功能*/
	return 0;
}

/*向设备写数据*/
static ssize_t chrtest_write(struct file *filp,const char __user *buf,size_t cnt,loft_t *offt)
{
	/*用户实现具体功能*/
	return 0;
}

/*关闭/释放设备*/
static int chrtest_release(struct inode *inode,struct file *filp)
{
	/*用户实现具体功能*/
	return 0;
}

static struct file_operations test_fops={
	.owner = THIS_MODULE,
	.open = chrtest_open,
	.read = chrtest_read,
	.write = chrtest_write,
	.release = chrtest_release,
} ;

/*驱动入口函数*/
static int __init xxx_init(void)
{
	/*入口函数具体内容*/
	int retvalue = 0;
	
	/*注册字符设备驱动*/
	retvalue = register_chrdev(200,"chrtest",&test_fops);
	if(retvalue<0){
	/*字符设备注册失败,自行处理*/
	}
	return 0;
}

/*驱动出口函数*/
static void __exit xxx_exit(void)
{
	/*注销字符设备驱动*/
	unregister_chrdev(200,"chrtest");
}

/*将上面两个函数指定为驱动的入口和出口函数*/
module_init(xxx_init);
module_exit(xxx_exit);

/*添加LICENSE和作者信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZHANGZHONGSHENG");

应用程序对驱动读写操作

驱动给应用传递数据的时候用到copy_to_user函数(该函数用来完成内核空间的数据到用户空间的复制),函数原型如下:

cpp 复制代码
static inline long copy_to_user(void *to,const void *from,unsigned long n);

参数to表示目的,参数from表示源,参数n表示要复制的数据长度,如果复制成功,返回值为0,如果复制失败返回负数。

因为用户空间的内存不能直接访问内核空间内存,所以使用copy_from_user函数来实现,函数原型如下:

cpp 复制代码
static inline long copy_from_user(void *to,const void *from,unsigned long n);

字符串转换为整型数据:

首先添加<stdlib>库,然后使用atoi函数。

MMU

全称是Memory Manage Unit,内存管理单元,其主要功能如下:

1、完成虚拟空间到物理空间的映射;

2、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性

裸机的时候可以直接对0x01010101这个物理地址进行操作,但是linux不行,因为linux会使能mmu

在Linux里面操作的都是虚拟地址,所以需要得到0x01010101这个物理地址的虚拟地址。

ioremap函数

如果我们没有使能MMU,可以直接向寄存器地址读写数据,但是我们现在启动Linux内核后,会自动使能MMU,此时我们需要将这个寄存器的物理地址转换为虚拟地址,涉及到两个函数,如下:

ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h中,本质是一个宏

第一个参数是物理地址起始大小,第二个参数是要转换的字节数量,例如0x01010101开始的10个地址进行转换:

cpp 复制代码
va = ioremap(0x01010101,10)

返回值是转换的起始地

iounmap函数

卸载驱动时使用iounmap函数释放掉ioremap函数所做的映射

cpp 复制代码
iounmap(va);

寄存器物理地址

cpp 复制代码
#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)

LED寄存器物理地址映射到虚拟地址

(1)地址映射后的虚拟地址指针

cpp 复制代码
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

(2)物理地址映射成虚拟地址供Linux使用(在驱动入口函数中)

cpp 复制代码
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);

(3)取消地址映射(在驱动出口函数中)

cpp 复制代码
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);

(4)对虚拟地址进行初始化配置

cpp 复制代码
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);      /*先清除以前的配置bit26,27*/
val |= 3 << 26;         /*bit26,27置1*/
writel(val,IMX6U_CCM_CCGR1);

writel(0x5,SW_MUX_GPIO1_IO03);          //设置复用
writel(0X10B0,SW_PAD_GPIO1_IO03);       //设置电气属性

val = readl(GPIO1_GDIR);
val |= 1 << 3;                  //bit3置1,设置为输出
writel(val,GPIO1_GDIR);    

val = readl(GPIO1_DR);
val &= ~(1 << 3);                  //bit3清零,打开LED
writel(val,GPIO1_DR);  

应用程序代码编写

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*
 *argc:应用程序参数个数
 *argv:具体的参数内容,字符串形式
 *./ledAPP <filename> <0:1> 0表示关灯 1表示开灯
 *./ledAPP /dev/led 0   关灯
 *./ledAPP /dev/led 1   开灯 
 */

#define LEDOFF  0
#define LEDON   1

int main(int argc,char *argv[])
{
    int fd,retvalue;  	//fd是文件描述符
    char *filename;		//filename是设备名称
    unsigned char databuf[1];
    if(argc != 3){
        printf("Error Usage!\r\n");
        return -1;
    }
    filename = argv[1];
    fd = open(filename,O_RDWR);	//O_RDWR是用读写模式
    if(fd < 0){
        printf("file %s open failed!\r\n",filename);
        return -1;
    }
    databuf[0] = atoi(argv[2]);   //将字符数据转换为数字

    retvalue = write(fd,databuf,sizeof(databuf));	//对设备进行写操作
    if(retvalue < 0){
        printf("LED control Failed!\r\n");
        close(fd);
        return -1;
    }

    close(fd);

    return 0;
}

创建设备结点命令:

cpp 复制代码
mknod /dev/led c 200 0

/dev/led是设备名称,是传递给应用程序的第二个参数,c表示字符设备,200是主设备号,0表示次设备号

相关推荐
乔巴不是狸猫18 分钟前
第11周作业
linux
Bessssss1 小时前
centos权限大集合,覆盖多种权限类型,解惑权限后有“. + t s”问题!
linux·运维·centos
silver6872 小时前
Linux 下的 GPT 和 MBR 分区表详解
linux
R-sz3 小时前
14: curl#6 - “Could not resolve host: mirrorlist.centos.org; 未知的错误“
linux·python·centos
code_abc3 小时前
Shell 脚本编程基础:变量
linux
星如雨落3 小时前
Linux shell脚本对常见图片格式批量转换为PDF文件
linux·shell
冰红茶兑滴水3 小时前
云备份项目--工具类编写
linux·c++
董健正3 小时前
centos制作离线安装包
linux·运维·centos
猫猫的小茶馆3 小时前
【数据结构】数据结构整体大纲
linux·数据结构·算法·ubuntu·嵌入式软件
shelby_loo3 小时前
使用 Docker 在 Ubuntu 下部署 Cloudflared Tunnel 服务器
服务器·ubuntu·docker