一、驱动代码的开发

1.树莓派寄存器的介绍

点击查看:树莓派(bcm2835芯片手册)

GPFSEL0 GPIO Function Select 0: 功能选择 输入/输出

GPSET0 GPIO Pin Output Set 0 : 输出0
GPSET1 GPIO Pin Output Set 1 : 输出1
0 = No effect
1 = Set GPIO pin n

GPCLR0 GPIO Pin Output Clear 0: 清零
0 = No effect
1 = Clear GPIO pin n
GPCLR1 GPIO Pin Output Clear 1 :清1

每个寄存器都是32位的

例如:我们把引脚4配置位输出引脚
FSEL4 14-12 001 我们把4引脚的14-12配置成001 GPIO Pin 4 is an output

注意:我们配置的底层引脚对应得是BCM
寄存器第0组位FESL0–9
寄存器第1组位FSEL10–19


具体的引脚也可通过官方手册查找
https://pinout.xyz/pinout/pin7_gpio4

2.寄存器的地址问题

我们在编写驱动程序的时候,IO空间的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。

特别注意,BCM2708 和BCM2709 IO起始地址不同,BCM2708是0x20000000,BCM2709是0x3f000000,这是造成大部分人驱动出现“段错误”的原因。树莓派3B的CPU为BCM2709。

查看树莓派型号

cat /proc/cupinfo


此处指令看到的型号不是树莓派cpu真的型号,其真正型号应该是BCM2837,也就是IO在物理地址上的基址应该是0x3F000000。

通过查看芯片手册发GPFSEL0寄存器VC  CPU总线地址是0x7E200000,
相对基址(0x7E000000)偏移0x00200000,那么ARM物理地址也是偏移这
么多,所以GPIO的物理地址应该是从0x3f200000 开始 。

该图的尾部偏移是对的根据GPIO的物理地址0x3f200000可以知道:
GPFSEL0 0x3f200000
GPSET0 0x3f20001c
GPCLR0 0x3f200028

这里我们需要注意的是,上面的地址是物理地址,一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。

所以我们必须通过函数void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);来映射获取到对应物理地址映射的虚拟地址,来访问寄存器。

void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);ioremap宏定义在asm/io.h内:#define ioremap(cookie,size)           __ioremap(cookie,size,0)

参数:

phys_addr:要映射的起始的IO地址

size:要映射的空间的大小

flags:要映射的IO空间和权限有关的标志

该函数返回映射后的内核虚拟地址(3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

这里我们不指定flags,函数使用方法如下:(volatile的作用是为了保证地址不会因编译器的优化而省略,每次直接读取值)

GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);   //初始化设置引脚功能的寄存器的地址 GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);  // 初始化控制引脚输出‘1’的寄存器地址GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);  //初始化控制引脚输出‘0’的寄存器的地址

关于file_operations结构体:Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。

我们写驱动都需要根据不同需求选择性的对file_operations结构体中的成员进行配置:

static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,   //配置open函数.write = pin4_write,  //配置write函数};

注意:第一个 file_operations 成员owner根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 这是一个在 <linux/module.h> 中定义的宏.

最后完成的驱动代码如下:

pin4driver.c

#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; //这三行是设置寄存器的地址,volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值.//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n");  //内核的打印函数*GPFSEL0 &=~(0x6<<12);//6的二进制是110左移12位后,110对应的位置是14-12,取反后110变为001其它位为1,和GPFSEL0进行与运算后就实现只有14、13位改变为0*GPFSEL0 |=(0x1<<12);//把bit14,bit13配置成0 //把位置12变为1//配置pin4引脚为输出引脚,bit 12~14配置成100return 0;
}
//read函数
static int pin4_read(struct file *file,char __user *buf,size_t count,loff_t *ppos)
{printk("pin4_read\n");  //内核的打印函数//可以用copy_to_user()函数读取引脚return 0;
}//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{int usercmd;printk("pin4_write\n");  //内核的打印函数//获取上层write函数的值copy_from_user(&usercmd,buf,count);//根据值来操作io口,高电平或者低电平if (usercmd==1){printk("set 1\n");*GPSET0 |=(0x1 <<4); //通常pin4用1左移4位进行或运算} else if(usercmd==0){printk("set 0\n");* GPCLR0 |=(0x1<<4); }else{printk("do nothing\n");}return 0;
}
static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,.write = pin4_write,.read  = pin4_read,
};
//static限定这个结构体的作用,仅仅只在这个文件。
int __init pin4_drv_init(void)   //真实的驱动入口
{int ret;printk("ismod pin4 successed\n");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);//物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);//ioremap函数将物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);//这三行是设置寄存器的地址,volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,//且要求每次直接读值.ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问。//ioremap函数第一个参数输物理地址,第二个参数是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);  //入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

