收藏

IMX6ULL学习笔记(14)——GPIO接口使用(C语言方式)

2022-12-24  本文已影响0人  Leung_ManWah

一、GPIO简介

i.MX6ULL 芯片的 GPIO 被分成 5 组,并且每组 GPIO 的数量不尽相同,例如 GPIO1 拥有 32 个引脚, GPIO2 拥有 22 个引脚, 其他 GPIO 分组的数量以及每个 GPIO 的功能请参考 《i.MX 6UltraLite Applications Processor Reference Manual》 第26章General Purpose Input/Output (GPIO)(P1133)


通过 GPIO 硬件结构框图,就可以从整体上深入了解 GPIO 外设及它的各种应用模式。

1.1 IO命名

打开 i.MX6ULL 参考手册的第 32 章“Chapter 32: IOMUX Controller(IOMUXC)”


i.MX6ULL 的 IO 分为两类:SNVS 域的和通用的,这两类 IO 本质上都是一样的。

“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00”的就是 GPIO 命名,命名形式就是“IOMUXC_SW_MUC_CTL_PAD_XX_XX”,后面的“XX_XX”就是 GPIO 命名,比如:GPIO1_IO01、UART1_TX_DATA、JTAG_MOD 等等。他是 根据某个 IO 所拥有的功能来命名的。比如我们一看到 GPIO1_IO01 就知道这个肯定能做 GPIO,看到 UART1_TX_DATA 肯定就知道这个 IO 肯定能做为 UART1 的发送引脚。

IO 复用功能。 i.MX6ULL 除了 GPIO1_IO00~GPIO1_IO09 引脚外,其它 IO 也是可以复用为 GPIO 功能。同样的,GPIO1_IO00~GPIO_IO09 也是可以复用为其它外设引脚。

1.2 IO复用

IOMUX 译为 IO 复用选择器。i.MX6ULL 的芯片每个 GPIO 都通过 IOMUX 支持多种功能, 例如一个 IO 可用于网络外设 ENET 的数据接收引脚,也可以被配置成 PWM 外设的输出引脚, 这样的设计大大增加了芯片的适用性,这样可选的功能就是由 IOMUX 实现的。IOMUX 相当于增加了多根内部信号线与 IO 引脚相连,最多有 8 根,也就是说一个 IO 最多可支持 8 种可选的功能

以“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00”这个 IO 为例,打开参考手册的 1568 页。



可以看到有个名为:IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00 的寄存器,寄存器地址为 0X020E005C,这个寄存器是 32 位的,但是只用到了最低 5 位,其中 bit0~bit3(MUX_MODE) 就是设置 GPIO1_IO00 的复用功能的。GPIO1_IO00 一共可以复用为 9 种功能 IO,分别对应 ALT0~ALT8,其中 ALT5 就是作为 GPIO1_IO00。GPIO1_IO00 还可以作为 I2C2_SCL、GPT1_CAPTURE1、ANATOP_OTG1_ID 等。

1.3 IO配置

IOMUX 由其左侧的 IOMUXC 控制(C表示Controler),IOMUXC 提供寄存器给用户进行配置, 它又分成 MUX Mode(IO模式控制) 以及 Pad Settings(Pad配置) 两个部分:

在 IOMUXC 外设中关于 MUX Mode 和 Pad Settings 寄存器命名格式如下:

IOMUXC控制类型 寄存器名称
MUX Mode IOMUXC_SW_MUX_CTL_PAD_XXXX
Pad Settings IOMUXC_SW_PAD_CTL_PAD_XXXX

每个引脚都包含这两个寄存器,表中的XXXX表示引脚的名字

1.3.1 MUX Mode配置

MUX Mode 就是用来配置引脚的复用功能,即选择引脚具体是用于网络外设 ENET 的数据接收, 还是用于 PWM 外设的输出引脚,当然,也可以配置成普通的 IO 口,仅用于控制输出高低电平。

以 GPIO1_IO04 引脚为例对 MUX 寄存器进行说明,该引脚相应的 MUX 寄存器在参考手册中的描述如下:

