编写驱动程序前需要移植树莓派内核以获取内核源码,选择树莓派3B(CPU:BCM2709),其IO空间的起始地址为0x3f000000(BCM2708是0x20000000),加上GPIO的偏移量0x2000000,实际GPIO的物理地址是从0x3f200000开始的。在此基础上,再进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。
目录
在虚拟机上分别编译,将模块、测试程序拷贝至树莓派,插入模块后运行测试程序。
sudo insmod xxx.ko
sudo ./led_test 1 打开LED
sudo ./led_test 0 关闭LED
1 LED驱动代码
整个程序基于Linux内核模块编写,主要用于控制树莓派上的GPIO26号引脚,来控制一个LED灯的开关。
1.1 头文件导入
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/delay.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <linux/list.h>
#include <linux/pci.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
#include <asm/unistd.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/ioport.h>
这些头文件定义了模块开发所需的各种宏和函数。
1.2 常量定义
#define PIN 26 // GPIO26 引脚号
#define BCM2835_GPSET0 0x001c
#define BCM2835_GPFSEL0 0x0000
#define BCM2835_GPCLR0 0x0028
#define BCM2835_GPIO_FSEL_OUTP 1
#define BCM2835_GPIO_BASE 0x3f200000
使用宏定义 GPIO 的一些控制寄存器和物理地址,PIN 26即我们需要控制的引脚。GPIO_BASE是树莓派的 GPIO 寄存器基地址,GPSET0设置 GPIO 输出寄存器的偏移,GPFSEL0为GPIO 功能选择寄存器的偏移,使用GPCLR0清除 GPIO 输出寄存器的偏移,GPIO_FSEL_OUTP用于设置 GPIO 为输出模式的值。
1.3 文件打开状态
int open_state = 0; // 文件打开状态
使用变量open_state跟踪设备文件是否已经打开。
1.4 GPIO操作函数
1)bcm2835_gpio_fsel()
int bcm2835_gpio_fsel(uint8_t pin, uint8_t mode)
{
volatile uint32_t * bcm2835_gpio = (volatile uint32_t *)ioremap(BCM2835_GPIO_BASE, 16);
volatile uint32_t * bcm2835_gpio_fsel = bcm2835_gpio + BCM2835_GPFSEL0/4 + (pin/10);
uint8_t shift = (pin % 10) * 3;
uint32_t value = mode << shift;
*bcm2835_gpio_fsel = *bcm2835_gpio_fsel | value;
printk("fsel address: 0x%lx : %x\n", (long unsigned int)bcm2835_gpio_fsel, *bcm2835_gpio_fsel);
return 0;
}
这个函数用于配置指定 GPIO 引脚的功能(如输入或输出),pin是 GPIO 引脚号,mode是模式(这里通常是输入或输出),具体步骤:
- 使用ioremap将物理地址BCM2835_GPIO_BASE映射到虚拟地址空间;
- 计算 GPIO 功能选择寄存器的地址;
- 设置对应引脚的功能为输出。
2)bcm2835_gpio_set()
int bcm2835_gpio_set(uint8_t pin)
{
volatile uint32_t * bcm2835_gpio = (volatile uint32_t *)ioremap(BCM2835_GPIO_BASE, 16);
volatile uint32_t * bcm2835_gpio_set = bcm2835_gpio + BCM2835_GPSET0/4 + pin/32;
uint8_t shift = pin % 32;
uint32_t value = 1 << shift;
*bcm2835_gpio_set = *bcm2835_gpio_set | value;
printk("set address: 0x%lx : %x\n", (long unsigned int)bcm2835_gpio_set, *bcm2835_gpio_set);
return 0;
}
这个函数用于设置指定 GPIO 引脚的输出电平为高电平,类似地,它先映射物理地址,然后计算具体的寄存器地址,最后将指定引脚置为高电平,点亮 LED。
3)bcm2835_gpio_clr()
int bcm2835_gpio_clr(uint8_t pin)
{
volatile uint32_t * bcm2835_gpio = (volatile uint32_t *)ioremap(BCM2835_GPIO_BASE, 16);
volatile uint32_t * bcm2835_gpio_clr = bcm2835_gpio + BCM2835_GPCLR0/4 + pin/32;
uint8_t shift = pin % 32;
uint32_t value = 1 << shift;
*bcm2835_gpio_clr = *bcm2835_gpio_clr | value;
printk("clr address: 0x%lx : %x\n", (long unsigned int)bcm2835_gpio_clr, *bcm2835_gpio_clr);
return 0;
}
该函数用于清除指定 GPIO 引脚的输出电平,将其置为低电平,熄灭 LED。
1.5 文件操作函数
1)leds_open()
static int leds_open(struct inode *inode, struct file *filp)
{
if(open_state == 0)
{
open_state = 1;
printk("Open file suc!\n");
return 0;
}
else
{
printk("The file has opened!\n");
return -1;
}
}
这是设备文件的打开函数,它检查设备文件是否已经打开,如果没有,则标记为打开并返回成功;否则提升该设备文件已经被打开。
2)leds_ioctl()
static long leds_ioctl(struct file*filp, unsigned int cmd, unsigned long arg)
{
switch(cmd)
{
case 0:
bcm2835_gpio_clr(PIN);
printk("LED OFF!\n");
break;
case 1:
bcm2835_gpio_set(PIN);
printk("LED ON!\n");
break;
default:
return -EINVAL;
}
return 0;
}
这是ioctl操作函数,用于处理用户空间发送的控制命令,cmd是用户传递的命令,可以控制 LED 的开关,具体地:
- 'cmd = 0' 关闭LED(低电平);
- 'cmd = 1' 打开LED(高电平)。
3)leds_release()
static int leds_release(struct inode *inode, struct file *filp)
{
if(open_state == 1)
{
open_state = 0;
printk("close file suc!\n");
return 0;
}
else
{
printk("The file has closed!\n");
return -1;
}
}
这是设备文件的关闭函数,它将open_state重置为 0,表示设备文件已经关闭。
1.6 文件操作结构体
static const struct file_operations leds_fops = {
.owner = THIS_MODULE,
.open = leds_open,
.unlocked_ioctl = leds_ioctl,
.release = leds_release,
};
该结构体将上面定义的文件操作函数绑定到设备驱动程序上,内核通过这个结构体知道如何处理打开、关闭和控制设备文件的请求。
1.7 混杂设备结构体
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "my_leds",
.fops = &leds_fops,
};
这个结构体定义了一个混杂设备(misc device),它是一种特殊类型的字符设备,主要用于实现一些不属于任何特定分类的设备,使用MISC_DYNAMIC_MINOR可以让系统自动分配次设备号。
1.8 模块初始化和清理
1)leds_init
static int __init leds_init(void)
{
int ret;
// 注册混杂设备
ret = misc_register(&misc);
// 配置功能选择寄存器为输出
bcm2835_gpio_fsel(PIN, BCM2835_GPIO_FSEL_OUTP);
// 设置输出电平为高电平,LED亮
bcm2835_gpio_set(PIN);
printk("ledsinit.\n");
return ret;
}
这是模块的初始化函数,当模块加载时,它会执行以下操作:
- 注册混杂设备,以使设备文件出现在/dev目录下;
- 配置 GPIO26 为输出模式;
- 将 GPIO26 的电平设置为高(LED 点亮)。
2)leds_exit
static void leds_exit(void)
{
// LED灭
bcm2835_gpio_clr(PIN);
// 注销混杂设备
misc_deregister(&misc);
printk("leds_exit\n");
}
这是模块的清理函数,当模块卸载时,它会执行以下操作:
- 将 GPIO26 的电平设置为低(LED 熄灭);
- 注销混杂设备,移除/dev/my_leds设备文件。
1.9 模块信息和入口/出口函数
module_init(leds_init);
module_exit(leds_exit);
MODULE_AUTHOR("***");
MODULE_LICENSE("GPL");
这段代码指定了模块的入口和出口函数,并提供了作者信息和许可证类型,module_init和 module_exit分别标记了模块加载和卸载时的执行函数,MODULE_LICENSE 表示该模块使用 GPL 许可证发布,这对 Linux 内核模块开发非常重要。
总结
该程序是一个简单的内核模块驱动程序,用于控制树莓派上的一个 LED,即通过'/dev/my_leds'设备文件控制 GPIO26 引脚的高低电平,从而控制 LED 的开关。