tips: 

volatile //特征修饰符 作为指令关键字作用:1.确保指令不会因编译器的优化而省略。(编译器自认为人为给的数据不行,可能会被编译器给优化掉)2.要求每次直接从寄存器读值。(寄存器随着硬件的执行可能会改变寄存器里面的数据,如果没有volatile修饰,读取的数据是原先数据的一个备份,是个老数据,数据时效性就很差。)unsigned 作用:整数分为有符号与无符号,如果要把类型声明为无符号数就需要使用unsigned来修饰(除char以外的数据类型中,默认情况下声明的整型变量都是有符号的类型),两者区别在于,有符号的数最高位的数作为符号位,无符号最高位作为值。如两个字节的short,有符号表示范围是-32768~32767,无符号范围是0~65535.

注意
为了不影响其他的引脚,配置寄存器的时候要用位操作,寄存器有32位,若想配13、14位为0,可以将二进制的110左移12位然后按位取反,在与上原来的寄存器地址,这样就不影响其他位。

上层应用代码如下:

pin4test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd,data;fd = open("/dev/pin4",O_RDWR);printf("plese input 1 or 0 //1:high 0:low\n ");scanf("%d",&data);if(fd<0){printf("open fail\n");perror("reson:");}else{printf("open successful\n");}fd=write(fd,&data,1);return 0;
}

编译驱动代码

完成驱动代码后,修改linux内核文件drivers /char下的Makefile 文件,

添加一条信息obj-m                +=pin4driver.o        (-m是模块方式编译,pin4driver.o是你自己写的驱动的文件名)

只有添加了这条信息,在编译内核的时候,才会去编译你自己写的驱动代码。

添加完成之后:

重新编译内核模块

指令为(该指令需要在内核代码文件路径下执行):ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4  modules

编译好后会在/driver/char/路径多生成一个pin4driver.ko文件

将这个.ko文件放到树莓派上

3、在树莓派上装载该驱动

将该驱动装载进树莓派:sudo insmod  pin4driver.ko

并且给予其操作权限: sudo chmod 666 /dev/pin4_moudle

可以看到驱动列表多了一个

当看见pin4driver就意味着驱动装载成功,这个时候会在根目录下的/dev/内看到一个pin4_moudle文件,这个文件的名字是在你写驱动代码时定义的模块名称

给予这个模块操作权限: sudo chmod 666 /dev/pin4_moudle

此时自己编写的驱动模块已经装载完毕。

4、测试驱动

我们可以在树莓派上编写代码来测试该驱动:

测试:

未输入之前树莓派gpio状态,pin4号脚输出为0

dmesg查看

gpio readall查看

输入1后

对应BCM4号脚输出变为1