该寄存器主要有两个配置域,分别是 SIONMUX_MODE

1.3.2 Pad Settings配置

Pad Settings 用于配置引脚的属性,例如驱动能力,是否使用上下拉电阻, 是否使用保持器,是否使用开漏模式以及使用施密特模式还是CMOS模式等。

以 GPIO1_IO04 引脚中 PAD 寄存器在参考手册中的描述如下:

相对来说 PAD 寄存器的配置项就更丰富了,而且图中仅是该寄存器的部分说明,如 HYS 设置使用施密特模式的滞后功能,PUS 配置上下拉电阻的阻值, 其它的还包含PUE、PKE、ODE、SPEED、DSE 及 SRE 的配置。

1.3.3 PAD(可跳过不看)

PAD 代表了一个 i.MX6ULL 的 GPIO 引脚。在它的左侧是一系列信号通道及控制线,如 input_on 控制输入开关,Dir 控制引脚的输入输出方向,Data_out 控制引脚输出高低电平,Data_in 作为信号输入,这些信号都经过一个 IOMUX 的器件连接到左侧的寄存器。

①PAD引脚
代表一个i.MX6ULL的引脚。
②输出缓冲区
当输出缓冲区使能时,引脚被配置为输出模式。在输出缓冲区中,又包含了如下的属性配置:

③输入缓冲区
当输入缓冲区使能时,引脚被配置为输入模式。在输入缓冲区中,又包含了如下的属性配置:

④Pull/Keeper上下拉、保持器
引脚的控制逻辑中还包含了上下拉、保持器的功能。芯片内部的上拉和下拉电阻可以将不确定的信号钳位在高、低电平,或小幅提高的电流输出能力,上拉提供输出电流,下拉提供输入电流。注意这些上下拉配置只是弱拉,对于类似I2C之类的总线,还是必须使用外部上拉电阻。i.MX6ULL芯片的电源模块中包含转换器,当转换器停止工作时,保持器会保持输入输出电压。

上下拉、保持器可以通过如下属性配置:

注意,当引脚被配置为输出模式时,不管上下拉、保持器是什么配置,它们都会被关闭。

1.4 GPIO配置

GPIO 模块是每个 IO 都具有的外设,它具有 IO 控制最基本的功能,如输出高低电平、检测电平输入等。 它也占用 IOMUX 分配的复用信号,也就是说使用 GPIO 模块功能时同样需要使用 IOMUX 选中 GPIO 外设,对其 GPIO 的功能进行配置。


1.4.1 GDIR方向寄存器

设置某个 IO 的工作方向。控制一个 GPIO 引脚时,要先用 GDIR 方向寄存器配置该引脚用于输出电平信号还是用作输入检测。 典型的例子是使用输出模式可以控制LED灯的亮灭,输入模式时可以用来检测按键是否按下。

GDIR 寄存器的每一个数据位代表一个引脚的方向,对应的位被设置为0时该引脚为输入模式,被设置为1时该引脚为输出模式。

例如,对 GPIO1 的 GDIR 寄存器的 bit3 位被写入为 1,那么 GPIO1.3 引脚的模式即为输出。

1.4.2 DR数据寄存器

DR 数据寄存器直接代表了引脚的电平状态,它也使用 1 个数据位表示 1 个引脚的电平,每位用 1 表示高电平,用 0 表示低电平。

当 GDIR 方向寄存器设置引脚为输出模式时,写入 DR 数据寄存器对应的位即可控制该引脚输出的电平状态, 如这时 GPIO1 的 DR 寄存器的 bit4 被写入为 1,则引脚为输出高电平。

当 GDIR 方向寄存器设置引脚为输入模式时,读取 DR 数据寄存器对应的位即可获取该引脚当前的输入电平状态,例如这里读取 GPIO1 的DR寄存器的 bit4,得到该位的值为 0,表示当前引脚的输入状态为低电平。


1.4.3 PSR引脚状态寄存器

