树莓派linux led字符设备驱动(传统)

  Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以 LED 灯驱动最终也是对树莓派的 IO 口进行配置,但是在 Linux 下编写驱动要符合 Linux的驱动框架。

一、树莓派GPIO

1.1、寄存器地址

  树莓派3B的CPU为BCM2709,树莓派4B的CPU为BCM2711。通过查看芯片的数据手册可以查看GPIO的寄存器地址。也可以通过指令查看树莓派GPIO地址的分配情况。

cat /proc/iomem

  在proc目录下有iomem和ioports文件,其主要描述了系统的io内存和io端口资源分布。Linux设计了一个通用的数据结构resource来描述各种I/O资源(如:I/O端口、外设内存、DMA和IRQ等)。该结构定义在include/linux/ioport.h头文件中。Linux是以一种倒置的树形结构来管理每一类I/O资源。每一类I/O资源都对应有一颗倒置的资源树,树中的每一个节点都是个resource结构。基于上述这个思想,Linux将基于I/O映射方式的I/O端口和基于内存映射方式的I/O端口资源统称为“I/O区域”(I/O Region)。/proc/iomem这个文件记录的是物理地址的分配情况,也是以树状结构显示,对其使用也是request_mem_region和ioremap

1.2、寄存器解析

  控制GPIO为LED,主要关注的是GPIO的寄存器为:
  1、GPFSELn选择寄存器
  2、GPSETn置位寄存器
  3、GPCLRn清除寄存器

二、地址映射

  在一些旧版本的 Linux 中要求处理器必须有 MMU,MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。。 MMU 主要完成的功能如下:
  ①、完成虚拟空间到物理空间的映射。
  ②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
  对于 32 位的处理器,其有限物理内存,要经过 MMU 可以将其映射到整个 4GB 的虚拟空间。Linux 内核启动的时候会初始化 MMU,设置好内存映射以后 CPU 访问的都是虚拟 地 址 。虚拟地址(VA,Virtual Address)、物理地址(PA, Physcical Address)之间的访问切换 需要用到两个函数: ioremap 和 iounmap。
  1、 ioremap 函数
  ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h 文件中。
  2、 iounmap 函数
  卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射。

三、I/O 内存访问

  使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
  1、读操作函数
  读操作函数有如下几个:

u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

  readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。
  2、写操作函数
  写操作函数有如下几个:

void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

  writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。

四、Linux字符设备驱动框架

4.1、字符设备驱动简介

  在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
  应用程序使用到的函数在具体驱动程序中都有与之对应的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合.
  在 Linux 驱动开发中需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是其驱动框架。

4.2、驱动模块的加载和卸载

  Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。模块的加载和卸载注册函数如下:

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

  module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当卸载具体驱动的时候 xxx_exit 函数就会被调用。
  驱动编译完成以后扩展名为.ko,modprobe 命令在加载驱动时还会检查模块的依赖性分析、错误检查、错误报告等功能。

4.3、 字符设备注册与注销

  对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:

static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

五、程序编写

5.1、编写驱动代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define LED_MAJOR		200		/* 主设备号 */
#define LED_NAME		"ledtest" 	/* 设备名字 */

#define LEDOFF 	0				/* 关灯 */
#define LEDON 	1				/* 开灯 */
 
#define LED_PIN					    26 //GPIO26 


  /* 寄存器物理地址 */	
 
#define BCM2837_GPFSEL2_BASE          (0X3F200008)  //GPIO的GPFSEL2寄存器的 bits 20-18 位控制 GPIO_26  
#define BCM2837_GPSET0_BASE           (0X3F20001C)  //GPIO的GPSET0的 bit26 控制 GPIO_26 置位
#define BCM2837_GPCLR0_BASE           (0X3F200028)  //GPIO的GPCLR0的 bit25 控制 GPIO_26 清零
 
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *BCM2837_GPFSEL2;
static void __iomem *BCM2837_GPSET0;
static void __iomem *BCM2837_GPCLR0;


/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
    
    

	if(sta == LEDON) {
    
    
		writel(1<<(LED_PIN), BCM2837_GPCLR0);
	}else if(sta == LEDOFF) {
    
    
		writel(1<<(LED_PIN), BCM2837_GPSET0);
	}
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    
    
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    
    
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    
    
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
    
    
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {
    
    	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
    
    
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
    
    
	return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
    
    
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
    
    
	int retvalue = 0;
	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
	BCM2837_GPFSEL2 = ioremap(BCM2837_GPFSEL2_BASE,4);  
	BCM2837_GPSET0 = ioremap(BCM2837_GPSET0_BASE,4); 
	BCM2837_GPCLR0 = ioremap(BCM2837_GPCLR0_BASE,4); 

	/*2、初始化引脚功能,同时输出高电平*/
	val = readl(BCM2837_GPFSEL2);
	val = val | (1<<((LED_PIN % 10) * 3));
	writel(val, BCM2837_GPFSEL2);
	writel(1<<(LED_PIN), BCM2837_GPSET0);


	/* 3、注册字符设备驱动 */
	retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
	if(retvalue < 0){
    
    
		printk("register chrdev failed!\r\n");
		return -EIO;
	}
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
    
    
	/* 取消映射 */

	iounmap(BCM2837_GPFSEL2);  
	iounmap(BCM2837_GPSET0);  
	iounmap(BCM2837_GPCLR0); 

	/* 注销字符设备驱动 */
	unregister_chrdev(LED_MAJOR, LED_NAME);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zsx");

5.2、编写测试代码

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"


#define LEDOFF 	0
#define LEDON 	1

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
    
    
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
    
    
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
    
    
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
    
    
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 关闭文件 */
	if(retvalue < 0){
    
    
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

5.3、编写makefile

KERNELDIR := /home/linux/
CURRENT_PATH := $(shell pwd)

obj-m := led.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

六、运行测试

  通过执行make编译驱动程序,通过命令gcc ledtest.c -o ledtest编译测试APP。将编译出来的 led.ko和 ledtest这两个文件拷贝到/lib/modules/5.10.17-v7l+/ 目录中,输入如下命令加载 led.ko 驱动模块:

sudo depmod //第一次加载驱动的时候需要运行此命令
sudo modprobe ledtest//加载驱动
dmesg //查看模块打印信息

  驱动加载成功以后创建“/dev/led”设备节点,命令如下:

mknod /dev/ledtest c 200 0

  驱动节点创建成功以后就可以使用 ledtest软件来测试驱动是否工作正常,输入如下命令:

./ledtest /dev/led 1 //打开 LED 灯

  输入如下命令关闭 LED 灯:

./ledtest /dev/led 0 //关闭 LED 灯

猜你喜欢

转载自blog.csdn.net/xxxx123041/article/details/120165724