阅读芯片手册
一、树莓派GPIO对应的寄存器介绍
二、代码的编写
- ioremap函数 将物理地址映射到内核的虚拟地址空间
- iounmap函数 解除通过ioremap函数映射关系
- copy_from_user函数 从用户空间复制数据到内核空间
- copy_to_user函数 将数据从内核空间复制到用户空间
阅读芯片手册
由于我们要进行引脚驱动的开发,所以我们直接查看第六章的内容。
查看树莓派CPU型号
要编写对IO口进行操控,我们首先需要去阅读芯片手册,我使用的是树莓派 3B,所以查看的手册是BCM2835,查看CPU型号可以用这个指令来查看:
cat /proc/cpuinfo
具体的引脚也可通过官方手册查找
一、树莓派GPIO对应的寄存器介绍
Register View
注意:这里的地址是总线地址不是物理地址要分清
GPFSEL0是pin0 ~ pin9的配置寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此类推,GPFSEL5就是pin50~pin53的配置寄存器。
Register | Description |
---|---|
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 Pin Output Set Registers (GPSETn)
概要(SYNOPSIS):
输出集寄存器用于设置GPIO管脚。SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。分离集和明确功能取消对读-修改-写操作的需要。GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位
GPIO Pin Output Clear Registers (GPCLRn)
概要(SYNOPSIS):
输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果的在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。分隔集与清函数消除了读-修改-写操作的需要。GPCLRn是清零功能寄存器
二、代码的编写
编写驱动程序时,首先要知道它的地址,IO口空间的起始地址是0x3f00 0000(文档的起始地址是错误的),加上GPIO的偏移量0x200 0000,所以GPIO的物理地址应该是0x3f20 0000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。
在编写Linux内核驱动程序时,了解硬件寄存器的物理地址以及进行适当的内存映射是至关重要的。在你的情况下,GPIO的物理地址起始于0x3f200000
。
以下是编写驱动程序时可能涉及的基本步骤,假设你的驱动程序与GPIO硬件进行交互:
-
获取GPIO的物理地址: 确定GPIO硬件寄存器的物理地址。在你的情况下,它应该是
0x3f200000
。 -
内存映射: 在Linux内核中,可以使用
ioremap
函数将硬件寄存器的物理地址映射到内核虚拟地址空间。这个映射的过程负责使用MMU将物理地址映射到相应的虚拟地址上。#include <linux/io.h> // 在初始化驱动程序时进行内存映射 void *gpio_base; static int __init mydriver_init(void) { gpio_base = ioremap(0x3f200000, sizeof(struct gpio_registers)); if (!gpio_base) { printk(KERN_ERR "Failed to map GPIO registers\n"); return -ENOMEM; } // 其他初始化代码 return 0; }
在上述示例中,
sizeof(struct gpio_registers)
取决于你的GPIO硬件寄存器的大小。确保ioremap
成功。 -
使用 GPIO 寄存器: 通过虚拟地址可以访问GPIO硬件寄存器。在访问时,确保使用
readl
和writel
等函数来进行合适的内存读写操作。#include <asm/io.h> // 读取GPIO寄存器的值 unsigned int value = readl(gpio_base + GPIO_REGISTER_OFFSET); // 写入GPIO寄存器的值 writel(new_value, gpio_base + GPIO_REGISTER_OFFSET);
这里的
GPIO_REGISTER_OFFSET
是相对于gpio_base
的偏移量,表示你想要访问的特定GPIO寄存器。 -
内存解映射: 在驱动程序退出时,使用
iounmap
函数解映射已映射的内存区域。static void __exit mydriver_exit(void) { // 其他清理代码 // 解映射GPIO寄存器的虚拟地址 iounmap(gpio_base); }
这是一个简单的例子,具体的驱动程序可能需要更多的初始化和清理工作,取决于你的硬件和驱动的需求。确保在访问硬件寄存器时采取适当的同步和错误处理措施。
ioremap函数 将物理地址映射到内核的虚拟地址空间
在Linux内核编程中,ioremap
函数用于将物理地址映射到内核的虚拟地址空间。这个函数在处理驱动程序中需要与硬件进行直接交互的情况下非常有用,例如访问I/O端口、寄存器或其他硬件资源。
void *ioremap(resource_size_t offset, unsigned long size);
offset
:要映射的物理地址的起始偏移量。size
:要映射的地址空间大小。
ioremap
返回映射后的虚拟地址,或者在映射失败时返回 NULL
。
以下是一个简单的示例,演示如何在内核中使用 ioremap
:
#include <linux/io.h>
void __iomem *my_mapped_address;
static int __init mydriver_init(void) {
// 0x3f200000 是要映射的物理地址,sizeof(struct gpio_registers) 是要映射的大小
my_mapped_address = ioremap(0x3f200000, sizeof(struct gpio_registers));
if (!my_mapped_address) {
pr_err("Failed to map GPIO registers\n");
return -ENOMEM;
}
// 在这里可以使用 my_mapped_address 访问 GPIO 寄存器
return 0;
}
static void __exit mydriver_exit(void) {
// 在退出时解除映射
iounmap(my_mapped_address);
}
module_init(mydriver_init);
module_exit(mydriver_exit);
在这个例子中,my_mapped_address
将保存 ioremap
返回的虚拟地址。一旦你完成对硬件的操作,记得使用 iounmap
来解除映射,以防止内存泄漏。
要注意的是,直接映射硬件寄存器可能会有一些风险,因为它涉及到对硬件资源的直接访问。在实际的驱动程序开发中,你可能需要采取一些额外的措施来确保对硬件的安全访问,如使用适当的同步机制和错误处理。
iounmap函数 解除通过ioremap函数映射关系
iounmap
是Linux内核提供的函数之一,用于取消对通过 ioremap
函数映射的物理地址的映射,即解除映射关系。这是在使用直接I/O访问硬件寄存器或设备内存时非常重要的一步。
函数原型如下:
void iounmap(volatile void __iomem *addr);
addr
:通过ioremap
映射的虚拟地址。
以下是一个简单的例子,演示了 ioremap
和 iounmap
的使用:
#include <linux/io.h>
void *my_mapped_address;
void my_function(void)
{
// ioremap 将物理地址映射到内核虚拟地址
my_mapped_address = ioremap(0x3f200000, 4);
// 在这里执行对硬件寄存器的读写操作,例如:
unsigned int value = readl(my_mapped_address);
writel(new_value, my_mapped_address);
// 解除映射
iounmap(my_mapped_address);
}
在上述示例中,ioremap
用于映射物理地址 0x3f200000
到内核虚拟地址,并且 iounmap
用于解除这个映射关系。这样,驱动程序在不再需要硬件寄存器的访问时,可以释放相应的内存映射。
copy_from_user函数 从用户空间复制数据到内核空间
copy_from_user
函数用于从用户空间复制数据到内核空间。在Linux内核编程中,它是一个关键的函数,因为它允许内核访问用户空间的数据。
函数原型如下:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
to
:目标缓冲区,即数据将被复制到该缓冲区。from
:源缓冲区,即数据将被复制自该缓冲区。n
:要复制的字节数。
返回值是未能复制的字节数。如果返回值为零,表示所有数据都成功复制。
下面是一个简单的例子,演示如何在内核中使用 copy_from_user
:
#include <linux/uaccess.h>
void my_kernel_function(void)
{
char kernel_buffer[100];
char __user *user_buffer = (char __user *)0x12345678; // 示例用户空间缓冲区地址
unsigned long bytes_not_copied = copy_from_user(kernel_buffer, user_buffer, sizeof(kernel_buffer));
if (bytes_not_copied > 0) {
// 处理未能复制的情况
printk(KERN_ERR "Failed to copy %lu bytes from user space\n", bytes_not_copied);
} else {
// 成功复制数据
printk(KERN_INFO "Data successfully copied from user space\n");
}
}
在这个例子中,copy_from_user
函数将尝试从用户空间复制数据到内核缓冲区。如果有字节无法复制,将会打印错误消息。这是一种在内核中从用户空间获取数据的常见方式。在实际的驱动程序或内核模块中,你可能会在读取文件或处理系统调用等情况下使用此函数。
copy_to_user函数 将数据从内核空间复制到用户空间
copy_to_user
函数是Linux内核中的一个函数,用于将数据从内核空间复制到用户空间。这个函数通常在驱动程序中用于将内核中的数据传递给用户空间的应用程序。
函数原型如下:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
to
:目标用户空间缓冲区,即数据将被复制到该缓冲区。from
:源内核空间缓冲区,即数据将被复制自该缓冲区。n
:要复制的字节数。
返回值是未能复制的字节数。如果返回值为零,表示所有数据都成功复制。
以下是一个简单的例子,演示如何在内核中使用 copy_to_user
:
#include <linux/uaccess.h>
void my_kernel_function(void)
{
char kernel_buffer[100];
char __user *user_buffer = (char __user *)0x12345678; // 示例用户空间缓冲区地址
// 在这里填充内核缓冲区的数据
unsigned long bytes_not_copied = copy_to_user(user_buffer, kernel_buffer, sizeof(kernel_buffer));
if (bytes_not_copied > 0) {
// 处理未能复制的情况
printk(KERN_ERR "Failed to copy %lu bytes to user space\n", bytes_not_copied);
} else {
// 成功复制数据
printk(KERN_INFO "Data successfully copied to user space\n");
}
}
在这个例子中,copy_to_user
函数将尝试将数据从内核缓冲区复制到用户空间。如果有字节无法复制,将会打印错误消息。这是一种在内核中向用户空间传递数据的常见方式。在实际的驱动程序或内核模块中,你可能会在写入文件或处理系统调用等情况下使用此函数。
上图尾部的偏移量是正确的,根据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),而现在设置的是物理地址,所以必须把物理地址转换成虚拟地址
3. 配置引脚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关键字的作用:确保指令不会因编译器的优化而省略,且要求每次直接读值,在这里的意思就是确保地址不会被编译器更换
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类似
// 由于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>
#include <unistd.h>
int main()
{
int fd;
int userCmd;
fd = open("/dev/pin4", O_RDWR);
if (fd < 0)
{
printf("Failed to open /dev/pin4\n");
perror("Reason: ");
}
else
{
printf("Successfully opened /dev/pin4\n");
}
printf("Please input 1 for HIGH or 0 for LOW:\n");
scanf("%d", &userCmd);
write(fd, &userCmd, sizeof(userCmd)); // 这里userCmd是一个整型数,所以写的是4个字节
close(fd);
return 0;
}
【树莓派】内核开发 说明 下载代码 编译 替换内核
【树莓派】高级开发概述、开发环境搭建(内核镜像拷贝至树莓派并启动新内核)
树莓派初始引脚
运行代码后