读取相应的位即可获取对应的 GPIO 的状态,也就是 GPIO 的高低电平值。PSR 引脚状态寄存器相当于 DR 寄存器的简化版,它仅在 GDIR 方向寄存器设置为输入模式时有效,它的每个位表示一个引脚当前的输入电平状态。PSR 寄存器的权限是只读的,对它进行写操作是无效的。

特别地,当引脚被配置成输出模式时,若 IOMUXC 中的 MUX 寄存器使能了 SION 功能(输出通道回环至输入), 可以通过 PSR 寄存器读取回引脚的状态值。

二、引脚确定

我使用的是 野火_EBF6ULL S1 Pro 开发板


从原理图可看到 RGB 灯的三个阴极 R、G、B 连接分别连接至标号 GPIO_4CSI_HSYNCCSI_VSYNC, 这些标号实际上与配套核心板上 i.MX6ULL 芯片的引脚相连。由于引脚功能众多, 绘制原理图时不可避免地无法完全表示引脚信息的所有信息。而无论是具体的引脚名还是复用功能, 我们都无法直接得知这些具体是 i.MX6ULL 芯片的哪个引脚。我们需要知道这些引脚是对应的具体 GPIO,这样我们才能编写程序进行控制。

由于还不清楚标号 GPIO_4CSI_HSYNCCSI_VSYNC 的具体引脚名,我们首先要在核心板原理图中查看它与 i.MX6ULL 芯片的关系。打开 《野火_EBF6ULL S1 邮票孔核心板V1.0原理图》,在PDF阅读器的搜索框输入前面的 GPIO_4CSI_HSYNCCSI_VSYNC 标号。


查找到了 GPIO_4 信号的具体引脚名为 GPIO1_IO04。 但是当我们使用同样的方法查找时发现只能找到 CSI_HSYNCCSI_VSYNC, 并没有我们熟悉的 GPIOx_IOx 标注的引脚名。这两个引脚默认情况下不用作 GPIO,而是用作摄像头的某一功能引脚,但是它可以复用为 GPIO,我们怎么找到对应的 GPIO 呢?

经查阅,我们把以上连接 LED 灯的各个 i.MX6ULL 芯片引脚总结出如表:

LED灯 原理图的标号 具体引脚名 GPIO端口及引脚编号
R灯 GPIO_4 GPIO1_IO04 GPIO1_IO04
G灯 CSI_HSYNC CSI_HSYNC GPIO4_IO20
B灯 CSI_VSYNC CSI_VSYNC GPIO4_IO19

三、编写启动文件

在 Ubuntu 下创建 start.S 文件用于编写启动文件。
在汇编文件中设置“栈地址”并执行跳转命令跳转到main函数执行C代码。

3.1 完整代码

/***********************第一部分*********************/
  .text            //代码段
  .align 2         //设置2字节对齐
  .global _start   //定义一个全局标号

/*************************第二部分*************************/
  _start:          //程序的开始
    b reset      //跳转到reset标号处

/*************************第三部分*************************/
reset:
   mrc     p15, 0, r0, c1, c0, 0     /*  将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中   */
   bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */
   bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */
   bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */
   bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */
   bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */
   mcr     p15, 0, r0, c1, c0, 0     /*  将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中   */

/***********************第四部分*********************/
      ldr sp, =0x84000000   //设置栈地址64M
      b main                //跳转到main函数

/***********************第五部分*******************/
    /*进入死循环*/
  loop:
      b loop

3.2 分析代码

/*************************第一部分*************************/
.text            //代码段
.align 2         //设置2字节对齐
.global _start   //定义一个全局标号
/*************************第二部分*************************/
_start:          //程序的开始
   b reset      //跳转到reset标号处
/*************************第三部分*************************/
reset:
   mrc     p15, 0, r0, c1, c0, 0     /*  将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中   */
   bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */
   bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */
   bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */
   bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */
   bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */
   mcr     p15, 0, r0, c1, c0, 0     /*  将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中   */
