本文源码:https://github.com/philon/rpi-drivers/tree/master/01-gpio-led
GPIO可以说是驱动中最最最简单的部分了,但我上网查了下,绝大部分所谓《树莓派GPIO驱动》的教程全是python、shell等编程,或者调用第三方库,根本不涉及任何ARM底层、Linux内核相关的知识。显然,这根本不是什么驱动实现,只是调用了一两个别人实现好的库函数而已,跟着那种文章走一遍,你只知道怎么用,永远不知道为什么。
所以本文是希望从零开始,在Linux内核下实现一个真正的gpio-led驱动程序,初步体验一下Linux内核模块的开发思想,知其然,知其所以然。
GPIO基础 General-purpose input/output(通用输入/输出),其引脚可由软件控制,选择输入、输出、中断、时钟、片选等不同的功能模式。以树莓派为例,我们可以通过pinout官网 查看板子预留的40pinGPIO分别是做什么的。
如上图,GPIO0-1、GPIO2-3脚,除了常规的输入/输出,还可作为I²C接口,GPIO14-15脚,可另作为TTL串口。
总之,GPIO平时就是个普通IO口,仅作为开关用,但开关只是为了掩人耳目,背后的复用功能才是它的真正职业。
三色LED电路 弄懂了GPIO原理,那就来实际操作一把,准备点亮LED灯吧!
先来看看原理图,为了区分三色灯不同颜色的LED,我特别用红绿蓝接入对应的RGB三个灯,黑线表示GND。
如图所示,三色灯的R、G、B正极分别接到树莓派GPIO的2、3、4脚,灯的公共负极随便接一个GND脚。因此,想要点亮其中一个灯,对应GPIO脚输出高电平即可,是不是很简单呐!
BCM2837寄存器分配 基于上述,要点亮LED只需要做一件事——GPIO输出高电平。如何通过程序让GPIO口输出高电平呢?
GPIO的控制其实是通过对应的CPU寄存器来实现的。在ARM架构的SoC中,所有的外围资源(寄存器)其实都是被映射到内存当中的,所以我们要读写寄存器,只需访问它映射到的内存地址即可。
那么问题来了,为什么不直接读写CPU的寄存器呢?因为现代的嵌入式系统往往都标配内存模块,处理器也带有MMU,所以其内部寄存器也就交由MMU来管理。
综上,我们现在要找出树莓派3B+这款芯片——BCM2837B0的GPIO物理内存地址。
这里不得不吐槽一下,我先是跑到树莓派3B+官方网址 去找芯片资料,得知BCM2837其实就是BCM2836的主频升级版;我又去看BCM2836的资料,得知这只不过是BCM2835从32位到64位的升级版;我又去看BCM2835的芯片资料,然而里面说的内存映射地址根本就是错的……
要确定BCM2837B0的内存映射需要参考两个地方:
BCM2835 Datasheet ,但要留意,里面坑很多,且并不完全适用于树莓派3B。
国外热心网友的代码 ,仅包含GPIO驱动,没有太多细节
官方文档在第6页和第90页有这样一句话和这样几张表:
Physical addresses range from 0x20000000 to 0x20FFFFFF for peripherals. The bus addresses for peripherals are set up to map onto the peripheral bus address range starting at 0x7E000000. Thus a peripheral advertised here at bus address 0x7Ennnnnn is available at physical address 0x20nnnnnn.
我就直说吧,共6个关键要素:
外围总线地址0x7E000000映射到ARM物理内存地址0x20000000,加上偏移,GPIO物理地址为0x20200000
GPIO操作需要先通过GPFSEL选择复用功能 ,再通过GPSET/GPCLR对指定位拉高/拉低
BMC2835共54个GPIO,分为两组BANK,第一组[0:31],第二组[32:53]
GPFSEL寄存器每3位表示一个GPIO的复用功能,因此一个寄存器可容纳10个GPIO,共6个GPFSEL
GPSET/GPCLR寄存器每1位表示一个GPIO的状态1/0,因此一个寄存器可容纳32个GPIO,共2个GPSET/GPCLR
⚠️国外热心网友指出:树莓派3B+的GPIO物理内存地址被映射到了0x3F200000!
好了,现在结合电路图可推导出思路:
R、G、B分别对应GPIO2、3、4,需要操作的寄存器为GPFSEL0/GPSET0/GPCLR0
要把三个脚全部设为“输出模式”,需要将GPFSEL0的第6、9、12都置为001
要控制三个脚的输出状态,需要将GPSET0/GPCLR0的第2、3、4脚置1
先点亮红灯 现在开始写Linux驱动模块,先不着急完整实现,这一步只是把其中的红灯点亮,为此我甚至把绿蓝线给拔了!
代码实现非常简单,就是在加载驱动时红灯亮,卸载驱动时红灯灭。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <linux/init.h> #include <linux/module.h> #include <asm/io.h> #define BCM2837_GPIO_BASE 0x3F200000 #define BCM2837_GPIO_FSEL0_OFFSET 0x0 #define BCM2837_GPIO_SET0_OFFSET 0x1C #define BCM2837_GPIO_CLR0_OFFSET 0x28 static void * gpio = 0 ;static int __init rgbled_init (void ) { gpio = ioremap(BCM2837_GPIO_BASE, 0xB0 ); int val = ioread32(gpio + BCM2837_GPIO_FSEL0_OFFSET); val &= ~(7 << 6 ); val |= 1 << 6 ; iowrite32(val, gpio); iowrite32(1 << 2 , gpio + BCM2837_GPIO_SET0_OFFSET); return 0 ; } module_init(rgbled_init); static void __exit rgbled_exit (void ) { iowrite32(1 << 2 , gpio + BCM2837_GPIO_CLR0_OFFSET); iounmap(gpio); } module_exit(rgbled_exit); MODULE_LICENSE("Dual BSD/GPL" ); MODULE_AUTHOR("Philon | ixx.life" );
这段代码太简单了,以至于我觉得完全不需要解释,直接看效果吧。图中的命令:
1 2 philon@rpi:~/modules$ insmod rgbled.ko philon@rpi:~/modules$ rmmod rgbled.ko
再点亮全部 其实点亮红灯后,绿蓝灯无非是改改地址而已,没什么难度。本文的目的是学习Linux驱动,点亮LED不过是驱动开发的感性认识,所以我决定把简单的问题复杂化😄。驱动主要为用户层提供了几种设备控制方式:
通过命令echo [white|black|red|yellow...] > /dev/rgbled
直接控制灯的颜色
通过命令cat /dev/rgbled
查看当前灯的状态
通过函数ioctl(fd, 1, 0)
可独立控制每个灯的状态
说白了,用户层只须关心等的输出的颜色,屏蔽了具体的电路引脚及状态。
为此,我们需要把三色LED模块当作一个字符设备 来实现,本文是驱动开发实战,所以更多的讲如何实现,有关字符设备的原理可以参考我的另一篇文章《ARM-Linux驱动开发四:字符设备》 。
驱动主要分为两大块:设备的read/write/ioctl
接口 以及字符设备的注册 。
先看看驱动的读写控制是如何实现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 static struct { const char * name; const bool pins[3 ]; } colors[] = { { "white" , {1 ,1 ,1 } }, { "black" , {0 ,0 ,0 } }, { "red" , {1 ,0 ,0 } }, { "green" , {0 ,1 ,0 } }, { "blue" , {0 ,0 ,1 } }, { "yellow" , {1 ,1 ,0 } }, { "cyan" , {0 ,1 ,1 } }, { "purple" , {1 ,0 ,1 } }, }; static void * gpio = 0 ; static bool ledstate[3 ] = {0 }; void gpioctl (int pin, bool stat) { void * reg = gpio + (stat ? BCM2837_GPIO_SET0_OFFSET : BCM2837_GPIO_CLR0_OFFSET); ledstate[pin-2 ] = stat; iowrite32(1 << pin, reg); } ssize_t rgbled_read (struct file* filp, char __user* buf, size_t len, loff_t * off) { int rc = 0 ; int i = 0 ; if (*off > 0 ) { return 0 ; } for (i = 0 ; i < sizeof (colors) / sizeof (colors[0 ]); i++) { const char * name = colors[i].name; const bool * pins = colors[i].pins; if (ledstate[0 ] == pins[0 ] && ledstate[1 ] == pins[1 ] && ledstate[2 ] == pins[2 ]) { char color[32 ] = {0 }; sprintf (color, "%s\n" , name); *off = strlen (color); rc = copy_to_user(buf, color, *off); return rc < 0 ? rc : *off; } } return -EFAULT; } ssize_t rgbled_write (struct file* filp, const char __user* buf, size_t len, loff_t * off) { char color[32 ] = {0 }; int rc = 0 ; int i = 0 ; rc = copy_from_user(color, buf, len); if (rc < 0 ) { return rc; } *off = 0 ; for (i = 0 ; i < sizeof (colors) / sizeof (colors[0 ]); i++) { const char * name = colors[i].name; const bool * pins = colors[i].pins; if (!strncasecmp(color, name, strlen (name))) { gpioctl(LED_RED_PIN, pins[0 ]); gpioctl(LED_GREEN_PIN, pins[1 ]); gpioctl(LED_BLUE_PIN, pins[2 ]); return len; } } return -EINVAL; } long rgbled_ioctl (struct file* filp, unsigned int cmd, unsigned long arg) { if (cmd >= 2 && cmd <= 4 ) { gpioctl(cmd, arg); } else { return -ENODEV; } return 0 ; } const struct file_operations fops = { .owner = THIS_MODULE, .read = rgbled_read, .write = rgbled_write, .unlocked_ioctl = rgbled_ioctl, };
关于读/写/控制这三种操作的代码实现,看似复杂,其实都很容易理解,无非就是通过copy_to_user
和copy_from_user
两个函数,实现内核层与用户层之间的数据交互,剩下的事情不过就是在colors
结构体数组中进行遍历和比对而已。
然后是字符设备注册、GPIO功能配置等内容的实现。每种字符设备都需要唯一的主设备号和次设备号,设备号可以静态指定或动态分配,原则上建议由内核动态分配,避免冲突。
字符设备的创建有很多种思路,普通字符设备、混杂设备、平台设备等,它们都是内核提供的编程框架。例如GPIO这类设备,内核其实是有专门的gpio类,但为了更好的学习驱动开发,别着急,一步步来,先从最简单的开始(因为难的我也不会)。
下边的代码主要看cdev_xxx
相关的部分即可,驱动加载时配置好GPIO映射,注册字符设备,获取设备号;驱动卸载时,取消GPIO映射,释放设备号,注销字符设备。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 static dev_t devno = 0 ; static struct cdev cdev ; static int __init rgbled_init (void ) { int val = ~((7 << (LED_RED_PIN*3 )) | (7 << (LED_GREEN_PIN*3 )) | (7 << LED_BLUE_PIN*3 )); gpio = ioremap(BCM2837_GPIO_BASE, 0xB0 ); val &= ioread32(gpio + BCM2837_GPIO_FSEL0_OFFSET); val |= (1 << (LED_RED_PIN*3 )) | (1 << (LED_GREEN_PIN*3 )) | (1 << (LED_BLUE_PIN*3 )); iowrite32(val, gpio); if (alloc_chrdev_region(&devno, 0 , 1 , "rgbled" )) { printk(KERN_ERR"failed to register kernel module!\n" ); return -1 ; } cdev_init(&cdev, &fops); cdev_add(&cdev, devno, 1 ); printk(KERN_INFO"rgbled device major & minor is [%d:%d]\n" , MAJOR(devno), MINOR(devno)); return 0 ; } module_init(rgbled_init); static void __exit rgbled_exit (void ) { iounmap(gpio); cdev_del(&cdev); unregister_chrdev_region(devno, 1 ); printk(KERN_INFO"rgbled free\n" ); } module_exit(rgbled_exit);
代码基本就是这样。来看看效果吧,操作指令如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 philon@a55v:~/drivers/01-gpio_led$ make philon@a55v:~/drivers/01-gpio_led$ scp rgbled.ko rgbled_test rpi.local:/home/philon/modules philon@rpi:~/modules$ sudo insmod rgbled.ko philon@rpi:~/modules$ dmesg ... [ 106.818009] rgbled: no symbol version for module_layout [ 106.818028] rgbled: loading out-of-tree module taints kernel. [ 106.820307] rgbled device major&minor is [240:0] 👈主从设备号 philon@rpi:~/modules$ sudo mknod /dev/rgbled c 240 0 philon@rpi:~/modules$ sudo sh -c "echo green > /dev/rgbled" philon@rpi:~/modules$ sudo ./rgbled_test b 1 philon@rpi:~/modules$ sudo cat /dev/rgbled cyan
PS:动态图,只是被我设置得比较慢,别着急换台呀!😂