树莓派BCM2835芯片手册导读以及IO口驱动代码编写

2023-10-10 20:30

本文主要是介绍树莓派BCM2835芯片手册导读以及IO口驱动代码编写,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

驱动的两大利器:电路图(通过电路图去寻找寄存器)和芯片手册

树莓派使用的是BCM2835CPU(博通),芯片手册做到哪一章就看哪一章。芯片提供了54个IO口,对应了树莓派的 BCM.

一、树莓派GPIO口介绍

根据手册知gpio在第6章,我们来看第89页

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

GPFSEL0

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

GPSET0

GPIO Pin Output Set 0:输出0

GPSET1

GPIO Pin Output Set 1:输出1

GPCLR0

GPIO Pin Output Clear 0:清零

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

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

输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果的在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。分隔集与清函数消除了读-修改-写操作的需要。GPCLRn是清零功能寄存器

二、代码的编写

编写驱动程序时,首先要知道它的地址,IO口空间的起始地址是0x3f00 0000(文档的起始地址是错误的),加上GPIO的偏移量0x200 0000,所以GPIO的物理地址应该是0x3f20 0000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。

上图尾部的偏移量是正确的,根据gpio的物理地址0x3f200 0000得到

GPFSEL0 0x3f20 0000 //IO口的初始的物理地址,而并不是手册里面的那个总线地址
GPSET0 0x3f20 001c  //地址通过查找芯片手册里面的对应的GPSET0 的总线地址的后两位决定是1c
GPCLR0 0x3f20 0028 //地址是查找GPCLR0在芯片手册里的总线地址确定的28,所以地址后两位是28

1.首先在原来的驱动框架上添加寄存器的定义

volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0  = NULL;
volatile unsigned int* GPCLR0  = NULL;

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

2.然后在pin4_drv_init这个函数里面添加寄存器地址的配置

GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);

ioremap将物理地址转换为虚拟地址

我们前面讲到了在内核里代码和上层代码访问的是虚拟地址(VA),而现在设置的是物理地址,**所以必须把物理地址转换成虚拟地址**

按位与或按位或

  1. 配置引脚4为输出引脚,为了不影响其他引脚,需要使用与运算或运算。

根据图片可知14-12bit需配置成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   |按位或

4.让引脚拉高

if(userCmd == 1){printk("set 1\n");*GPSET0 |= (0x1 << 4); //写1左移4位是让寄存器    开启置1  让bit4为高电平}else if(userCmd == 0){printk("set 0\n");*GPCLR0 |= (0x1 << 4); //写1左移4位是让清0寄存器 开启置0 让bit4为低电平}else{printk("nothing undo\n"); }

补充:ioremap用法

开始映射:void* ioremap(unsigned long phys_addr , unsigned long size , unsigned long flags)
//用map映射一个设备意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或写入,实际上就是对设备的访问。
phys_addr:要映射的起始的IO地址
size:要映射的空间的大小
flags:要映射的IO空间和权限有关的标志第二个参数怎么定?
这个由你的硬件特性决定。
比如,你只是映射一个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); //卸载驱动时释放地址映射

函数copy_from_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

三、代码整合

驱动代码

#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;
}

如何编译驱动代码并在树莓派运行

树莓派初始引脚

运行代码后

学习笔记,仅供参考

优秀博客

这篇关于树莓派BCM2835芯片手册导读以及IO口驱动代码编写的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/182894

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

Python中顺序结构和循环结构示例代码

《Python中顺序结构和循环结构示例代码》:本文主要介绍Python中的条件语句和循环语句,条件语句用于根据条件执行不同的代码块,循环语句用于重复执行一段代码,文章还详细说明了range函数的使... 目录一、条件语句(1)条件语句的定义(2)条件语句的语法(a)单分支 if(b)双分支 if-else(

MySQL数据库函数之JSON_EXTRACT示例代码

《MySQL数据库函数之JSON_EXTRACT示例代码》:本文主要介绍MySQL数据库函数之JSON_EXTRACT的相关资料,JSON_EXTRACT()函数用于从JSON文档中提取值,支持对... 目录前言基本语法路径表达式示例示例 1: 提取简单值示例 2: 提取嵌套值示例 3: 提取数组中的值注意

CSS3中使用flex和grid实现等高元素布局的示例代码

《CSS3中使用flex和grid实现等高元素布局的示例代码》:本文主要介绍了使用CSS3中的Flexbox和Grid布局实现等高元素布局的方法,通过简单的两列实现、每行放置3列以及全部代码的展示,展示了这两种布局方式的实现细节和效果,详细内容请阅读本文,希望能对你有所帮助... 过往的实现方法是使用浮动加

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav

Java实现状态模式的示例代码

《Java实现状态模式的示例代码》状态模式是一种行为型设计模式,允许对象根据其内部状态改变行为,本文主要介绍了Java实现状态模式的示例代码,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来... 目录一、简介1、定义2、状态模式的结构二、Java实现案例1、电灯开关状态案例2、番茄工作法状态案例

树莓派启动python的实现方法

《树莓派启动python的实现方法》本文主要介绍了树莓派启动python的实现方法,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录一、RASPBerry系统设置二、使用sandroidsh连接上开发板Raspberry Pi三、运

nginx-rtmp-module模块实现视频点播的示例代码

《nginx-rtmp-module模块实现视频点播的示例代码》本文主要介绍了nginx-rtmp-module模块实现视频点播,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习... 目录预置条件Nginx点播基本配置点播远程文件指定多个播放位置参考预置条件配置点播服务器 192.