/***********************第四部分*********************/
      ldr sp, =0x84000000   //设置栈地址64M
      b main                //跳转到main函数
/***********************第五部分*******************/
  /*进入死循环*/
  loop:
      b loop

四、编程流程

1. 开启GPIO时钟
2. 设置引脚的复用功能以及引脚属性
3. 设置引脚方向以及输出电平

五、编写C语言代码

在 Ubuntu 下创建 led.c 文件用于驱动 LED 闪烁。

5.1 完整代码

//时钟控制寄存器
#define CCM_CCGR1 (volatile unsigned long*)0x20C406C
//GPIO1_04复用功能选择寄存器
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E006C
//PAD属性设置寄存器
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E02F8
//GPIO方向设置寄存器
#define GPIO1_GDIR (volatile unsigned long*)0x0209C004
//GPIO输出状态寄存器
#define GPIO1_DR (volatile unsigned long*)0x0209C000

#define uint32_t  unsigned int

/*简单延时函数*/
void delay(uint32_t count)
{
   volatile uint32_t i = 0;
   for (i = 0; i < count; ++i)
   {
      __asm("NOP"); /* 调用nop空指令 */
   }
}

int main()
{
   *(CCM_CCGR1) = 0xFFFFFFFF;                     //开启GPIO1的时钟
   *(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04) = 0x5;     //设置PAD复用功能为GPIO
   *(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04) = 0x1F838; //设置PAD属性
   *(GPIO1_GDIR) = 0x10;                          //设置GPIO为输出模式
   *(GPIO1_DR) = 0x0;                             //设置输出电平为低电平

   while(1)
   {
      *(GPIO1_DR) = 0x0;
      delay(0xFFFFF);
      *(GPIO1_DR) = 1<<4;
      delay(0xFFFFF);
   }
   return 0;
}

5.2 分析代码

//时钟控制寄存器
#define CCM_CCGR1 (volatile unsigned long*)0x20C406C
//GPIO1_04复用功能选择寄存器
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E006C
//PAD属性设置寄存器
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E02F8
//GPIO方向设置寄存器
#define GPIO1_GDIR (volatile unsigned long*)0x0209C004
//GPIO输出状态寄存器
#define GPIO1_DR (volatile unsigned long*)0x0209C000
/*简单延时函数*/
void delay(uint32_t count)
{
   volatile uint32_t i = 0;
   for (i = 0; i < count; ++i)
   {
      __asm("NOP"); /* 调用nop空指令 */
   }
}
CCM_CCGR1[26:27]的值 时钟状态描述
00 时钟在所有模式下都是关闭的
01 时钟在运行模式下为开,但在等待和停止模式下为关
10 保留
11 除停止模式外,时钟一直开启

我们将CCM_CCGR1[26:27]设置为11(二进制)即可。仔细观察可以发现发现CCM_CCGR1寄存器默认全为1,即默认开启了时钟。 为了程序规范我们再次使用代码开启时钟。将CCM_CCGR1寄存器设置全为1

从上图可知IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04[MUX_MODE]=0101(二进制)时GPIO1_04复用功能是GPIO。所以在程序中我们将0x5写入该寄存即可。

*(CCM_CCGR1) = 0xFFFFFFFF;                     //开启GPIO1的时钟
*(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04) = 0x5;     //设置PAD复用功能为GPIO
*(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04) = 0x1F838; //设置PAD属性
*(GPIO1_GDIR) = 0x10;                          //设置GPIO为输出模式
*(GPIO1_DR) = 0x0;                             //设置输出电平为低电平
while(1)
{
  *(GPIO1_DR) = 0x0;
  delay(0xFFFFF);
  *(GPIO1_DR) = 1<<4;
  delay(0xFFFFF);
}

六、编写链接脚本

写好的代码(无论是汇编还是C语言)都要经过编译、汇编、链接等步骤生成二进制文件或者可供下载的文件。在编译阶编译器会对每个源文件进行语法检查并生成对应的汇编语言,汇编是将汇编文件转化为机器码。

