jjzjj

树莓派高级开发之树莓派博通BCM2835芯片手册导读与及“相关IO口驱动代码的编写”

嵌入式软件工程师 2023-07-13 原文

首先我们要知道,驱动的两大利器:电路图(通过电路图去寻找寄存器)和芯片手册

一、寄存器的介绍

芯片手册第六章的89页,GPIO有41个寄存器,所有访问都是32位的。Description是寄存器的功能描述。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)下图的地址是:总线地址(并不是真正的物理地址)

GPFSEL0是pin0 ~ pin9的配置寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此类推,GPFSEL5就是pin50~pin53的配置寄存器。

字段名描述用法
GPFSEL0GPIO Function select 0,功能选择输出/输入以引脚9举例:000 = GPIO Pin 9 is an input,001 = GPIO Pin 9 is an output
GPSET0GPIO Pin output Set 0,输出00 = No effect ,1 = Set GPIO pin n
GPSET1GPIO Pin output set 1,输出10 = No effect ,1 = Set GPIO pin n
GPCLR0GPIO Pin output clear 0,清00 = No effect ,1 = Clear GPIO pin n

在上面的文档里已经说的很清楚了,000是引脚输入,而001则是引脚输出,在这里要注意每个寄存器都是32位的

  • FSELn表示GPIOn,下图给出第九个引脚的功能选择示例,对寄存器的29-27进行配置,进而设置相应的功能。根据图片下方的register
    0表示0~9使用的是register 0(即GPFSEL0)这个寄存器。

  • 输出集寄存器用于设置GPIO管脚。SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。分离集和明确功能取消对读-修改-写操作的需要。GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位。
  • 输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。分隔集与清函数消除了读-修改-写操作的需要。GPCLRn是清零功能寄存器。

把pin4引脚配置为输出引脚:
FSEL4 14-12 001 我们把4引脚的14-12配置成001 GPIO Pin 4 is an output
详细操作:
只需要将GPFSL0这个寄存器的14~12位设置为001就可以了。只需要将0x6(对应的2进制是110)左移12位·然后取反再与上GPFSL0就可以将13、14这两位配置为0,然后再将0x6(对应2进制110)左移12位,然后或上GPFSL0即可将12位置1。

特别提示:进行取反后再进行按位与操作是为了不影响其他引脚

配置pin4引脚为输出引脚 bit 12-14 配置成001

31 30 ······14 13 12 11 10 9 8 7 6 5 4 3 2 1 
0  0  ······0  0  1  0  0  0 0 0 0 0 0 0 0 0 

 //配置pin4引脚为输出引脚      bit 12-14  配置成001  
  *GPFSEL0 &= ~(0x6 <<12); // 把bit13 、bit14置为0  
 //0x6是110  <<12左移12位 ~取反 &按位与
  *GPFSEL0 |= (0x1 <<12); //把12置为1   |按位或

忘了按位与和按位或的点这里

代码实现:

  *GPFSEL0 &=~(0x6 <<12); // 把13 、14置为0
  *GPFSEL0 |= (0x1 <<12); //把12置为1
  • 注意:我们配置的底层引脚对应得是BCM 寄存器第0组位FESL0–9, 这个就是在寄存器GPFSEL0里,寄存器已经分好组了
    寄存器第1组位FSEL10–19,这个在寄存器GPFSEL1里

更多的引脚对应的寄存器可以去树莓派官网进行查看
树莓派引脚查看官网

在上图中我们可以点击对应的引脚编号,就可以查看到对应的引脚的相关的信息

二、寄存器的地址问题

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

上图的尾部偏移是对的,根据GPIO的物理地址0x3f20 0000可以知道:

GPFSEL0 0x3f20 0000 //IO口的初始的物理地址,而并不是手册里面的那个总线地址
GPSET0 0x3f20 001c  //地址通过查找芯片手册里面的对应的GPSET0 的总线地址的后两位决定是1c
GPCLR0 0x3f20 0028 //地址是查找GPCLR0在芯片手册里的总线地址确定的28,所以地址后两位是28
  • 在原来框架的基础上,添加寄存器的定义
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0  = NULL;
volatile unsigned int* GPCLR0  = NULL;

完成以上代码需要搞清楚的几点

  1. 弄清楚寄存器的分组
    GPFSEL0是pin0 ~ pin9的配置寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此类推,GPFSEL5就是pin50~pin53的配置寄存器。这个由查阅芯片手册可以得知

  2. volatile关键字的使用(笔试可能会考)

  • 在此处的作用:防止编译器优化(可能是省略,也可能是更改)这些寄存器地址变量,常见于在内核中对IO口进行操作

  • 作用:确保指令不会因编译器的优化而省略,且要求每次直接读值,在这里的意思就是确保地址不会被编译器更换

  1. 如何配置寄存器的地址
    首先是在1.的基础上,在pin4_drv_init这个函数里面添加寄存器地址的配置
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);

