想法

之前在单片机上写过很多控制 GPIO 的代码,代码比较简单,也很好理解。同样的事情到了嵌入式 Linux 上似乎变得复杂了很多。这很不符合我的预期,我认为事物应该照着简单的方向发展(俗话说,高端的食材往往采用最朴素的烹饪方法。。咳咳。。扯远了)。不过我知道,嵌入式 Linux 是为了分层、复用、扩展性强、便于移植等方面才将 LED 的驱动和应用做成了那个(diao)样子。目前我没能感受到它的强大之处,说明我的等级还不够,还需要猥琐发育,我也期待能快点悟到嵌入式 Linux 驱动的真谛。不过,单片机出身的我,还是更迫切地想知道如何才能最简单、最直接地控制 LED,先有一个简单的实现方法,再向复杂的驱动靠拢,一步步去感悟驱动的美妙。

在 51 单片机上控制一个引脚输出高低电平非常简单,只需要给相应的寄存器赋值 0 或 1 就可以了,如下

#include <reg52.h>

sbit IN=P3^0;
sbit OUT=P1^0;

void main (void)
{
	while (1)
	{
		if(IN==0)
			OUT=0;
		else
			OUT=1;
	 }
}

在 STM32 上稍微复杂点,需要配置引脚的模式(通用、复用,输入、输出,上拉、下拉),还需要配置时钟线,不过也就这几个步骤,也都看得懂,说白了就是配置几个寄存器。那么,嵌入式 Linux 不管设计得多复杂,控制 GPIO 最终应该也就是配置几个寄存器而已,所以,我就想着在嵌入式 Linux 系统中直接操作寄存器来控制 GPIO 输出高低电平,最终还真被我实现了,下面我就讲一讲我实现的思路

devmem

思路很简单:找到读写寄存器的方法、确定 GPIO 的寄存器地址、将值写入寄存器。

首先要去查嵌入式 Linux 中有没有什么命令或方法能够直接读写寄存器,找到了 devmem 这个命令,用法是这样的

# devmem
BusyBox v1.33.0 (2021-11-28 00:41:57 CST) multi-call binary.

Usage: devmem ADDRESS [WIDTH [VALUE]]

Read/write from physical address

        ADDRESS Address to act upon
        WIDTH   Width (8/16/...)
        VALUE   Data to be written

例:devmem 0x12345678 就能读出地址为 0x12345678 的寄存器中的数据。并且,看到 devmem 是 busybox 中的程序,所以后面可以阅读 busybox 源码进一步探索该命令原理。

寄存器地址

接下来就要查找控制 GPIO 需要配置哪些寄存器,这就需要阅读芯片的数据手册了。我使用的是树莓派 3B+,主芯片是 BCM2837,由于没找到 BCM2837 的 DataSheet,只找到了 BCM2835 的,不过问题不大,大体一样,不一样的地方在网上查查资料也可以知道,后面我也会提到。

首先,选定要控制的引脚,这里我选择控制 Header 的 7 引脚,对应 BCM 的 4 pin 脚。

img

控制这个引脚需要操作三个寄存器:GPFSLn(GPIO Function Select Registers,GPIO 功能选择寄存器)、GPSETn(GPIO Pin Output Set Registers,输出设置寄存器)、GPCLRn(GPIO Pin Output Clear Registers,输出清除寄存器)。再结合引脚 4,可得上述三个寄存器的地址分别为:0x7E200000(功能选择寄存器)、0x7E20001C(输出设置寄存器)、0x7E200028(输出清除寄存器)。

不过,这三个地址为总线地址,而 devmen 访问的是物理地址,所以我们需要知道这两个地址的映射(换算)关系,看下图

树莓派寄存器地址

从图中可知,I/O 外设的总线起始地址为 0x7E000000,对应的物理地址为 0x3F000000(上图是 BCM2835 芯片的映射关系,我们用的是 BCM2837,所以对图片做了一点修正)。同理可得,刚才的三个寄存器映射到物理地址分别为:0x3F200000、0x3F20001C、0x3F200028。

读取

下面我们就来读取一下这三个地址中的值

# devmem 0x3f200000
0x00000000
# devmem 0x3f20001C
0x6770696F
# devmem 0x3f200028
0x6770696F

进一步的,功能选择寄存器是每个 GPIO 用 3 bits 来表示,从 000-111 分别代表 8 个不同的功能,我们用的是 4 引脚,对应 14-12bit,所以我们只需要读取 0x3f200000 寄存器的低 16bits 就可以了,可以使用下面这个命令

# devmem 0x3f200000 16
0x0000

树莓派GPFSL

输出设置寄存器和输出清除寄存器,每一引脚对应 1 位,我们只要关系 bit4 就可以了,也就是我们只需要读写 1 字节就行了,

命令如下:

# devmem 0x3f20001C 8
0x6F
# devmem 0x3f200028 8
0x6F

树莓派GPSET

设置

一切准备就绪,那我们就开始设置,看能不能点亮 LED

先将 4 脚配置成输出模式,命令如下,对应 14-12bit 的值为 1,即设置为输出模式

# devmem 0x3f200000 16 0x1000

刚下完命令,LED 就亮了(因为 LED 负极接在了 4 引脚上,且引脚默认输出低电平),见下图

树莓派LED

设置 4 引脚为高电平

# devmem 0x3f20001C 8 0x7F

LED 就灭了,见下图

树莓派LED灭

再次设置 4 引脚为低电平

# devmem 0x3f200028 8 0x7F

LED 又亮了

成功!

参考

树莓派GPIO驱动原理