对树莓派gpio的操作有好多方法,比如mmap映射cpu内存,编写内核驱动模块等。这里推荐一篇文章外链网址已屏蔽
其实树莓派官方使用linux内核gpio驱动框架内置了一个驱动,让我们可以直接使用标准的内核gpio模型来开发。今天就来分析下这个驱动,驱动文件在arch/arm/mach-bcm2709/bcm2708_gpio.c 。该文件是一个模块,但是只能编译进内核,不能编译成模块,所以在实验的时候只能连内核加模块一起更新到树莓派。
一·首先来看下platform驱动的注册和取消注册函数
static int __init bcm2708_gpio_init(void)
{return platform_driver_register(&bcm2708_gpio_driver);//注册平台驱动
}static void __exit bcm2708_gpio_exit(void)
{
platform_driver_unregister(&bcm2708_gpio_driver);//取消注册平台驱动
}
很简单的调用platform_driver_register和platform_driver_unregister函数进行注册和取消注册平台驱动。
二·平台设备的注册
平台设备的注册在板级初始化文件中(arch/arm/mach-bcm2709/bcm2709.c)进行的。
static struct resource bcm2708_gpio_resources[] ={
[0] = { /*general purpose I/O*/.start=GPIO_BASE,
.end= GPIO_BASE + SZ_4K - 1,
.flags=IORESOURCE_MEM,
},
};static u64 gpio_dmamask =DMA_BIT_MASK(DMA_MASK_BITS_COMMON);static struct platform_device bcm2708_gpio_device ={
.name=BCM_GPIO_DRIVER_NAME,
.id= -1, /*only one VideoCore I/O area*/.resource=bcm2708_gpio_resources,
.num_resources=ARRAY_SIZE(bcm2708_gpio_resources),
.dev={
.dma_mask= &gpio_dmamask,
.coherent_dma_mask=DMA_BIT_MASK(DMA_MASK_BITS_COMMON),
},
};
我们可以看到设备资源的开始地址是GPIO_BASE,他的值在arch/arm/mach-bcm2709/includes/platform.h中定义为0x3F200000,这个地址就是bcm2709的gpio的基地址。
三·平台设备初始化probe函数分析
static int bcm2708_gpio_probe(struct platform_device *dev)
{struct bcm2708_gpio *ucb;struct resource *res;intbank;int err = 0;
printk(KERN_INFO DRIVER_NAME": bcm2708_gpio_probe %p\n", dev);
ucb= kzalloc(sizeof(*ucb), GFP_KERNEL);//分配自定义的结构体内存
if (NULL ==ucb) {
printk(KERN_ERR DRIVER_NAME": failed to allocate mailbox memory\n");
err= -ENOMEM;gotoerr;
}
res= platform_get_resource(dev, IORESOURCE_MEM, 0);//获取板级初始化中的平台资源
platform_set_drvdata(dev, ucb);//将ucb保存到本驱动的私有数据指针中
ucb->base = __io_address(GPIO_BASE);//获取gpio物理地址
/*以下是相关数据和函数的结构体赋值*/ucb->gc.label = "bcm2708_gpio";
ucb->gc.base = 0;
ucb->gc.ngpio =BCM2708_NR_GPIOS;
ucb->gc.owner =THIS_MODULE;
ucb->gc.direction_input =bcm2708_gpio_dir_in;
ucb->gc.direction_output =bcm2708_gpio_dir_out;
ucb->gc.get =bcm2708_gpio_get;
ucb->gc.set =bcm2708_gpio_set;
ucb->gc.can_sleep = 0;/*这个是初始化两个bank(这么多个gpio一共分成两个bank)的某个属性*/
for (bank = 0; bank < GPIO_BANKS; bank++) {
writel(0, ucb->base +GPIOREN(bank));
writel(0, ucb->base +GPIOFEN(bank));
writel(0, ucb->base +GPIOHEN(bank));
writel(0, ucb->base +GPIOLEN(bank));
writel(0, ucb->base +GPIOAREN(bank));
writel(0, ucb->base +GPIOAFEN(bank));
writel(~0, ucb->base +GPIOEDS(bank));
}
bcm2708_gpio_irq_init(ucb);//中断初始化
err= gpiochip_add(&ucb->gc);//向系统的gpio子系统注册
err:returnerr;
}
structbcm2708_gpio {structlist_head list;void __iomem *base;structgpio_chip gc;
unsignedlong rising[(BCM2708_NR_GPIOS + 31) / 32];
unsignedlong falling[(BCM2708_NR_GPIOS + 31) / 32];
unsignedlong high[(BCM2708_NR_GPIOS + 31) / 32];
unsignedlong low[(BCM2708_NR_GPIOS + 31) / 32];
};
首先驱动定义了一个存放私有数据的结构体bcm2708_gpio的指针,并为其分配了内存。接着获取板级初始化中的平台资源,将ucb私有数据保存到本驱动的私有数据指针中,获取gpio物理地址,有了这些信息后便进行ucb私有数据相关指针的赋值。由于我没有找到bcm2709的数据手册,所以下面代码的作用不是很了解,只是明白这个是初始化两个bank(这么多个gpio一共分成两个bank)的某个属性。函数最后向系统的gpio子系统注册gpio_chip。gpio_chip结构体需要实现多个系统回调函数,分别是GPIO方向设置函数,GPIO设置输出函数,GPIO读取状态函数。下面会一一看每个函数。
四·平台设备注销remove函数分析
gpiochip_remove函数向系统的gpio子系统取消注册,platform_set_drvdata函数清空驱动的数据指针。
五·GPIO方向设置函数分析
static int bcm2708_set_function(struct gpio_chip *gc, unsigned offset, intfunction)
{struct bcm2708_gpio *gpio = container_of(gc, structbcm2708_gpio, gc);
unsignedlongflags;
unsigned gpiodir;
unsigned gpio_bank= offset / 10;//确定是哪个寄存器(每个寄存器可以配置10个gpio 每个gpio需要3位)
unsigned gpio_field_offset = (offset - 10 * gpio_bank) * 3;//在确定寄存器中的找到需要操作的三个位的偏移
printk(KERN_ERR DRIVER_NAME": bcm2708_gpio_set_function %p (%d,%d)\n", gc, offset, function);if (offset >= BCM2708_NR_GPIOS)//判断端口偏移是否大于54
return -EINVAL;
spin_lock_irqsave(&lock, flags);
gpiodir= readl(gpio->base + GPIOFSEL(gpio_bank));//gpio->base=0x3f200000 求出该端口所在的寄存器地址并读出来
gpiodir &= ~(7 << gpio_field_offset);//将该端口所对应的3个位清零(输出)
gpiodir |= function << gpio_field_offset;//根据参数设置该端口实际的输入输出状态
writel(gpiodir, gpio->base + GPIOFSEL(gpio_bank));//写寄存器
spin_unlock_irqrestore(&lock, flags);
gpiodir= readl(gpio->base +GPIOFSEL(gpio_bank));return 0;
}
根据数据手册记载,每个gpio口占用方向寄存器的3个位,也就是说每个寄存器管理32/3一共10个gpio口,所以
1.unsignedgpio_bank = offset /10;//确定是哪个寄存器(每个寄存器可以配置10个gpio 每个gpio需要3位)
2.unsignedgpio_field_offset = (offset -10* gpio_bank) *3;//在确定寄存器中的找到需要操作的三个位的偏移
设置步骤为
A.读取该寄存器的值
1.gpiodir =readl(gpio->base +GPIOFSEL(gpio_bank));//gpio->base=0x3f200000 求出该端口所在的寄存器地址并读出来
B.修改该寄存器的值
1.gpiodir &= ~(7<< gpio_field_offset);//将该端口所对应的3个位清零(输出)
2.gpiodir |= function << gpio_field_offset;//根据参数设置该端口实际的输入输出状态
C.写入寄存器
1.writel(gpiodir, gpio->base +GPIOFSEL(gpio_bank));//写寄存器
六·GPIO设置输出函数分析
static void bcm2708_gpio_set(struct gpio_chip *gc, unsigned offset, intvalue)
{struct bcm2708_gpio *gpio = container_of(gc, structbcm2708_gpio, gc);
unsigned gpio_bank= offset / 32;//确定是哪个寄存器(每个寄存器可以配置32个gpio 每个gpio需要1位)
unsigned gpio_field_offset = (offset - 32 * gpio_bank);//在确定寄存器中的找到需要操作的1个位的偏移//printk(KERN_ERR DRIVER_NAME ": bcm2708_gpio_set %p (%d=%d)\n", gc, offset, value);
if (offset >= BCM2708_NR_GPIOS)//判断端口偏移是否大于54
return;if(value)
writel(1 << gpio_field_offset, gpio->base + GPIOSET(gpio_bank));//直接根据偏移写入数值
elsewritel(1 << gpio_field_offset, gpio->base + GPIOCLR(gpio_bank));//直接根据偏移写入数值
}
根据数据手册记载,每个gpio口占用输出寄存器的1个位,也就是说每个寄存器管理32/1一共32个gpio口,所以
1.unsignedgpio_bank = offset /32;//确定是哪个寄存器(每个寄存器可以配置32个gpio 每个gpio需要1位)
2.unsignedgpio_field_offset = (offset -32* gpio_bank);//在确定寄存器中的找到需要操作的1个位的偏移
接着便可以进行写操作
1.writel(1<< gpio_field_offset, gpio->base +GPIOSET(gpio_bank));//直接根据偏移写入数值
七·GPIO读取状态函数分析
static int bcm2708_gpio_get(struct gpio_chip *gc, unsigned offset)
{struct bcm2708_gpio *gpio = container_of(gc, structbcm2708_gpio, gc);
unsigned gpio_bank= offset / 32;//确定是哪个寄存器(每个寄存器可以配置32个gpio 每个gpio需要1位)
unsigned gpio_field_offset = (offset - 32 * gpio_bank);//在确定寄存器中的找到需要操作的1个位的偏移
unsigned lev;if (offset >= BCM2708_NR_GPIOS)//判断端口偏移是否大于54
return 0;
lev= readl(gpio->base + GPIOLEV(gpio_bank));//直接根据偏移读取数值
printk(KERN_ERR DRIVER_NAME": bcm2708_gpio_get %p (%d)=%d\n", gc, offset, 0x1 & (lev>>gpio_field_offset));return 0x1 & (lev >> gpio_field_offset);//取所需的3位清零其他位
}
根据数据手册记载,每个gpio口占用输出寄存器的1个位,也就是说每个寄存器管理32/1一共32个gpio口,所以
1.unsignedgpio_bank = offset /32;//确定是哪个寄存器(每个寄存器可以配置32个gpio 每个gpio需要1位)
2.unsignedgpio_field_offset = (offset -32* gpio_bank);//在确定寄存器中的找到需要操作的1个位的偏移
接着便可以进行读操作
1.lev =readl(gpio->base +GPIOLEV(gpio_bank));//直接根据偏移读取数值
2.return0x1 & (lev >> gpio_field_offset);//取所需的3位清零其他位
8·关于几个宏定义的作用
#define GPIOFSEL(x) (0x00+(x)*4)//方向寄存器偏移地址
#define GPIOSET(x) (0x1c+(x)*4)//输出寄存器偏移地址
#define GPIOCLR(x) (0x28+(x)*4)//清零寄存器偏移地址
#define GPIOLEV(x) (0x34+(x)*4)//读取寄存器偏移地址
#define GPIOEDS(x) (0x40+(x)*4)
#define GPIOREN(x) (0x4c+(x)*4)
#define GPIOFEN(x) (0x58+(x)*4)
#define GPIOHEN(x) (0x64+(x)*4)
#define GPIOLEN(x) (0x70+(x)*4)
#define GPIOAREN(x) (0x7c+(x)*4)
#define GPIOAFEN(x) (0x88+(x)*4)
#define GPIOUD(x) (0x94+(x)*4)
#define GPIOUDCLK(x) (0x98+(x)*4)
这几个宏定义用来计算要操作的gpio的各种寄存器的地址。
9.烧录到树莓派实验一下
(烧到树莓派的时候要把内核和编译出来的modules全部都拷贝覆盖到树莓派)
最后给一个内核编译的脚本
#!/bin/shPWD=`pwd`
export PATH=${PWD}/../tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin:$PATH
#make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-bcm2709_defconfigmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- || exit 1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules || exit 1
rm -rf _install*
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install INSTALL_PATH=$PWD/_installmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_install INSTALL_MOD_PATH=$PWD/_installcp arch/arm/boot/Image _install/
cp ../RaspberryKernel/script/install.sh _install/
tar -czf _install.tar.gz _install/