写出以上的代码,要搞清楚以下几点
分别找到几个IO寄存器的物理地址(非常易错),弄清楚GPIO的物理地址(真实地址)
记住并不是用下面这张图的地址来对应GPIO功能选择寄存器0的地址,否则编译后运行会有段错误。

IO口的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的实际物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上,编程都是操作虚拟地址。

然后我们可以根据这个偏移值来确定寄存器的物理地址(真实的地址
可以看到寄存器GPSET0相对于GPIO物理地址的偏移值为1C。即0x3f20001C

同样的方法,寄存器GPCLR0的偏移值为28,即0x3f200028
寄存器GPFSEL0的偏移值为0,即0x3f200000

  1. 如何让引脚拉高或拉低电平
    代码实现:
if(userCmd == 1)
	{
		printk("set 1\n");
		*GPSET0 |= (0x1 << 4); //这里的1左移4位的目的就是促使寄存器将电平拉高,即变为HIGH
	}
	else if(userCmd == 0)
	{
		printk("set 0\n");
		*GPCLR0 |= (0x1 << 4); //这里的1左移4位也是一样只是为了让寄存器将电平拉低,即变为LOW
	}
	else
	{
		printk("nothing undo\n"); 
	}

引脚输出高电平:
*GPSET0 |= (0x1 << 4);
左移4位, 这里无论什么寄存器都是写1,写1并不是为某个io口去写1,而是1是驱动(SET)设置寄存器工作将bit4的电平拉高即变为高电平,为什么要进行或操作,是因为为了不影响其他引脚的状态

引脚输出低电平:
*GPCLR0 |= (0x1 << 4);
同样道理,左移4位,这里的1也并不是为了某个io口去写1,而是1是驱动(CLR)清零寄存器将电平拉低,即变为低电平,进行或操作也一样是为了不影响其他引脚的电平状态

  1. 在Linux内核的io.h头文件中声明了ioremap()函数,用来将IO内存资源映射到核心虚拟地址空间(3Gb~4GB)中,当然不用了可以将其取消映射iounmap()。这两个函数在mm/ioremap.c文件中:
开始映射:void* ioremap(unsigned long phys_addr , unsigned long size , unsigned long flags)
//用map映射一个设备意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或写入,实际上就是对设备的访问。
第一个参数是映射的起始地址
第二个参数是映射的长度
第二个参数怎么定啊?
====================
这个由你的硬件特性决定。
比如,你只是映射一个32位寄存器,那么长度为4就足够了。
(这里树莓派IO口功能设置寄存器、IO口设置寄存器都是32位寄存器,所以分配四个字节就够了)

比如:GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
	  GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
      GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
这三行是设置寄存器的地址,volatile的作用是作为指令关键字
确保本条指令不会因编译器的优化而省略,且要求每次直接读值
ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问。
 
解除映射:void iounmap(void* addr)//取消ioremap所映射的IO地址
比如:
	    iounmap(GPFSEL0);
        iounmap(GPSET0);
        iounmap(GPCLR0); //卸载驱动时释放地址映射
  1. 浅谈一下copy_from_usercopy_to_user 函数的用法
    copy_from_usercopy_to_user这两个函数相信做内核开发的人都非常熟悉,分别是将用户空间的数据拷贝到内核空间以及将内核空间中的数据拷贝到用户空间
 函数copy_from_user原型:
 copy_from_user(void *to, const void __user *from, unsigned long n)

返回值:失败返回没有被拷贝成功的字节数,成功返回0
参数详解:
1. to 将数据拷贝到内核的地址,即内核空间的数据目标地址指针
2. from 需要拷贝数据的地址,即用户空间的数据源地址指针
3. n 拷贝数据的长度(字节)
也就是将@from地址中的数据拷贝到@to地址中去,拷贝长度是n

详细了解copy_from_user和copy_to_user

三、驱动代码与应用测试代码

3.1 相关代码
底层驱动代码:

#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";   //模块名--这个模块名到时候是在树莓派的/dev底下显示相关驱动模块的名字

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");  //内核的打印函数和printf类似    
	
	//由于pin4在 14-12位,所以将14-12位分别置为001即为输出引脚,所以下面的那两个步骤分别就是将14,13置为0,12置为1
	*GPFSEL0 &= ~(0x6 << 12); //把13,14位 置为0
	*GPFSEL0 |=  (0x1 << 12); //把12位 置为1 
	
    return 0;
}