使用 arm-none-eabi-gcc -g -c led.S -o led.o 命令完成源码的编译、汇编工作,生成了 .o文件。编译和汇编是针对单个源文件,也就编译完成后一个源文件(.c.S.s)对应一个 .o 文件。程序链接阶段就会将这些 .o 链接成一个文件。

链接脚本的作用就是告诉编译器怎么链接这些文件,比如那个文件放在最前面,程序的代码段、数据段、bss段分别放在什么位置等等。

在 Ubuntu 下创建 led.lds 链接脚本。

6.1 完整代码

 ENTRY(_start)
 SECTIONS {
   . = 0x80000000;

   . = ALIGN(4);
   .text :
   {
   start.o (.text)
   *(.text)
   }

   . = ALIGN(4);
   .data :
   {
   *(.data)
   }

   . = ALIGN(4);
   .bss :
   {
   *(.bss)
   }
 }

6.2 分析代码

 ENTRY(_start)
 SECTIONS {
···
···
}
. = 0x80000000;
   . = ALIGN(4);
   .text :
   {
   start.o (.text)
   *(.text)
   }
   . = ALIGN(4);
   .data :
   {
   *(.data)
   }
. = ALIGN(4);
   .bss :
   {
   *(.bss)
   }

七、编写makefile文件

程序编写完成后需要依次输入编译、链接、格式转换命令才能最终生成二进制文件。这种编译方式效率低、容易出错。

使用makefile只需要在所在文件夹下执行make命令,makefile工具便会自动完成程序的编译、链接、格式转换等工作。正常情况下我们可以在当前目录看到生成的一些中间文件以及我们期待的.bin文件。

在 Ubuntu 下创建 makefile 文件。

7.1 完整代码

 all: start.o led.o
    arm-none-eabi-ld -Tled.lds  $^ -o led.elf
    arm-none-eabi-objcopy -O binary -S -g led.elf led.bin

  %.o : %.S
    arm-none-eabi-gcc -g -c $^ -o start.o
  %.o : %.c
    arm-none-eabi-gcc -g -c $^ -o led.o


  .PHONY: clean
  clean:
    rm *.o *.elf *.bin

7.2 分析代码

 all: start.o led.o
    arm-none-eabi-ld -Tled.lds  $^ -o led.elf
    arm-none-eabi-objcopy -O binary -S -g led.elf led.bin
  %.o : %.S
    arm-none-eabi-gcc -g -c $^ -o start.o
  %.o : %.c
    arm-none-eabi-gcc -g -c $^ -o led.o
  .PHONY: clean
  clean:
    rm *.o *.elf *.bin

八、编译下载验证

8.1 编译代码

make

执行make命令,生成led.bin文件。

8.2 代码烧写

编译成功后会在当前文件夹下生成.bin文件,这个.bin文件也不能直接放到开发板上运行, 这次是因为需要在.bin文件缺少启动相关信息。

为二进制文件添加头部信息并烧写到SD卡。查看 IMX6ULL学习笔记(12)——通过SD卡启动官方SDK程序

进入烧写工具目录,执行 ./mkimage.sh <烧写文件路径> 命令,例如要烧写的 led.bin 位于 home 目录下,则烧写命令为 ./mkimage.sh /home/led.bin

执行上一步后会列出linux下可烧写的磁盘,选择你插入的SD卡即可。这一步 非常危险!!!一定要确定选择的是你插入的SD卡!!,如果选错很可能破坏你电脑磁盘内容,造成数据损坏!!! 确定磁盘后SD卡以“sd”开头,选择“sd”后面的字符即可。例如要烧写的sd卡是“sdb”则输入“b”即可。

8.3 实验现象

将开发板设置为SD卡启动,接入SD卡,开发板上电,可以看到开发板RGB红灯闪烁。


• 由 Leung 写于 2022 年 12 月 25 日

• 参考:5. LED灯进阶——C语言实现

上一篇下一篇

猜你喜欢

热点阅读