Linux下的任何外设驱动,最终都是要配置相应的硬件寄存器。 前面的文章中介绍了新旧字符设备的驱动开发框架,也介绍了IMX6ULL处理器GPIO的工作原理及配置方法,本篇我们将实际操作一个GPIO,点亮Linux驱动开发路上的第一个灯
1. 地址映射
1.1 MMU介绍
MMU (Memory Manage Unit),即内存管理单元,它提供统一的内存空间抽象,程序访问虚拟内存中的地址,MMU将虚拟地址翻译成实际的物理地址,之后CPU即可操作实际的物理地址
MMU具有如下功能:
1.2 IO映射函数
Linux内核启动时会初始化MMU,设置内存映射,之后CPU访问的都是虚拟地址。 在程序编写时,可使用下面两个函数进行物理内存和虚拟内存之间的转换
ioremap():将物理地址映射为虚拟地址
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype){
return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}
//phys_addr:被映射的IO起始地址(物理地址)
//size:需要映射的空间大小,以字节为单位
//mtype: ioremap的类型
//return: __iomem类型的指针,指向映射成功后返回的虚拟空间起始地址
ioremap():将物理地址映射为虚拟地址
void iounmap (volatile void __iomem *addr)
//addr: 要取消映射的虚拟地址空间首地址
//return: void
1.3 IO内存访问函数
使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,理论上就可以直接通过指针访问这些地址了,但是为了符合驱动的跨平台以及可移植性,推荐使用一组操作函数来对映射后的内存进行读写操作
u8 readb(const volatile void __iomem *addr); /*读取一个字节*/
u16 readw(const volatile void __iomem *addr); /*读取一个字*/
u32 readl(const volatile void __iomem *addr); /*读取一个双字*/
void writeb(u8 value, volatile void __iomem *addr); /*写入一个字节*/
void writew(u16 value, volatile void __iomem *addr); /*写入一个字*/
void writel(u32 value, volatile void __iomem *addr); /*写入一个双字*/
2. 程序编写
本实验目的:编写Linux下的LED灯驱动,通过应用程序对I.MX6U开发板上的LED灯(GPIO1_IO03)进行开关操作
2.1 驱动程序编写
LED驱动属于字符设备驱动,之前介绍了新旧两种字符驱动的写法,本篇中按照新字符设备驱动的框架来编写
接下来分步骤完善具体的驱动代码:
GPIO寄存器宏定义、设备结构体定义
#define NEWCHRLED_CNT 1 //设备号个数
#define NEWCHRLED_NAME "newchrled" //名字
#define LEDOFF 0 //关灯
#define LEDON 1 //开灯
/* 寄存器物理地址 */
#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)
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; //设备号
struct cdev cdev; //cdev
struct class *class; //类
struct device *device; //设备
int major; //主设备号
int minor; //次设备号
};
struct newchrled_dev newchrled; //led设备
控制LED亮灭函数
void led_switch(u8 sta){
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val|= (1 << 3);
writel(val, GPIO1_DR);
}
}
设备操作函数集合
/* 打开设备 */
static int led_open(struct inode *inode, struct file *filp){
filp->private_data = &newchrled; //设置私有数据
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){
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\\r\\n");
return -EFAULT;
}
ledstat = databuf[0]; //获取状态值
if(ledstat == LEDON) {
led_switch(LEDON); //打开LED灯
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); //关闭LED灯
}
return 0;
}
/* 关闭设备 */
static int led_release(struct inode *inode, struct file *filp){
return 0;
}
/* 设备操作函数 */
static struct file_operations newchrled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
在驱动入口函数中:初始化GPIO外设
/* 驱动入口函数 */
static int __init led_init(void){
u32 val = 0;
/* 初始化LED */
/* 1、寄存器地址映射 */
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);
/* 2、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); //之前的设置
val |= (3 << 26); //设置新值
writel(val, IMX6U_CCM_CCGR1);
/* 3、设置复用功能,并设置IO属性 */
writel(5, SW_MUX_GPIO1_IO03);
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 4、设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); //清除之前的设置
val |= (1 << 3); //设置为输出
writel(val, GPIO1_GDIR);
/* 5、默认关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
在驱动入口函数中:注册字符设备
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (newchrled.major) { //定义了设备号
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
} else { //没有定义设备号
alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);
newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MINOR(newchrled.devid);
}
printk("newcheled major=%d,minor=%d\\r\\n",newchrled.major, newchrled.minor);
/* 2、初始化cdev */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
/* 3、添加一个cdev */
cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
/* 4、创建类 */
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class)) {
return PTR_ERR(newchrled.class);
}
/* 5、创建设备 */
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device)) {
return PTR_ERR(newchrled.device);
}
return 0;
}
在驱动出口函数中:取消地址映射,注销字符设备驱动
/* 驱动出口函数 */
static void __exit led_exit(void){
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/* 注销字符设备驱动 */
cdev_del(&newchrled.cdev);
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
device_destroy(newchrled.class, newchrled.devid);
class_destroy(newchrled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
2.2 应用程序编写
LED驱动加载成功以自动创建设备节点,应用程序通过向节点文件写0或1,关闭/打开LED灯
#define LEDOFF 0
#define LEDON 1
int main(int argc, char *argv[]){
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\\r\\n");
return -1;
}
filename = argv[1];
/* 打开led驱动 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\\r\\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]); //要执行的操作:打开或关闭
/* 向/dev/led文件写入数据 */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\\r\\n");
close(fd);
return -1;
}
retvalue = close(fd);
if(retvalue < 0){
printf("file %s close failed!\\r\\n", argv[1]);
return -1;
}
return 0;
}
3. 编译测试
3.1 程序编译
驱动程序编译:创建Makefile文件,使用make命令,编译驱动程序
KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi
CURRENT_PATH := $(shell pwd)
obj-m := newchrled.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
应用程序编译:无需内核参与,直接编译即可
arm-linux-gnueabihf-gcc newchrledApp.c -o newchrledApp
3.2 运行测试
为了方便,选择通过TFTP从网络启动,并使用NFS挂载网络根文件系统。 确保开发板能正常启动,在Ubuntu中将驱动和测试文件复制到modules/4.1.15目录中
加载驱动模块
depmod #第一次加载驱动的时候需要运行此命令
modprobe newchrled.ko #加载驱动
驱动加载成功后会自动在/dev目录下创建设备节点文件/dev/newchrdev,输入如下命令查看
ls /dev/newchrled -l
之后就可使用应用程序来测试驱动是否能正常工作
./newchrledApp /dev/newchrled 1 #打开LED灯
./newchrledApp /dev/newchrled 0 #关闭LED灯
若要卸载驱动输入如下命令
rmmod newchrled.ko
至此,Linux 驱动开发路上的第一个灯被成功点亮。 本文主要是通过操作寄存器来点亮开发板上的LED,通过编写相应的驱动程序和应用程序,实现程序设计的分层
全部0条评论
快来发表一下你的评论吧 !