嵌入式LINUX学习心得–基于正点原子IMX6ULL
观前提醒:用电脑带目录看,此文更多是介绍性和作者自我小结用。
一.前言
笔者基于正点原子IMX6ULL来学习嵌入式LINUX驱动开发,嵌入式LINUX开发大致可分为两个大方向:1.应用开发;2.驱动开发。其中应用开发主要是使用已经编写好的驱动来完成具体的项目应用,对芯片底层相关知识要求较低,程序运行于用户态;驱动开发顾名思义,主要是为应用开发提供各个功能模块的接口,开发者需要对芯片的硬件性能和LINUX内核有一定了解。由于笔者目前只学习了驱动开发故只介绍驱动开发相关知识。
本文绝大多出自I.MX6U 嵌入式 Linux 驱动开发指南 V1.5.1中,关于驱动开发,笔者最大的经验就是多写代码,多做有关的项目应用。
二.驱动开发前预备内容
1.LINUX系统相关命令学习
这部分主要包括LINUX的一些常用基本命令,VIM编辑器使用,以及shell编程的内容,这部分上手较快,网上有很多资料,笔者不再赘述。
2.LINUX系统移植
1.U-BOOT系统
Linux 系统要启动就必须需要一个 bootloader 程序,也就说芯片上电以后先运行一段bootloader程序。这段bootloader程序会先初始化DDR等外设,然后将Linux内核从flash(NAND,NOR FLASH, SD, MMC 等)拷贝到 DDR 中,最后启动 Linux 内核。当然了, bootloader 的实际工作要复杂的多,但是它最主要的工作就是启动 Linux 内核, bootloader 和 Linux 内核的关系,就跟 PC 上的 BIOS 和 Windows 的关系一样, bootloader 就相当于 BIOS。所以我们要先搞定bootloader,很庆幸,有很多现成的 bootloader 软件可以使用,比如 U-Boot、 vivi、 RedBoot 等等,其中以 U-Boot 使用最为广泛。
uboot启动过程的具体行为
第一阶段:
初始化时钟,关闭看门狗,关中断,启动ICACHE,关闭DCACHE和TLB,关闭MMU,初始化NAD FLASH,重定位。
第二阶段:
初始化一个串口,检测系统内存映射,将内核映像和根文件系统从Flash读到SDRAM空间中,为内核设置启动参数,调用内核。
uboot与内核如何完成参数传递?
uboot启动后已完成基本的硬件初始化(内存,串口),接下来,主要任务是加载Linux内核到开发板的内存,然后跳转到LINUX内核所在地址运行。
TIPS:面经,只要问到uboot,面试官必问uboot和内核的参数传递。
具体如何跳转?直接修改PC寄存器的值为LINUX内核所在的地址,这样CPU就会从LINUX内核所在地址去取指令,从而执行代码。
跳转到内核之前,uboot需要做的三件事情:
1.CPU寄存器的设置
R0=0;
R1=机器类型ID
R2=启动参数标记列表在RAM中起始基地址
2.CPU工作模式
必须静止中断(IRQs和FIQs)
CPU必须为SVC模式
3.cache和MMU的设置
MMU必须关闭
指令Cache可以打开也可以关闭
数据Cache必须关闭
其中上面第一步CPU寄存器的设置中,就是通过R0,R1,R2三个参数给内核传递参数的。
uboot系统移植
见正点原子I.MX6U 嵌入式 Linux 驱动开发指南 V1.5.1
2.LINUX内核移植
见正点原子I.MX6U 嵌入式 Linux 驱动开发指南 V1.5.1
3.根文件系统构建
Linux 中的根文件系统更像是一个文件夹或者叫做目录(在我看来就是一个文件夹,只不过是特殊的文件夹),在这个目录里面会有很多的子目录。根目录下和子目录中会有很多的文件,这些文件是 Linux 运行所必须的,比如库、常用的软件和命令、设备文件、配置文件等等。以后我们说到文件系统,如果不特别指明,统一表示根文件系统。
根文件系统首先是内核启动时所 mount(挂载)的第一个文件系统,内核代码映像文件保存在根文件系统中,而系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。
根文件系统构建方法
见正点原子I.MX6U 嵌入式 Linux 驱动开发指南 V1.5.1
三.驱动开发相关知识点
驱动开发设备大致分为三类:字符设备驱动,块设备驱动,网络设备驱动,笔者主要学习字符设备驱动
一.字符驱动开发步骤
1.驱动模块的加载和卸载
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
2.字符设备注册与注销
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
3.实现设备的具体操作函数
1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t
*);
1592 ssize_t (*write) (struct file *, const char __user *, size_t,
loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct
*);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned
long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned
long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
1606 int (*fasync) (int, struct file *, int);
1607 int (*lock) (struct file *, int, struct file_lock *);
1608 ssize_t (*sendpage) (struct file *, struct page *, int, size_t,
loff_t *, int);
1609 unsigned long (*get_unmapped_area)(struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
1610 int (*check_flags)(int);
1611 int (*flock) (struct file *, int, struct file_lock *);
1612 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,
loff_t *, size_t, unsigned int);
1613 ssize_t (*splice_read)(struct file *, loff_t *, struct
pipe_inode_info *, size_t, unsigned int);
1614 int (*setlease)(struct file *, long, struct file_lock **, void
**);
1615 long (*fallocate)(struct file *file, int mode, loff_t offset,
1616 loff_t len);
1617 void (*show_fdinfo)(struct seq_file *m, struct file *f);
1618 #ifndef CONFIG_MMU
1619 unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };
4.添加LICENSE和作者信息
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
Linux设备号
Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
实验代码:
1.驱动代码:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/module.h>
#include<linux/ide.h>
#include <linux/init.h>
#define CHRDEVYQX_MAJOR 233
#define CHRDEVYQX_NAME "chrdevYqx"
char readbuf[100];//读缓存区
char writebuf[100];//写缓存区
char kerneldata[]= "hello,I'm kenerdata";
/*打开设备*/
static int chrdevYqx_open(struct inode *inode,struct file *flip){
printk("chrdevYqx is opened\r\n");
return 0;
}
/*从设备读取数据*/
static ssize_t chrdevYqx_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt){
int retvalue = 0;
memcpy(readbuf,kerneldata,sizeof(kerneldata));
retvalue = copy_to_user(buf,readbuf,cnt);
if(retvalue==0){
printk("send data is sucessful\r\n");
} else {
printk("send data is failed\r\n");
}
return 0;
}
/*从设备写数据*/
static ssize_t chrdevYqx_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt){
int retvalue = 0;
retvalue = copy_from_user(writebuf,buf,cnt);
if(retvalue == 0){
printk("receive DATA:%s\r\n",writebuf);
} else {
printk("receive DATA is failed\r\n");
}
return 0;
}
/*关闭/释放设备*/
static int chrdevYqx_release(struct inode *inode,struct file *filp){
return 0;
}
/*设备具体函数操作集*/
static struct file_operations chrdevYqx_fops = {
.owner=THIS_MODULE,//结构体变量初始化
.open=chrdevYqx_open,
.read=chrdevYqx_read,
.write=chrdevYqx_write,
.release=chrdevYqx_release,
};
/**驱动入口函数**/
static int __init chrdevYqx_init(void){
int retvalue = 0;
/*注册chrdevYqx设备*/
retvalue = register_chrdev(CHRDEVYQX_MAJOR,CHRDEVYQX_NAME,&chrdevYqx_fops);
if(retvalue<0){
printk("chrdevYqx is failed\r\n");//字符设备注册失败
}
printk("chrdevYqx_init\r\n");
return 0;
}
/**驱动出口函数**/
static void __exit chrdevYqx_exit(void){
/*注销字符设备驱动*/
unregister_chrdev(CHRDEVYQX_MAJOR,CHRDEVYQX_NAME);
}
/*驱动模块加载*/
module_init(chrdevYqx_init);//chrdevYqx模块加载函数
module_exit(chrdevYqx_exit);//chrdevYqx模块卸载函数
/*作者信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yqx");
二.新字符设备驱动开发实验
与上个字符设备驱动开发实验相比,此字符设备驱动开发可以自动向内核申请设备号,并且可以自动创建设备节点。
1.地址映射
在编写驱动之前,我们需要先简单了解一下 MMU 这个神器, MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在Linux 内核已经支持无 MMU 的处理器了。 MMU 主要完成的功能如下:
①、完成虚拟空间到物理空间的映射。
②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA, Physcical Address)。
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU访问的都是虚拟 地 址 。
这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap。
1.1、 ioremap 函数
ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h 文件中
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
//宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址, SW_MUX_GPIO1_IO03 是
//映射后的虚拟地址。对于 I.MX6ULL 来说一个寄存器是 4 字节(32 位)的,因
//此映射的内存长度为 4。映射完成以后直接对 SW_MUX_GPIO1_IO03 进行读写操作即可。
1.2.iounmap 函数
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射, iounmap 函数原型如下:
void iounmap (volatile void __iomem *addr)
iounmap(SW_MUX_GPIO1_IO03);
1.3.I/O内存访问函数
1.读操作函数
1 u8 readb(const volatile void __iomem *addr)
2 u16 readw(const volatile void __iomem *addr)
3 u32 readl(const volatile void __iomem *addr)
//readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读
//操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。
2.写操作函数
1 void writeb(u8 value, volatile void __iomem *addr)
2 void writew(u16 value, volatile void __iomem *addr)
3 void writel(u32 value, volatile void __iomem *addr)
//writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit
//写操作,参数 value 是要写入的数值, addr 是要写入的地址。
2.自动创建设备节点
1.分配和注册设备号
解决这两个问题最好的方法就是要使用设备号的时候向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。这个就是我们在 讲解的设备号的分配,如果没有指定设备号的话就使用如下函数来申请设备号。
如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过
alloc_chrdev_region 函 数 还 是register_chrdev_region 函数申请的设备
号,统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
3.cdev
在 Linux 中使用 cdev 结构体表示一个字符设备
在 cdev 中有两个重要的成员变量: ops 和 dev,这两个就是字符设备文件操作
函数集合file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义
一个 cdev 结构体变量,这个变量就表示一个字符设备,如下所示:
struct cdev test_cdev;
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化, cdev_init
函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用
cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向
Linux 系统添加这个字符设备。
cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设
备, cdev_del函数原型如下:
void cdev_del(struct cdev *p)
2.mdev机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除, udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除。
3.创建和删除类
创建类
struct class *class_create (struct module *owner, const char *name)
删除类:
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
4.创建设备
上一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备,
device_create 函数原型如下:
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为device_destroy,函数原型如下:
void device_destroy(struct class *class, dev_t devt)
3.设置文件私有数据
4.实验代码:
1.驱动代码
#include<linux/types.h>
#include<linux/kernel.h>
#include<linux/delay.h>
#include<linux/ide.h>
#include<linux/init.h>
#include<linux/module.h>
#include<linux/errno.h>
#include<linux/gpio.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<asm/mach/map.h>
#include<asm/io.h>
/*寄存器地址*/
#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;
#define NEWCHRDEV_NAME "newchrdev"
#define NEWCHRDEV_CNT 1
#define LED_ON 1
#define LED_OFF 0
/*设备结构体*/
struct newchrdev_dev{
dev_t devid; //设备号
struct cdev cdev; //cdev
struct class *class; //类指针
struct device *device ; //设备
int major; //主设备号
int minor; //副设备号
};
struct newchrdev_dev newchrdev;
void led_switch(u8 ledstat){
u32 val=0;
switch(ledstat){
case 1:
val = readl(GPIO1_DR);
val &= ~(1<<3);
writel(val,GPIO1_DR);
break;
case 0:
val = readl(GPIO1_DR);
val |= (1<<3);
writel(val,GPIO1_DR);
break;
}
}
/*打开设备函数*/
static int newchrdev_open(struct inode *inode,struct file *filp){
filp->private_data = &newchrdev;//将私有数据定义到newchrdev设备结构体中,从而可在设备操作函数中调用设备结构体中数据
return 0;
}
/*设备读取函数*/
static ssize_t newchrdev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt){
return 0;
}
/*设备写入函数*/
static ssize_t newchrdev_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt){
int retvalue=0;
unsigned char databuf[1];
unsigned char ledsta;
retvalue = copy_from_user(databuf,buf,cnt);
if(retvalue < 0){
printk("用户写入数据失败!\r\n");
}
ledsta = databuf[0];
switch(ledsta){
case 1:
led_switch(LED_ON);
break;
case 0:
led_switch(LED_OFF);
break;
default:
printk("输入错误,请重新输入!\r\n");
break;
}
return 0;
}
/*设备关闭函数*/
static int newchrdev_release(struct inode *inode,struct file *filp){
return 0;
}
/*设备操作函数集*/
static struct file_operations newchrdev_fops = {
.owner = THIS_MODULE,
.open = newchrdev_open,
.release = newchrdev_release,
.read = newchrdev_read,
.write = newchrdev_write,
};
/*驱动入口函数*/
static int __init newchrdev_init(void){
u32 val = 0;
/*物理地址转变为虚拟地址*/
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);
/*使能GPIO1时钟*/
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3<<26);//清除过去时钟设置
val |= (3<<26);//使能GPIO1时钟
writel(val,IMX6U_CCM_CCGR1);
/*设置GPIO1——IO03端口属性*/
writel(5,SW_MUX_GPIO1_IO03);//GPIO3IO模式
writel(0X10B0,SW_PAD_GPIO1_IO03);//设置IO口电气属性
val = readl(GPIO1_GDIR);
val &= ~(1<<3);//清除以前的设置
val |= (1<<3) ;//设置为输出
writel(val,GPIO1_GDIR);
/*初始默认关闭IO3端口*/
val = readl(GPIO1_DR);
val |= (1<<3);
writel(val,GPIO1_DR);
/*注册字符设备驱动*/
if(newchrdev.major){
//给定了主设备号,手动申请设备号
newchrdev.devid = MKDEV(newchrdev.major,0);//使用MKDEV()构建设备号,次设备号选0
register_chrdev_region(newchrdev.devid,NEWCHRDEV_CNT,NEWCHRDEV_NAME);
} else {
alloc_chrdev_region(&newchrdev.devid,0,NEWCHRDEV_CNT,NEWCHRDEV_NAME);
newchrdev.major = MAJOR(newchrdev.devid);
newchrdev.minor = MINOR(newchrdev.minor);
}
printk("设备号成功申请:MAJOR:%d,MINOR:%d\r\n",newchrdev.major,newchrdev.minor);
/*初始化cdev;注册设备*/
cdev_init(&newchrdev.cdev,&newchrdev_fops);
cdev_add(&newchrdev.cdev,newchrdev.devid,NEWCHRDEV_CNT);
/*自动创建设备节点*/
/*1.创建类*/
newchrdev.class = class_create(THIS_MODULE,NEWCHRDEV_NAME);
if(IS_ERR(newchrdev.class)){
return PTR_ERR(newchrdev.class);
}
/*2.创建设备*/
newchrdev.device = device_create(newchrdev.class,NULL,newchrdev.devid,NULL,NEWCHRDEV_NAME);
if(IS_ERR(newchrdev.device)){
return PTR_ERR(newchrdev.device);
}
return 0;
}
/*驱动出口函数*/
static void __exit newchrdev_exit(void){
/*取消映射*/
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/*1.删除设备*/
device_destroy(newchrdev.class,newchrdev.devid);
/*2.删除类*/
class_destroy(newchrdev.class);
/*3.删除cdev*/
cdev_del(&newchrdev.cdev);
/*4.删除设备号*/
unregister_chrdev_region(newchrdev.devid,NEWCHRDEV_CNT);
}
/*注册驱动出口入口函数,确定代码开源协议和作者名字*/
module_init(newchrdev_init);
module_exit(newchrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yqx");
三.设备树相关知识
前面一个实验是通过直接使用MMU来操作物理地址和虚拟地址直接对寄存器进行访问,这种技术现在已经不是主流,现在大多使用设备树技术进行操作。用户将需要使用的设备的节点以及其相关信息,比如寄存器地址等信息写入设备树文件中,然后LINUX内核读取设备树文件,驱动文件在使用设备树里面保存好的设备信息,从而达到控制设备的目的,此项技术大大减少了冗余重复代码的出现。
本章通过实验来学习设备树,具体原理读者可自行翻阅其它书籍。
实验例程
1.在dts文件中加入设备节点
/*YQX 2022/4/21*/
yqxled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "yqx,yqxLed";
status = "okay";
reg = < 0X020C406C 0x04 /*CCM_CCR1_BASE*/
0X020E0068 0x04 /*SW_MUX_GPIO1_IO03_BASE*/
0X020E02F4 0x04 /*SW_PAD_GPIO1_IO03_BASE*/
0X0209C000 0x04 /*GPIO1_DR_BASE*/
0X0209C004 0x04>; /*GPIO1_GDIR_BASE*/
};
2.驱动程序编写
#include<linux/types.h>
#include<linux/kernel.h>
#include<linux/delay.h>
#include<linux/ide.h>
#include<linux/init.h>
#include<linux/module.h>
#include<linux/errno.h>
#include<linux/gpio.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/of.h>
#include<linux/of_address.h>
#include<asm/mach/map.h>
#include<asm/uaccess.h>
#include<asm/io.h>
#define DEVICE_NAME "dtsled"
#define DEVICE_CNT 1
#define LED_ON 1
#define LED_OFF 0
/*定义寄存器虚拟地址指针变量*/
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;
//dtsled设备结构体
typedef struct dtsled_dev{
dev_t devid;//设备号
struct cdev cdev;//cdev
struct class *class;//类
struct device *device;//节点设备
int major;//主设备号
int minor; //次设备号
struct device_node *nd;//设备树节点
} dtsled_dev;
dtsled_dev dtsled;//led设备
void led_switch(int led_flag){
u32 val;
if(led_flag == 1){
//开LED
val = readl(GPIO1_DR);
val &= ~(1<<3);
writel(val,GPIO1_DR);
} else {
val = readl(GPIO1_DR);
val |= (1<<3);
writel(val,GPIO1_DR);
}
}
/*打开设备*/
static int led_open(struct inode *inode,struct file *filp){
filp->private_data = &dtsled;//设置私有数据
return 0;
}
static ssize_t led_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt){
char writebuf[1];
int retvalue;
dtsled_dev *tempdata = filp->private_data;
printk("MAJOR:%d,MINOR:%d",tempdata->major,tempdata->minor);
retvalue = copy_from_user(writebuf,buf,cnt);
if(retvalue < 0){
printk("kerneldata error!\r\n");
}
if(writebuf[0]==1){
led_switch(LED_ON);
}
if(writebuf[0]== 0){
led_switch(LED_OFF);
}
return 0;
}
static ssize_t led_read(struct file *filp,char __user* buf,size_t cnt,loff_t *offt){
return 0;
}
static int led_release(struct inode *inode,struct file *filp){
return 0;
}
//dtsled设备操作函数集初始化
static struct file_operations dtsled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release
};
/*驱动入口函数*/
static int __init led_init(void){
struct property *ledproperty;
const char* str;
u32 val;
u32 regdata[14];
int retvalue;
/*获取设备树属性*/
dtsled.nd = of_find_node_by_path("/yqxled");//获取设备树节点
if(dtsled.nd == NULL){
printk("节点无法找到\r\n");
return -EINVAL;
} else {
printk("成功找到节点\r\n");
}
ledproperty = of_find_property(dtsled.nd,"compatible",NULL);//读取兼容性属性值
if(ledproperty == NULL){
printk("设备兼容性属性无法找到\r\n");
} else {
printk("设备兼容性属性为:%s\r\n",(char *)ledproperty->value);
}
retvalue = of_property_read_string(dtsled.nd,"status",&str);//读取status属性
if(retvalue < 0){
printk("数据读取失败\r\n");
} else {
printk("status = %s\r\n",str);
}
retvalue = of_property_read_u32_array(dtsled.nd,"reg",regdata,10);
if(retvalue<0){
printk("读取reg属性失败\r\n");
return -EINVAL;
} else {
printk("读取reg属性成功\r\n");
}
/*虚拟地址映射*/
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd,0);
SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd,1);
SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd,2);
GPIO1_DR = of_iomap(dtsled.nd,3);
GPIO1_GDIR = of_iomap(dtsled.nd,4);
/*初始化时钟*/
val = readl(IMX6U_CCM_CCGR1);
val &=~(3<<26);
val |= (3<<26);
writel(val,IMX6U_CCM_CCGR1);
/*初始化IO复用功能*/
writel(5,SW_MUX_GPIO1_IO03);
/*设置IO电气属性*/
writel(0x10b0,SW_PAD_GPIO1_IO03);
/*设置IO03为输出*/
val = readl(GPIO1_GDIR);
val &= ~(1<<3);
val |= (1<<3);
writel(val,GPIO1_GDIR);
/*默认关闭led,电位高电位*/
val = readl(GPIO1_DR);
val |=(1<<3);
writel(val,GPIO1_DR);
/*注册字符设备驱动*/
/*注册字符设备驱动*/
if(dtsled.major){
//给定了主设备号,手动申请设备号
dtsled.devid = MKDEV(dtsled.major,0);//使用MKDEV()构建设备号,次设备号选0
register_chrdev_region(dtsled.devid,DEVICE_CNT,DEVICE_NAME);
} else {
alloc_chrdev_region(&dtsled.devid,0,DEVICE_CNT,DEVICE_NAME);
dtsled.major = MAJOR(dtsled.devid);
dtsled.minor = MINOR(dtsled.minor);
}
printk("设备号成功申请:MAJOR:%d,MINOR:%d\r\n",dtsled.major,dtsled.minor);
/*初始化cdev;注册设备*/
cdev_init(&dtsled.cdev,&dtsled_fops);
cdev_add(&dtsled.cdev,dtsled.devid,DEVICE_CNT);
/*自动创建设备节点*/
/*1.创建类*/
dtsled.class = class_create(THIS_MODULE,DEVICE_NAME);
if(IS_ERR(dtsled.class)){
return PTR_ERR(dtsled.class);
}
/*2.创建设备*/
dtsled.device = device_create(dtsled.class,NULL,dtsled.devid,NULL,DEVICE_NAME);
if(IS_ERR(dtsled.device)){
return PTR_ERR(dtsled.device);
}
return 0;
}
/*驱动出口函数*/
static void __exit led_exit(void){
u32 val;
/*释放内存前关闭led*/
val = readl(GPIO1_DR);
val |=(1<<3);
writel(val,GPIO1_DR);
/*释放虚拟内存*/
iounmap(SW_MUX_GPIO1_IO03);
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_GDIR);
iounmap(GPIO1_DR);
/*删除设备节点*/
device_destroy(dtsled.class,dtsled.devid);
/*删除类*/
class_destroy(dtsled.class);
/*删除设备*/
cdev_del(&dtsled.cdev);
/*释放设备号*/
unregister_chrdev_region(dtsled.devid,DEVICE_CNT);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yqx");
四.pinctrl 和 gpio 子系统实验
上一章我们编写了基于设备树的 LED 驱动,但是驱动的本质还是没变,都是配置 LED 灯所使用的 GPIO 寄存器,驱动开发方式和裸机基本没啥区别。 Linux 是一个庞大而完善的系统,尤其是驱动框架,像 GPIO 这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则就相当于你买了一辆车,结果每天推着车去上班。 Linux 内核提供了 pinctrl 和 gpio 子系统用于GPIO 驱动,本章我们就来学习一下如何借助 pinctrl 和 gpio 子系统来简化 GPIO 驱动开发。
其本质是使用NXP中为我们写好的linux内核中的GPIO框架来操控GPIO端口
1.pinctrl子系统
大多数 SOC 的 pin 都是支持复用的,比如 I.MX6ULL 的 GPIO1_IO03 既可以作为普通的GPIO 使用,也可以作为 I2C1 的 SDA 等等。此外我们还需要置 pin 的电气特性,比如上/下拉、速度、驱动能力等等。传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。 pinctrl 子系统就是为了解决这个问题而引入的, pinctrl子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成, pinctrl 子系统源码目录为drivers/pinctrl。
2.gpio子系统
上一小节讲解了 pinctrl 子系统, pinctrl 子系统重点是设置 PIN(有的 SOC 叫做 PAD)的复用和电气属性,如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。 gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取GPIO 的值等。 gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO, Linux 内核向驱动开发者屏蔽掉了 GPIO的设置过程,极大的方便了驱动开发者使用 GPIO。
1.gpio子系统API函数:
1、 gpio_request 函数
gpio_request 函数用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使
用 gpio_request进行申请,函数原型如下:
int gpio_request(unsigned gpio, const char *label)
2、 gpio_free 函数
如果不使用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放。函数原型如下:
void gpio_free(unsigned gpio)
3、 gpio_direction_input 函数
int gpio_direction_input(unsigned gpio)
4、 gpio_direction_output 函数
int gpio_direction_output(unsigned gpio, int value)
5、 gpio_get_value 函数
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
6、 gpio_set_value 函数
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
2.与gpio子系统相关的of函数
1、 of_gpio_named_count 函数
of_gpio_named_count 函数用于获取设备树某个属性里面定义了几个 GPIO 信
息,要注意的是空的 GPIO 信息也会被统计到
int of_gpio_named_count(struct device_node *np, const char *propname)
2、 of_gpio_count 函数
和 of_gpio_named_count 函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属性的 GPIO 数量,而 of_gpio_named_count 函数可以统计任意属性的 GPIO 信息,函数原型如下所示:
int of_gpio_count(struct device_node *np)
3、 of_get_named_gpio 函数
此函数获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用
GPIO 编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息
转换为对应的 GPIO 编号,此函数在驱动中使用很频繁!函数原型如下:
int of_get_named_gpio(struct device_node *np,const char *propname,int index)
3.实验编写
1.设备树:
yqxgpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "yqx,yqxgpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_yqxgpioled>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
2.驱动程序编写
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define DEV_NAME "yqxgpioled"
#define DEV_CNT 1
#define LED_ON 1
#define LED_OFF 0
typedef struct gpioled{
dev_t devid;//设备号
struct cdev cdev;//cdev设备
struct class* class;//类
struct device* device;//设备
int major;//主设备号
int minor; //副设备号
struct device_node *nd; //设备树节点
int led_gpio;//led_gpio编号
} gpioled;
gpioled yqxgpioled;
/*设备打开函数*/
static int led_open(struct inode *inode,struct file *filp){
filp->private_data = &yqxgpioled;
return 0;
}
/*设备关闭函数*/
static int led_close(struct inode *inode,struct file *filp){
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){
gpioled *yqx_dev = filp->private_data;
unsigned char databuf[1];
unsigned char ledstat;
int retvalue;
retvalue = copy_from_user(databuf,buf,cnt);
if(retvalue < 0){
printk("访问内核失败\r\n");
return -EINVAL;
}
ledstat = databuf[0];
if(ledstat == LED_ON){
gpio_set_value(yqx_dev->led_gpio,0);//亮灯
}else {
gpio_set_value(yqx_dev->led_gpio,1);//灭灯
}
return 0;
}
/*初始化操作函数结构体*/
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_close,
.write = led_write,
.read = led_read,
};
/*驱动入口函数*/
static int __init led_init(void){
int ret = 0;
/*设置LED所使用的GPIO*/
/*1.获取设备节点:gpioled*/
yqxgpioled.nd = of_find_node_by_path("/yqxgpioled");
if(yqxgpioled.nd == NULL){
printk("访问gpio节点失败\r\n");
return -EINVAL;
} else {
printk("gpio设备节点已找到\r\n");
}
/*获取gpio编号*/
yqxgpioled.led_gpio = of_get_named_gpio(yqxgpioled.nd,"led-gpio",0);
if(yqxgpioled.led_gpio< 0){
printk("获取gpio编号失败\r\n");
return -EINVAL;
}
printk("gpio设备编号为:%d\r\n",yqxgpioled.led_gpio);
/*设置gpio1_io03为输出*/
ret = gpio_direction_output(yqxgpioled.led_gpio,1);
if(ret < 0){
printk("gpio1_3设置IO输出失败\r\n");
}
/*1.分配设备号*/
if(yqxgpioled.major){
yqxgpioled.devid=MKDEV(yqxgpioled.major,0);
register_chrdev_region(yqxgpioled.devid,DEV_CNT,DEV_NAME);
} else {
alloc_chrdev_region(&yqxgpioled.devid,0,DEV_CNT,DEV_NAME);
yqxgpioled.major = MAJOR(yqxgpioled.devid);
yqxgpioled.minor = MINOR(yqxgpioled.devid);
}
printk("major:%d,minor:%d\r\n",yqxgpioled.major,yqxgpioled.minor);
/*2.注册设备*/
cdev_init(&yqxgpioled.cdev,&gpioled_fops);
cdev_add(&yqxgpioled.cdev,yqxgpioled.devid,DEV_CNT);
/*3.自动创建设备节点*/
yqxgpioled.class = class_create(THIS_MODULE,DEV_NAME);
if(IS_ERR(yqxgpioled.class)){
printk("创建类失败\r\n");
return PTR_ERR(yqxgpioled.class);
}
yqxgpioled.device = device_create(yqxgpioled.class,NULL,yqxgpioled.devid,NULL,DEV_NAME);
if(IS_ERR(yqxgpioled.device)){
printk("创建设备节点失败");
return PTR_ERR(yqxgpioled.device);
}
return 0;
}
/*驱动出口函数*/
static void __exit led_exit(void){
gpio_set_value(yqxgpioled.led_gpio,1);//灭灯
gpio_free(yqxgpioled.led_gpio);//释放gpio标号
device_destroy(yqxgpioled.class,yqxgpioled.devid);
class_destroy(yqxgpioled.class);
cdev_del(&yqxgpioled.cdev);
unregister_chrdev_region(yqxgpioled.devid,1); //注销设备号
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YQX");
五.Linux并发与竞争实验
Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源,就和共享单车一样。在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问。比如共享单车,大家按照谁扫谁骑走的原则来共用这个单车,如果没有这个并发访问共享单车的原则存在,只怕到时候为了一辆单车要打起来了。在 Linux 驱动编写过程中对于并发控制的管理非常重要,本章我们就来学习一下如何在 Linux 驱动中处理并发。
1.原子操作
首先看一下原子操作,原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。假如现在要对无符号整形变量 a 赋值,值为 3,对于 C 语言来讲很简单。
实验程序:
1.驱动程序
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define DEV_NAME "yqxgpioled"
#define DEV_CNT 1
#define LED_ON 1
#define LED_OFF 0
typedef struct gpioled{
dev_t devid;//设备号
struct cdev cdev;//cdev设备
struct class* class;//类
struct device* device;//设备
int major;//主设备号
int minor; //副设备号
struct device_node *nd; //设备树节点
int led_gpio;//led_gpio编号
atomic_t flag;//原子量
} gpioled;
gpioled yqxgpioled;
/*设备打开函数*/
static int led_open(struct inode *inode,struct file *filp){
filp->private_data = &yqxgpioled;
if(!atomic_dec_and_test(&yqxgpioled.flag)){
atomic_inc(&yqxgpioled.flag);
printk("设备正在备占用,请稍候再使用\r\n");
return -EBUSY;
}
return 0;
}
/*设备关闭函数*/
static int led_close(struct inode *inode,struct file *filp){
gpioled *yqx_dev = filp->private_data;
atomic_set(&yqx_dev->flag,1);
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){
gpioled *yqx_dev = filp->private_data;
unsigned char databuf[1];
unsigned char ledstat;
int retvalue;
retvalue = copy_from_user(databuf,buf,cnt);
if(retvalue < 0){
printk("访问内核失败\r\n");
return -EINVAL;
}
ledstat = databuf[0];
if(ledstat == LED_ON){
gpio_set_value(yqx_dev->led_gpio,0);//亮灯
}else {
gpio_set_value(yqx_dev->led_gpio,1);//灭灯
}
return 0;
}
/*初始化操作函数结构体*/
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_close