GPIO 也就是通用输入/输出外设,在嵌入式开发中会经常遇到的一种外部设备。通过使用软件控制 GPIO 可实现对外输出电平信号;通过读取 GPIO 管脚上的信号可在软件中获取外部信息。操作 GPIO 外设的方法很多,这里将介绍一种在 Linux 环境中通过 mmap
系统调用来完成控制的方法。
软硬件环境
- CPU: iMX6Q(ARM)
- Kernel: Linux 4.19.72
- OS: Ubuntu18.04
GPIO 控制流程简介
iMX6Q 芯片手册上有如下描述:
由此可知读取 GPIO 某一个管脚上的信号分为3个步骤:
- 配置管脚复用寄存器,以将此管脚设置为 GPIO 模式。
- 配置管脚方向寄存器,设置此管脚为输入。
- 从管脚数据寄存器中读取值。
反之,如果要向外输出信号则为以下3步骤:
- 配置管脚复用寄存器,以将此管脚设置为 GPIO 模式。
- 配置管脚方向寄存器,设置此管脚为输出。
- 向管脚数据寄存器写入值。
寄存器地址
iMX6Q 有7组 GPIO 每组有8个 32-bit 的寄存器,每个寄存器上 1-bit 控制着其对应的1个管脚的配置,也就是每组 GPIO 有32个管脚,每个管脚有8个配置项。关于 iMX6Q GPIO 更为详细的介绍可参阅 Definitive GPIO guide。
对于只使用输入/输出功能的情况,8个寄存器中我们只需关心数据寄存器和方向寄存器这2个(对于管脚复用寄存器,因为默认状态下几乎所有管脚都是 GPIO 模式所以基本不用关心,具体情况请参考芯片手册)。
GPIO 控制寄存器的基地址为 0x0209C000
,地址宽度为 0x4000
也就是第一组 GPIO 的控制寄存器在地址 0x0209C000
上,第二组在 0x020A0000
,以此类推。每一个寄存器是 32-bit 大小所以,第一组 GPIO 的数据寄存器在 0x0209C000
方向寄存器在 0x0209C004
,如下图:
由此可以在代码中做如下定义:
1 | const ( |
对于指定的某一组 GPIO 的寄存器地址可这样获得:
1 | func getGPIOMMapper(group int) ([]uint32, error) { |
内存地址转换
当程序运行起来后操作系统为每一个进程分配了一个虚拟地址空间,而寄存器的地址在实地址空间中,所以需要使用内存地址转换将实地址映射到虚拟地址空间,这样我们才能通过内存地址访问 GPIO 寄存器。
执行终端命令 man mmap
可查阅关于 mmap 详细的资料,这里我们通过 Go 语言 unix
包中封装的 Mmap
函数来使用 mmap 系统调用,如下:
1 | func getGPIOMMapper(group int) ([]uint32, error) { |
其中指定映射的地址长度为8字节,原因是这里我们只使用前2个寄存器。memMapFile
可由如下函数获得:
1 | func initGPIOMemMap() *os.File { |
完成地址转换之后 unix.Mmap
函数返回一个 byte
类型的切片,而每个寄存器是 32-bit 宽度,也就是一个寄存器对应 4 个 byte,这样操作起来比较麻烦。为了简化操作可以将 []byte
转换为 []uint32
类型:
1 | func byte2uint32(b []byte) []uint32 { |
通过 GPIO 外设输出或读取值
完成以上操作之后,假定得到的内存映射保存在 var mapper []uint32
中。如果我们要设置管脚 10 为输出,并且对外输出高电平,则可通过以下操作完成:
1 | // 设置方向寄存器中第 10 bit 为1表示管脚10为输出 |
如果我们要设置管脚 10 为输入,并且读取管脚上的电平信息,则可通过以下操作完成:
1 | // 设置方向寄存器中第 10 bit 为0表示管脚10为输入 |
总结
本文介绍了 GPIO 的基本操作、iMX6Q GPIO 外设寄存器的分布以及 Go 语言中使用内存映射的方法,最后实现了使用 Go 语言对 GPIO 进行控制。对于以上的过程我们可以进一步将其封装成 struct
方便使用,类似如下:
1 | type GPIOPin struct { |
除了内存映射之外,还可以使用其他方法操作 GPIO,例如通过 sysfs
文件系统或者使用 /dev/gpiochip
设备文件等,这里就不做过多介绍了。