基本概念
驱动的两大利器

编写操作I/O口的字符设备的驱动框架需要目的平台的的开发板的电路图和芯片手册,电路图和芯片手册是驱动的两大利器。

地址

  • 总线地址:地址总线(Address Bus;又称:位址总线)属于一种电脑总线(一部份),是由CPU 或有 DMA能力的单元,用来沟通这些单元想要存取(读取/写入)电脑内存元件/地方的实体位址。(即 cpu 能够访问内存的范围)。
  • 物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,成为物理地址。(硬件实际地址或绝对地址)
  • 虚拟地址:虚拟地址是指应用程序运行在 386 保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址
  • 内核里的代码跟应用程序的代码访问的是虚拟地址,而不是物理地址,因此我们在驱动框架访问的I/O口应该是虚拟地址,要将树莓派I/O口的物理地址映射成虚拟地址。

获取树莓派4B的基地址

#include <stdio.h>
#include <bcm_host.h>
 
void main()
{
	unsigned t;
	t = bcm_host_get_peripheral_address();
	printf("dizhi = %x\n",t);
    printf("gcc dizhi.c -I/opt/vc/include -L/opt/vc/lib -lbcm_host -o dizhi")
}




树莓派4B的基地址为0xFE00 0000,因此基地址加上偏移地址得到物理地址为0xFE20 0000


驱动框架
操作I/O口输入、输出0和输入、输出1,根据芯片手册,需要操作三个寄存器

  • 功能选择寄存器:GPFSEL0 选择I/O口及I/O口功能
  • 输出设置寄存器:GPSET0 选择I/O口输出1
  • 输出清除寄存器:GPCLR0 选择I/O口输出0

功能选择寄存器(GPFSELn)


由芯片手册得知,GPFSELn (n= 0时控制引脚0~9,n=1时控制引脚10~19,以此类推)为功能选择寄存器,功能选择寄存器用于定义通用 I/O 引脚的操作,有32位,每3位为1一组进行配置一个引脚,如果想要配置引脚4为输出,则应该让GPFSELn = 为GPFSEL0且配置bit 14-12 为001,其他位保持不变。



输出设置寄存器(GPSETn)


由芯片手册得知,GPFSETn (n= 0时控制引脚0~31,n=1时控制引脚32~57)为输出设置寄存器,输出设置寄存器用于定义通用 I/O 引脚的操作,有32位,每1位配置一个引脚,如果想要配置引脚4输出1,则应该让GPFSETn = 为GPFSET0且配置bit 4为1,其他位保持不变。




输出清除寄存器(GPCLRn)
由芯片手册得知,GPCLRn (n= 0时控制引脚0~31,n=1时控制引脚32~57)为输出设置寄存器,输出设置寄存器用于定义通用 I/O 引脚的操作,有32位,每1位配置一个引脚,如果想要配置引脚4输出1,则应该让GPCLRn = 为GPCLR0且配置bit 4为1,其他位保持不变。




驱动程序(该驱动程序在往期博文Linux内核结构体与字符设备驱动有详细注释)
volatile关键字

防止寄存器变量被编译器优化
要求每次直接从寄存器读值

#include <linux/fs.h>        
#include <linux/module.h>    
#include <linux/init.h>      
#include <linux/device.h>    
#include <linux/uaccess.h>   
#include <linux/types.h>     
#include <asm/io.h>          
 
static struct class *pin5_class;  
static struct device *pin5_class_dev;
 
static dev_t devno;                
static int major = 232;             
static int minor = 1;               
static char *module_name="pin5";   
 
volatile unsigned int* GPFSEL0 = NULL;  //GPFSEL0 选择I/O口及I/O口功能,功能选择寄存器
volatile unsigned int* GPSET0 = NULL;	//GPSET0  选择I/O口输出1,输出设置寄存器	
volatile unsigned int* GPCLR0 = NULL;	//GPCLR0  选择I/O口输出0,输出清除寄存器
 
static int pin5_open(struct inode *inode,struct file *file)
{
	printk("pin5_open\n");   
	*GPFSEL0 &= ~(0x6 << 15);
	*GPFSEL0 |= (0x1 << 15);			  //GPFSEL0 选择I/O口及I/O口功能	
	return 0;
}
 
static ssize_t pin5_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
	int user_cmd = 2;
	printk("pin5_write\n");  
	copy_from_user(&user_cmd,buf,count);  //获取上层程序代码write()传进来的值
	if(user_cmd == 1){
		printk("set 1\n");
		*GPSET0 |= (0X1 << 5);			  //选择I/O口输出1
	}else if(user_cmd == 0){
		printk("set 0\n");
		*GPCLR0 |= (0X1 << 5);			  //选择I/O口输出0
	}else{
		printk("cmd is not know");
	}
 
	return 0;
}
 
static struct file_operations pin5_fops = {
	.owner = THIS_MODULE,
	.open  = pin5_open,
	.write = pin5_write,
};
 
int __init pin5_drv_init(void)   
{
 
	int ret;
	printk("insmod driver pin5 is success\n");
	devno = MKDEV(major,minor);  
	ret   = register_chrdev(major, module_name,&pin5_fops);  
	pin5_class=class_create(THIS_MODULE,"mypin5demo");  
	pin5_class_dev =device_create(pin5_class,NULL,devno,NULL,module_name);  
 
	//ioremap:该宏将4个字节即32位的I/O地址空间映射到内核的虚拟空间
	GPFSEL0 = (volatile unsigned int *)ioremap(0xFE200000,4); 
	GPSET0  = (volatile unsigned int *)ioremap(0xFE20001C,4);
	GPCLR0  = (volatile unsigned int *)ioremap(0xFE200028,4);
 
	return 0;
}
 
void __exit pin5_drv_exit(void)
{
	//iounmap:解除虚拟空间的映射
	iounmap(GPFSEL0); 
	iounmap(GPSET0);
	iounmap(GPCLR0);
 
	device_destroy(pin5_class,devno);  
	class_destroy(pin5_class);  
	unregister_chrdev(major, module_name);  
 
}
 
module_init(pin5_drv_init);  
module_exit(pin5_drv_exit);
MODULE_LICENSE("GPL v2");

用户态应用程序



执行效果




命令dmesg查看内核打印数据