一、相关概念
-
总线地址:cpu能够访问内存的范围
可以通过cat /proc/meminfo 来查看内存条大小
-
物理地址:硬件的实际地址或绝对地址
-
虚拟地址:逻辑(基于算法的地址(软件层面的地址,假地址))地址称为虚拟地址
虚拟地址空间的大小也由操作系统决定,32位的操作系统虚拟地址空间的大小为 2^32 字节,也就是 4G,64 系统的操作系统虚拟地址空间大小为 2^64 字节
-
mmu:MMU是Memory Management Unit的缩写,中文名是内存管理单元,有时称作分页内存管理单元(英语:paged memory management unit,缩写为PMMU)。它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。虚拟地址和物理地址的映射关系存储在页表中
MMU位于CPU内,作用:
页表:
-
设备号是用来区分硬件的
linux一切皆为文件,其设备管理同样是和文件系统紧密结合。各种设备都以文件的形式存在/dev目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。
在目录/dev下都能看到鼠标,键盘,屏幕,串口等设备文件,硬件要有相对应的驱动,那么open怎样区分这些硬件呢? 依靠文件名与设备号。
主设备号和次设备号
设备号分为:主设备号:用来区分不同种类的设备
次设备号:用来区分同种类型的多个设备
驱动链表:管理所有设备的驱动
1、添加:在我们编写完驱动程序,加载到内核
2、查找:调用驱动程序,用户空间去open
3、驱动插入链表的顺序由设备号检索
-
二、gpio驱动编写
cat /proc/iomem 获取虚拟地址所对应的物理地址
pinout 获取对应芯片型号
-
通过阅读芯片手册我们可以得知,写pin4的gpio驱动我们需要使用GPFSEL0、GPSET0、GPCLR0寄存器,且每个寄存器占4位
GPFSEL0:GPIO Function Select 功能选择 输出或输入(GPIO Function Select Registers)
000 = GPIO Pin 9 is an input
001 = GPIO Pin 9 is an output
由该部分可以得知,我们对pin4进行操作就是对FSEL4进行操作,也是对12-14位进行操作,并且为001时为输出模式。
GPSET0: GPIO Pin Output Set; gpio输出1设置
GPCLR0: GPIO Pin Output Clear;gpio输出0设置
-
因为代码操作的是虚拟地址,而我们需要对物理地址进行操作,所以我们需要使用ioremap函数进行物理地址和虚拟地址之间的相互转换
函数原型:void *ioremap(unsigned long phys_addr, unsigned long size)
phys_addr:要映射的起始的IO物理地址;
size:要映射的空间的大小;
因为程序在编译过程中会对程序进行优化,所以我们需要使用volatile关键字来避免程序对该操作的优化
volatile的作用是作为指令关键字,确保本条 指令不会因编译器的优化而省略,且要求每次直接读值
volatile unsigned int *GPFSEL0 = (volatile unsigned int*)ioremap(0xfe200000,4);
volatile unsigned int *GPCLR0 = (volatile unsigned int*)ioremap(0xfe200028,4);
volatile unsigned int *GPSET0 = (volatile unsigned int*)ioremap(0xfe20001C,4);
进行了地址映射,我们还需要解除映射
void iounmap(void* addr)//取消ioremap所映射的IO地址
iounmap(GPFSEL0); //放入exit函数中
iounmap(GPSET0);
iounmap(GPCLR0);
-
由芯片手册看出我们需要对32位中的其中几位,如果我们通过
GPSET0 = 0000000000000....0000..0
该方式进行赋值的话,这样对于我们的检查和编写都会造成很大的难度。所以我们使用位操作(|、&)。
将pin4引脚设置为输出模式
*GPFSEL0 &= ~(0x6 << 12);//12-14 13、14置0
*GPFSEL0 |= (0x1 << 12);//12置1
将pin4引脚值1
*GPSET0 |= (0x1 << 4);
将pin4引脚置0
*GPCLR0 |= (0x1 << 4);
我们设置的是输出模式,所以我们需要获取上层的指令,可以通过copy_from_user函数
unsigned long copy_from_user(void * to, const void __user * from, unsigned long n)
第一个参数to是
内核空间的数据目标地址指针,
第二个参数from是
用户空间的数据源地址指针,
第三个参数n是
数据长度。
此函数将from指针指向的用户空间地址开始的连续n个字节的数据产送到to指针指向的内核空间地址,简言之是用于将用户空间的数据传送到内核空间
-
内核驱动框架
#include <linux/fs.h> //file_operations声明
#include <linux/module.h> //module_init module_exit声明
#include <linux/init.h> //__init __exit 宏定义声明
#include <linux/device.h> //class devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //ioremap iounmap的头文件
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; //设备号
static int major = 231; //主设备号
static int minor = 0; //次设备号
static char *module_name = "pin4"; //模块名
//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
return 0;
}
//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
printk("pin4_write\n");
return 0;
}
//led_read函数
static ssize_t pin4_read (struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
printk("pin4_read\n");
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
.read = pin4_read,
};
int __init pin4_drv_init(void) //真实驱动入口
{
int ret;
devno = MKDEV(major, minor); //创建设备号
ret = register_chrdev(major, module_name, &pin4_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
pin4_class=class_create(THIS_MODULE, "myfirstdemo"); //用代码在dev自动生成设备
pin4_class_dev =device_create(pin4_class, NULL, devno, NULL, module_name); //创建设备文件
return 0;
}
void __exit pin4_drv_exit(void)
{
device_destroy(pin4_class, devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //入口,内核加载该驱动的时候,这个宏被使用,在insmod时候就会调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
上层应用程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd;
fd = open("/dev/pin4",O_RDWR);
if(fd < 0){
printf("open failed\n");
perror("reson");
}else{
printf("open success\n");
}
fd = write(fd,1,1);//写一个字符'1',写一个字节
return 0;
}
驱动模块的编译以及测试
将我们写好的驱动放入/drivers/char/文件夹下,并且对Makefile进行添加驱动
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7l make -j4 modules
sudo chmod 666 /dev/pin4
此时就可以运行上层应用程序进行测试了,当程序运行结束后,通过dmesg进行驱动的输出检测
出现以上两行信息,及说明我们的模块是正确的、可以使用的。
三、进行pin4口的驱动编写
将以上我们所查阅的资料以及准备的模板进行整合
内核驱动
//字符设备驱动框架
#include <linux/fs.h> //file_operations声明
#include <linux/module.h> //module_init module_exit声明
#include <linux/init.h> //__init __exit 宏定义声明
#include <linux/device.h> //class devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //ioremap iounmap的头文件
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; //设备号
static int major = 231; //主设备号
static int minor = 0; //次设备号
static char *module_name = "pin4"; //模块名
volatile unsigned int *GPFSEL0 = NULL;
volatile unsigned int *GPSET0 = NULL;
volatile unsigned int *GPCLR0 = NULL;
//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
//printk("pin4_open\n"); //内核的打印函数和printf类似
*GPFSEL0 &= ~(0x6 << 12);//12-14 13、14置0
*GPFSEL0 |= (0x1 << 12);//12置1
printk("pin4 set output success\n");
return 0;
}
//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
//printk("pin4_write\n");
char cmd_user;
copy_from_user(&cmd_user,buf,count);
printk("get value\n");
if(cmd_user == 1){
*GPSET0 |= (0x1 << 4);
printk("pin4 set high\n");
}
if(cmd_user == 0){
*GPCLR0 |= (0x1 << 4);
printk("pin4 set low\n");
}
return 0;
}
//led_read函数
static ssize_t pin4_read (struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
printk("pin4_read\n");
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
.read = pin4_read,
};
int __init pin4_drv_init(void) //真实驱动入口
{
int ret;
devno = MKDEV(major, minor); //创建设备号
ret = register_chrdev(major, module_name, &pin4_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
pin4_class=class_create(THIS_MODULE, "myfirstdemo"); //用代码在dev自动生成设备
pin4_class_dev =device_create(pin4_class, NULL, devno, NULL, module_name); //创建设备文件
GPFSEL0 = (volatile unsigned int*)ioremap(0xfe200000,4);
GPCLR0 = (volatile unsigned int*)ioremap(0xfe200028,4);
GPSET0 = (volatile unsigned int*)ioremap(0xfe20001C,4);
printk("pin4driver success\n");
return 0;
}
void __exit pin4_drv_exit(void)
{
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
device_destroy(pin4_class, devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //入口,内核加载该驱动的时候,这个宏被使用,在insmod时候就会调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
上层应用
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd;
int cmd;
fd = open("/dev/pin4",O_RDWR);
if(fd < 0){
printf("open failed\n");
perror("reson");
}else{
printf("open success\n");
}
printf("请输入0 / 1\n 0:设置pin4为低电平\n 1:设置pin4为高电平\n");
scanf("%d",&cmd);
if(cmd == 0){
printf("pin4设置成低电平\n");
}else if(cmd == 1){
printf("pin4设置成高电平\n");
}
fd = write(fd,&cmd,1);//写一个字符'1',写一个字节
return 0;
}
当我们编写、编译、加载完成后,可以通过wiringPi中的gpio readall指令进行检查驱动是否正常运行。