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 灯