//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
	int userCmd;
	int copy_cmd;
	
	printk("pin4_write\\n");
	
	//copy_from_user(void *to, const void __user *from, unsigned long n)
	
	copy_cmd = copy_from_user(&userCmd,buf,count); //函数的返回值是,如果成功的话返回0,失败的话就是返回用户空间的字节数
	
	if(copy_cmd != 0)
	{
		printk("fail to copy from user\n");
	}

	if(userCmd == 1)
	{
		printk("set 1\n");
		*GPSET0 |= (0x1 << 4); //这里的1左移4位的目的就是促使寄存器将电平拉高,即变为HIGH
	}
	else if(userCmd == 0)
	{
		printk("set 0\n");
		*GPCLR0 |= (0x1 << 4); //这里的1左移4位也是一样只是为了让寄存器将电平拉低,即变为LOW
	}
	else
	{
		printk("nothing undo\n"); 
	}
	
    return 0;
}

static ssize_t pin4_read(struct file *file, const 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(0x3f200000,4); //由于寄存器是32位的,所以是映射4个字节,一个字节为8位
	GPSET0  = (volatile unsigned int *)ioremap(0x3f20001c,4);
	GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);
	
    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);  //真正的入口
module_exit(pin4_drv_exit);  //卸载驱动
MODULE_LICENSE("GPL v2");	

上层应用测试代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
		int fd;
		int userCmd;
		
		fd = open("/dev/pin4",O_RDWR);
		
		if(fd < 0)
		{
			printf("fail to open the pin4\n");
			perror("the reason:");
		}
		else
		{
			printf("success to open the pin4\n");
		}
			
		printf("please Input 1-HIGH,0-LOW \n");
		scanf("%d",&userCmd);

		write(fd,&userCmd,4); //这里userCmd是一个整型数,所以写的是4个字节

		return 0;
}

至于怎么在虚拟机中编译驱动文件以及如何将编译好的文件发送至树莓派,敬请关注以下博文
如何将编译好的文件发送至树莓派底下

3.2 在树莓派底下进行代码的测试与验证
相关的驱动的装载与卸载也查看驱动装载与卸载

  • 先来查看一下树莓派4号引脚的初始状态是什么

    输入1,将引脚电平变为高电平

    输入0,将引脚电平变为低电平

    到目前为止,我们经过那么多节的对驱动的学习的博文,现在终于自己终于实现了类似于wiringPi这样的一个驱动文件,我们在这里做的是引脚4的驱动,那么我们就可以按着模样来写引脚5,引脚6,甚至其他引脚的驱动,在这里想说一句,驱动代码的编写,都是基于linux内核源码来进行编写的,linux内核源码这个文件在前面的博文有,如有需要自行去下载。

学习笔记,仅供参考

树莓派高级开发之IO口驱动代码编写 优秀博文参考一
树莓派高级开发之IO口驱动代码编写 优秀博文参考二

有关树莓派高级开发之树莓派博通BCM2835芯片手册导读与及“相关IO口驱动代码的编写”的更多相关文章

  1. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  2. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  3. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  4. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  5. ruby-on-rails - 如何在 Ruby on Rails 中实现由 JSF 2.0 (Primefaces) 驱动的 UI 魔法 - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭10年前。问题1)我想知道ruby​​onrails是否有功能类似于primefaces的gem。我问的原因是如果您使用primefaces(http://www.primefaces.org/showcase-labs/ui/home.jsf),开发人员无需担心javascript或jquery的东西。据我所知,JSF是一个规范,基于规范的各种可用实现,prim

  6. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  7. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  8. ruby-on-rails - 如何为空白字段编写 rspec? [Rails3.1] - 2

    我使用rails3.1+rspec和factorygirl。我对必填字段(validates_presence_of)的验证工作正常。我如何让测试将该事实用作“成功”而不是“失败”规范是:describe"Addanindustrywithnoname"docontext"Unabletocreatearecordwhenthenameisblank"dosubjectdoind=Factory.create(:industry_name_blank)endit{shouldbe_invalid}endend但是我失败了:Failures:1)Addanindustrywithnona

  9. ruby - 为什么不能使用类IO的实例方法noecho? - 2

    print"Enteryourpassword:"pass=STDIN.noecho(&:gets)puts"Yourpasswordis#{pass}!"输出:Enteryourpassword:input.rb:2:in`':undefinedmethod`noecho'for#>(NoMethodError) 最佳答案 一开始require'io/console'后来的Ruby1.9.3 关于ruby-为什么不能使用类IO的实例方法noecho?,我们在StackOverflow上

  10. ruby-on-rails - 尝试为 Rails 中的用户名验证编写 REGEX - 2

    我正在尝试用Ruby(Rails)编写一个正则表达式,以便用户名的字符仅包含数字和字母(也没有空格)。我有这个正则表达式,/^[a-zA-Z0-9]+$/,但它似乎没有用,我在Rails中收到一个错误,说“The如果正则表达式使用多行anchor(^或$),这可能会带来安全风险。您是要使用\A和\z,还是忘记添加:multiline=>true选项?"我的user.rb模型中此实现的完整代码是:classUser我做错了什么以及如何修复此正则表达式,使其仅对数字和字母有效而不对空格有效?谢谢。 最佳答案 简短回答:使用/\A[a-z

随机推荐