树莓派底层IO驱动开发示例(一个简单io口驱动的开发)相关推荐

  1. 驱动开发之六 --- 一个简单的显示驱动之一 [译文]

    这个系列的文章在网上到处都是 这里也不清楚谁才是原文作者 我这里做个整理,标注一下希望大家能看的更加舒服一点 目录 (一)驱动开发一个简单的显示驱动 (二)驱动开发一个简单的显示驱动 (三)驱动开发一 ...

  2. 从零开始学习linux的I2C设备驱动框架——写一个简单的SHT20驱动

    目录 0.测试环境说明 1.设备树的修改 2.设备驱动框架 3.I2C数据传输过程 3.1 struct i2c_msg 3.2 SHT20的数据收发 4.I2C适配器超时等待时间的修改 本文资源 参 ...

  3. python小项目案例-Python小项目:快速开发出一个简单的学生管理系统

    本文根据实际项目中的一部分api 设计抽象出来,实例化成一个简单小例子,暂且叫作「学生管理系统」. 这个系统主要完成下面增删改查的功能: 包括: 学校信息的管理 教师信息的管理 学生信息的管理 根据A ...

  4. python项目开发实例-Python小项目:快速开发出一个简单的学生管理系统

    本文根据实际项目中的一部分api 设计抽象出来,实例化成一个简单小例子,暂且叫作「学生管理系统」. 这个系统主要完成下面增删改查的功能: 包括: 学校信息的管理 教师信息的管理 学生信息的管理 根据A ...

  5. 树莓派初次配置C++环境以及进行简单的hcsr04驱动

    树莓派初次配置C++环境以及进行简单的hcsr04驱动 这是我第一次使用树莓派,就想溜一下hcsr04模块,所以开整.这篇文章仅仅针对初学者的初次学习以及尝试. 当你已经拥有一块树莓派,并且成功烧写好 ...

  6. python小项目实例流程-Python小项目:快速开发出一个简单的学生管理系统

    原标题:Python小项目:快速开发出一个简单的学生管理系统 本文根据实际项目中的一部分api 设计抽象出来,实例化成一个简单小例子,暂且叫作「学生管理系统」. 这个系统主要完成下面增删改查的功能: ...

  7. (超多图)基于Android studio开发的一个简单入门小应用(超级详细!!)(建议收藏)

    基于Android studio开发的一个简单入门小应用 一.前言 二.前期准备 三.开发一个小应用 五.运行应用 一.前言 在暑假期间,我学习JAVA基础,为了能早日实现自己用代码写出一个app的& ...

  8. [stm32] 一个简单的stm32vet6驱动的天马4线SPI-1.77寸LCD彩屏DEMO

    书接上文<1.一个简单的nRF51822驱动的天马4线SPI-1.77寸LCD彩屏DEMO> 我们发现用16MHz晶振的nRF51822驱动1.77寸的spi速度达不到要求 本节主要采用7 ...

  9. php怎么做考勤行事例,PHP开发制作一个简单的活动日程表Calendar

    材料取之深入PHP与JQuery开发,这本书实际上就是讲述一个活动日程表. 此文章适合从其它语言(如java,C++,python等)转到php,没有系统学习php,或者是php初学者,已经对程序有较 ...

  10. 如何用php做每天日程安排,PHP开发制作一个简单的活动日程表Calendar,日程表calendar...

    PHP开发制作一个简单的活动日程表Calendar,日程表calendar 材料取之深入PHP与JQuery开发,这本书实际上就是讲述一个活动日程表. 此文章适合从其它语言(如java,C++,pyt ...

最新文章

  1. 微信小程序底部导航Tabbar
  2. 数据库中关于convert的参数学习(转化函数用法)
  3. [原创] Easy SysLite V1.2 (2016.5.29更新,新增加WIN10支持,一个程序适配所有系统减肥)...
  4. 使用alipaySDK编译时找不到openssl/asn1.h文件的解决办法(初探)
  5. 朴素贝叶斯法---朴素贝叶斯法的参数估计
  6. 阿里云服务器下安装LAMP环境(CentOS Linux 6.3) 安装与配置 Apache 服务
  7. 为什么码农要了解业务呢?网友:不是敲代码就好了吗?
  8. 情人节海报模板,甜到牙疼!
  9. ci php做记录删除,PHP CI APC 使用记录
  10. [zz]为小米创建虚拟机路由器
  11. memset 函数使用
  12. 微软最有价值专家(MVP)四连任
  13. java常用api-字符串
  14. 软件工程专业的大三学生经历和感悟
  15. 域名备案和网站备案是一个意思吗?
  16. 学习游戏服务器编程进阶篇之全球同服技术架构
  17. IPv6路由信息的序号
  18. PyTorch神经网络框架
  19. 扣血抖动和FPS显示
  20. 【Linux】云服务器的购买与Linux远程连接

热门文章

  1. win10系统经常遇到资源管理器卡死
  2. CADSoftTools CADEditorX v15.0 Crack
  3. 优化苦难,新手站长说说之SEO优化过程中原创内容怎么去进行
  4. 风电滑环 风力发电机滑环 导电环 集电环
  5. 【王佩丰】PowerPoint2010视频教程 2
  6. 项目场景:对接支付宝支付,沙箱环境提示:支付存在钓鱼风险!防钓鱼网站的方法
  7. k型热电偶检测温度c语言程序,[请教]普中51单片机与max6675芯片用k型热电偶检测温度的问题...
  8. java tick_java.time.Clock.tick()方法
  9. SHBrowseForFolder 打开默认路径
  10. 如何用pycharm编译器打包,最简单的方法