刚买的树莓派到了,拿到后的第一件事当然是点灯啦!今天就使用树莓派2B通过编写驱动的形式点亮LED小灯,废话少说,直接开干!
树莓派需要装载有Linux系统,并且已经安装了Makefile和vim,并且内核中存在build文件夹,如果build文件夹不存在的话可以读一下我之前的文章:
解决Linux内核目录下没有build文件夹的问题
在正式编写程序之前我们需要先知道几个比较重要的概念,比如树莓派比较重要的三个地址(地址总线、物理地址、虚拟地址),怎么通过移位操作寄存器,如何查看芯片手册,如何查看电路原理图等等知识,现在就带上你的好奇心咱们一起开始学习一下一些基本概念!
在设备驱动程序的开发过程中,我们通常需要跟硬件地址打交道。在不带操作系统的嵌入式平台上(如51单片机,STM32等),我们是可以直接操作的是硬件物理地址。
但是在基于Linux的嵌入式平台上,我们想要直接控制硬件的地址基本是不可能的。操作系统操作的是虚拟地址,因此我们只有将物理地址映射成为虚拟地址才能对硬件进行操作。
我使用的硬件平台是树莓派Raspberry Pi 2 Model B,处理器芯片是BCM2836。我想用树莓派直接控制引脚电平高低(简单的GPIO操作)。官方没有提供BCM2837芯片手册,但是提供了BCM2835芯片手册。BCM2837架构是在BCM2835上做了改变,但是基本不变。BCM2835下载地址
我们从芯片手册中可以看出三个地址,分别是总线地址、ARM虚拟地址、ARM物理地址。我们下面对这三个地址进行一 一介绍。
总线地址,顾名思义,是与总线相关的,就是总线的地址线或在地址周期上产生的信号, 外设使用的是总线地址。
物理地址与总线地址之间的关系是由系统的设计决定的。在x86平台上,物理地址与PCI总线地址是相同的。 在其他平台上,也许会有某种转换,通常是线性的转换。
在存储器里以字节位单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址(Physical Address),又叫实际地址或绝对地址。
我们在对Linux系统中是无法直接对物理地址进行操作的,我们操作的都是虚拟地址,而虚拟地址和物理地址之间存在映射关系,具体如下图所示:
物理内存只有 512MB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理。
Linux 内核启动的时候会初始化 MMU
,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址 。
在Linux中,虚拟地址和虚拟地址之间的映射是通过ioremap
和 iounmap
函数实现的。
虚拟地址又称为逻辑地址,基于算法实现的软件层面的地址,物理地址通过MMU(分页管理系统)映射后得到的就是虚拟地址,虚拟地址也是我们在编写程序时使用最多的地址。
我们通过上面的概念可以了解到虚拟地址向物理地址的映射是通过ioremap
和 iounmap
这两个函数实现的。
ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h
文件中,定义如下:
#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));
}
ioremap 是个宏,有两个参数:cookie
和 size
,真正起作用的是函数__arm_ioremap
,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:
phys_addr:
要映射给的物理起始地址。size:
要映射的内存空间大小。mtype:
ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。返回值:
__iomem 类型的指针,指向映射后的虚拟空间首地址。卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原型如下:
void iounmap (volatile void __iomem *addr)
iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。
我们想要控制一个LED实现亮灭的操作其实就是控制树莓派开发板上的一个GPIO输出高低电平。
而对于树莓派这种具有操作系统的嵌入式来说,我们无法直接对硬件地址进行操作,我们需要通过虚拟地址映射到物理地址上去。而GPIO 的操作就需要用到这种地址映射,进而对相应的寄存器进行操作。
在ARM架构的SOC中,所有的外围设备都被映射到内存中,所有我们要想控制GPIO的话,我们需要首先知道GPIO的物理地址,进而通过地址偏移的操作获得虚拟地址。
那我们如何知道硬件对应的物理地址呢?这就需要我们拿到芯片手册和电路原理图了,芯片手册的下载地址我上面已经给出,还没拿到芯片手册的可以向上翻一下。
注意: 目前市面上流传出来的芯片手册只有BCM2835这一本,这本手册为树莓派B和B+的手册,但是通过寻找资料得知BCM2837其实就是BCM2836的主频升级版;我又去看BCM2836的资料,得知这只不过是BCM2835从32位到64位的升级版;我又去看BCM2835的芯片资料,所以对于树莓派来说这个文档就可以满足你的大部分需求了。
我们查看芯片手册主要关注一下文档90页Register View
中的几个内容
注意: 我们在编写驱动程序的时候,IO空间的起始地址是0x3f000000
,加上GPIO的偏移量0x2000000
,所以GPIO的物理地址应该是从0x3f200000
开始的。
我们通过芯片手册知道了物理地址的起始地址,接下来我们只需要找到和GPIO相关的寄存器,然后对其进行操作即可。
通过阅读手册我们可以知道和GPIO相关的寄存器主要有三个,分别为GPFSELn、GPSETn、GPCLRn
三个寄存器的介绍主要在文档91~95页,下面是相关寄存器介绍的截图:
GPFSEL0
(寄存器名)
GPIO Function Select 0(功能选择:输入或输出);
GPSET0
(寄存器名)
GPIO Pin Output Set 0(将IO口置0);
GPSET1
(寄存器名)
GPIO Pin Output Set 1(将IO口置1);
GPCLR0
(寄存器名)
GPIO Pin Output Clear 0 (清0)下图的地址是:总线地址(并不是真正的物理地址)
我们先查看第一个寄存器的值,以此来确定我们需要的GPIO具体是被该寄存器的哪一位控制的。通过查看表Table 6-1 GPIO Register Assignment
可以确定每个GPIO对应的寄存器的哪一位。
我们看到表中第一行中内容比较多,但是其他的表中为什么没有呢?其实这是官方偷懒了,其实每一行第三列中都是该内容,即可改为如下:
000 = GPIO Pin n is an input
001 = GPIO Pin n is an output
100 = GPIO Pin n takes alternate function 0
101 = GPIO Pin n takes alternate function 1
110 = GPIO Pin n takes alternate function 2
111 = GPIO Pin n takes alternate function 3
011 = GPIO Pin n takes alternate function 4
010 = GPIO Pin n takes alternate function 5
所以我们的引脚就可以通过该表得到具体在哪一位,比如说我们想要确定GPIO4在寄存器GPFSEL0
中的哪一位,我们就可以查看表中的FSEL4
对应的行,具体如下:
加上我们上面所得通用Description
,可以确定如果我们想要对GPIO4进行配置我们需要对哪些位写入什么内容。
Description
内容:
000 = GPIO Pin 4 is an input
001 = GPIO Pin 4 is an output
100 = GPIO Pin 4 takes alternate function 0
101 = GPIO Pin 4 takes alternate function 1
110 = GPIO Pin 4 takes alternate function 2
111 = GPIO Pin 4 takes alternate function 3
011 = GPIO Pin 4 takes alternate function 4
010 = GPIO Pin 4 takes alternate function 5
比如我现在需要将GPIO4配置成输出引脚,通过上面的Description
和表格我们可以知道我们需要将14
、13
、12
位依次写入001
这样我们就明白了我们的意图,那么用代码如何实现呢?
大家可以先思考一下自己的想法,我这里给出一个思路,供大家参考:
//配置pin4引脚为输出引脚
*GPFSEL0 &=~(0x6 <<12); // 把bit13 、bit14置为0
//0x6是110 <<12左移12位 ~取反 &按位与
*GPFSEL0 |=~(0x1 <<12); //把12置为1 |按位或
代码讲解:
我们知道我们对一个特定的位想要写入1,那么我们就需要使用该位为1,其他位为0的数值或
上原来的数值,具体如下:
比如原来存在一个四位的2进制数:0101
我们现在想要该数的从高位数第三位变成1
我们是不是可以使用 0101 | 0010
最后我们得到:0111
达到目的!
那如果我们想要将某位变成0,其他位不变该如何操作呢?
我们可以使用只有该位为0,其他位都为1的数与
上原来的数,具体如下:
比如原来存在一个四位的2进制数:0110
我们现在想要该数的从高位数第三位变成0
我们是不是可以使用 0100 & 1101
最后我们得到:0100
达到目的!
知道了这个思想我们再来看上面的两个语句就不难了,具体如下:
0x7E20 &=~(0x6 <<12);//这里为了方便,我们使用的是16位的数
//实际上树莓派中是32位的,原理相同
0x6即为2进制的110,0x6左移12位可以得到:
0110 0000 0000 0000
我们对上面的数值进行取反,得到:
1001 1111 1111 1111
通过我上面举的简单的例子我们知道,任何数与该数值进行与操作都会将14、13位变成0
其他位保持不变,所以0x7E20 &=~(0x6 <<12)操作如下:
0111 1110 0010 0000 & 1001 1111 1111 1111
得到:0001 1110 0010 0000
达到目的!
这样我们就成功的将14
、13
位变成了0
,下一步就是将12
位变成1
即可,将12
位变成1
是通过下面的语句实现的:
*GPFSEL0 |=~(0x1 <<12); //把12置为1 |按位或
这句应该比较好理解,配合我上面举得例子,大家可以自己思考一下为什么这么写!
至此,我们成功配置好了寄存器GPFSEL0
,下一步我们还有两个寄存器需要配置,另外两个寄存器的配置和这个基本相同,我就不再做详细讲解了,大家可以自己尝试配置一下,我后面会直接给出代码!
至此我们就得到了GPIO对应的虚拟地址,下一步就是将虚拟地址映射到物理上。(不知道如何得到的可以看章节查看物理地址
)
这里我们就需要用到上面基础知识里面讲到的Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。用到了一个函数ioremap
:
//物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问
GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4); //4是4个字节
具体ioremap
第一个参数需要填什么是需要大家查看下面的这个表的!
想必看到这里大家应该就比较清楚我们是如何对LED相关寄存器操作,下一步就是将我们对寄存器的代码加入到框架中,学到现在Linux驱动的框架我就不需要再啰嗦了吧,直接上代码!
/* 打开设备*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*关闭/释放设备*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 从设备读取数据 */
static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos)
{
return 0;
}
/*向设备写数据 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret = 0;
// printk("chrdevbase_write\r\n");
ret = copy_from_user(writebuf, buf, count);
if (ret == 0)
{
printk("kernel recevdata:%s\r\n", writebuf);
}
else
{
}
return 0;
}
/*字符设备,操作集合*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.release = chrdevbase_release,
.read = chrdevbase_read,
.write = chrdevbase_write,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
}
/*驱动出口函数*/
static void __exit chrdevbase_exit(void)
{
printk("chrdevbase_exit\r\n");
/*注销字符设备*/
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
/* LICENSE和作者信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jamesbin");
首先我们需要在入口函数中创建设备号、创建类、映射地址等操作,所以我们的init
可以这样写:
static int __init led_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(0x3f200000,4);
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
printk("insmod driver pin4 success\n");
return 0;
}
在出口函数中对我们申请到的资源和映射的内存进行释放,具体代码如下:
void __exit led_exit(void)
{
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0); //卸载驱动时释放地址映射
device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动
}
写完入口和出口函数下一步我们就需要开始编写操作函数了,即open
、write
等,具体代码如下,不懂的话可以看代码中的注释!
led_drv.c
#include //file_operations声明
#include //module_init module_exit声明
#include //__init __exit 宏定义声明
#include //class devise声明
#include //copy_from_user 的头文件
#include //设备号 dev_t 类型声明
#include //ioremap iounmap的头文件
static struct class *led_class;
static struct device *led_class_dev;
static dev_t devno; //设备号
static int major =231; //主设备号
static int minor =0; //次设备号
static char *module_name="led_drv"; //模块名
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
//这三行是设置寄存器的地址
//volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
//led_open函数
static int led_open(struct inode *inode,struct file *file)
{
printk("led_open\n"); //内核的打印函数和printf类似
//配置pin4引脚为输出引脚
*GPFSEL0 &=~(0x6 <<12); // 把bit13 、bit14置为0
//0x6是110 <<12左移12位 ~取反 &按位与
*GPFSEL0 |=(0x1 <<12); //把12置为1 |按位或
return 0;
}
//read函数
static int led_read(struct file *file,char __user *buf,size_t count,loff_t *ppos)
{
printk("led_read\n"); //内核的打印函数和printf类似
return 0;
}
//led_write函数
static ssize_t led_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int usercmd;
printk("led_write\n"); //内核的打印函数和printf类似
//获取上层write函数的值
copy_from_user(&usercmd,buf,count); //将应用层用户输入的指令读如usercmd里面
//根据值来操作io口,高电平或者低电平
if(usercmd == 1){
printk("set 1\n");
*GPSET0 |= (0x1 << 4);
}
else if(usercmd == 0){
printk("set 0\n");
*GPCLR0 |= (0x1 << 4);
}
else{
printk("undo\n");
}
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.read = led_read,
};
//static限定这个结构体的作用,仅仅只在这个文件。
int __init led_init(void) //真实的驱动入口
{
int ret;
devno = MKDEV(major,minor); //创建设备号
ret = register_chrdev(major, module_name,&led_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
led_class=class_create(THIS_MODULE,"myfirstdemo");//让代码在dev下自动>生成设备
led_class_dev =device_create(led_class,NULL,devno,NULL,module_name); //创建设备文件
GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
printk("insmod driver led success\n");
return 0;
}
void __exit led_exit(void)
{
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0); //卸载驱动时释放地址映射
device_destroy(led_class,devno);
class_destroy(led_class);
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(led_init); //入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数
module_exit(led_exit);
MODULE_LICENSE("GPL");
至此我们的LED驱动程序就已经编写完成了!
Makefile
KVERSION = $(shell uname -r)
KERN_DIR = /lib/modules/$(KVERSION)/build
all:
make -C $(KERN_DIR) M=`pwd` modules
led_test:led_test.c
gcc led_test.c -o led_test
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm led_test
obj-m += led_drv.o
led_test.c
#include
#include //open
/*
* led_test on
* led_test off
*/
int main(int argc, char **argv)
{
int fd;
int val = 0;
if(argc != 2)
{
printf("Usage: \n");
printf("%s: \n" , argv[0]); //<> must have on | off
return 0;
}
fd = open("/dev/led_drv",O_RDWR);
if( fd < 0 )
{
printf("can not open\n");
}
if( strcmp(argv[1],"on") == 0 )
{
val = 1;
printf("led on\n");
}
else if( strcmp(argv[1],"off") == 0 )
{
val = 0;
printf("led off\n");
}
//write(fd, &val, 4);
write(fd, &val,4); //val类型是int 所以 写4
}
make
编译drv
文件:
make
编译led_test
文件
make led_test
装载驱动
sudo insmod led_drv.ko
测试程序
sudo ./led_test on //开灯
sudo ./led_test off //关灯
至此你的小灯应该已经被你点亮了!成功的话记得一键三连哦!!!