[ARM裸机开发] NXP官方SDK包使用以及BSP工程管理
文章标题中所说的“SDK包使用”,并不是真正介绍 i.MX 6ULL SDK 三方库的使用,而是仅仅利用到里面寄存器组的结构体定义。
相比于自己裸机程序里面写的各个地址常数,SDK 包里面的 define 定义更具可读性,而且因为结构体的封装可以让人更容易理解寄存器组的定义。同时仅仅是使用寄存器的定义并不会抽象和脱离本身的裸机开发,这点也是最重要的,因为毕竟现在在学 ARM 裸机开发。
最后文章会记录一下 BSP 工程管理,说明如何有条理的组织代码文件、如何提高代码复用性,可以算是“锦上添花”。
1. i.MX 6ULL SDK 包
首先我们需要在 NXP 官网上下载 SDK 包:下载链接(如图 1 所示位置)。之后像安装软件一样就能得到 SDK 源码。因为只需要寄存器的定义,从而方便裸机开发,所以其实只需要用到下面三个文件。从目录和文件名上也能大致猜测到:第一个是总体头文件;第二个是通用寄存器相关文件;第三个是寄存器复用相关头文件。
- devices\MCIMX6Y2\MCIMX6Y2.h
- devices\MCIMX6Y2\drivers\fsl_common.h
- devices\MCIMX6Y2\drivers\fsl_iomuxc.h

1.1 代码原理
代码需要实现的功能是 LED 灯交替亮灭。实现的思路和原理在之前的 [ARM裸机开发] 汇编语言LED灯实验 文章中已经介绍过,但因为过了挺长时间的,这边重新回顾一下,温故而知新。
代码需要完成三个步骤:(1)IO 复用设置;(2)IO 设置;(3)GPIO 设置;(4)GPIO 时钟使能。同时还需要使用汇编语言写一段“引导”程序跳转到 C 语言的 main 函数。
IO 复用设置。如图 2 所示,可以在 《i.MX 6ULL Applications Processor Reference Manual》 用户手册的第 32 章看到众多 IO 复用功能的寄存器,它们的命名格式为 IOMUXC_SW_MUX_CTL_PAD_<PADNAME>。因为当前实验连接 LED 灯的管脚是 GPIO1_03 口,所以需要使用 SW_MUX_CTL_PAD_GPIO1_IO03 寄存器进行配置。

IO 设置。如图 3 所示,在图 2 往后一点的位置可以看到 IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME> 格式的寄存器。这些寄存器就是用来设置 IO 功能的,比如设置上拉电阻和 IO 速率等等。此时 GPIO1_03 口对应的寄存器为 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 。

GPIO 设置。注意,GPIO 是 IO 的子集,它只是众多复用功能中的一种,所以还需要单独设置一下。用户手册的第 28 章专门讲 GPIO,在图 4 可以看到,每组 GPIO 有 DR、GDIR、PSR、ICR1、ICR2、IMR、ISR、EDGE_SEL 这 8 个寄存器需要设置。DR(data) (指定) IO 输入/输出的电平;GDIR(direction) 控制 IO 是输入还是输出;PSR(pad status) 可以读取 IO 电平;ICR1/ICR2(interrupt configuration) 设置中断的触发方式;IMR(interrupt mask) 中断掩码控制中断是否使能;ISR(interrupt status) 可以读取中断是否发生并且可以清除;EDGE_SEL(edge select) 可以进行更详细的边沿触发设置。

GPIO 时钟使能。如果要使用某个外设,必须要使能其外设时钟,GPIO 也不例外。用户手册的第 18 章专门讲时钟系统,其中的 CCM_CCGR0~CCM_CCGR6 这 7 个寄存器控制着所有外设时钟的开关。这些寄存器每两位控制一个外设的开关,为了方便代码编写,可以全部置 1,代表开启所有的外设时钟。
1.2 利用 SDK 头文件编写代码
在温习了一遍原理之后,就可以使用 SDK 头文件编写代码了。start.s 汇编部分的“引导”文件内容不变,只是起到选择运行模式和跳转到 C 语言 main 函数的作用。这边要重写 C 语言部分的代码:
首先引入本章开头所说的三个头文件。
- #include "MCIMX6Y2.h"
- #include "fsl_common.h"
- #include "fsl_iomuxc.h"
外设时钟使能部分的代码方便了很多,并且非常统一。其中使用 CCM 变量来代表时钟控制模块,CCM 是一个 CCM_Type 结构体。CCM_Type 结构体成员众多,这边没有具体给出,内容和图 5 用户手册里面的各个寄存器地址排布完全相同。同时对着图 5 可以看到,首地址(CCM_BASE)等于 0x20C4000u 也是正确的。
- void clk_enable()
- {
- CCM->CCGR0 = 0xFFFFFFFF;
- CCM->CCGR1 = 0xFFFFFFFF;
- CCM->CCGR2 = 0xFFFFFFFF;
- CCM->CCGR3 = 0xFFFFFFFF;
- CCM->CCGR4 = 0xFFFFFFFF;
- CCM->CCGR5 = 0xFFFFFFFF;
- CCM->CCGR6 = 0xFFFFFFFF;
- }
- /** Peripheral CCM base address */
- #define CCM_BASE (0x20C4000u)
- /** Peripheral CCM base pointer */
- #define CCM ((CCM_Type *)CCM_BASE)

led_init 函数负责 IO 复用和属性设置的功能。IOMUXC_SetPinMux 函数负责 IO 的复用功能,IOMUXC_SetPinConfig 函数负责 IO 的属性设置。代码中这两个函数都使用了同一个 IOMUXC_GPIO1_IO03_GPIO1_IO03 宏,这是有道理的,因为确立了 IO 复用何种功能之后,自然就能确定相对应的属性设置寄存器:IOMUXC_GPIO1_IO03_GPIO1_IO03 代表复用 GPIO1_03 的功能(0x5U),相关 IO 复用寄存器地址为 0x020E0068U,则相应 GPIO1_03 复用功能的配置寄存器地址为 0x020E02F4U。
如代码中所示,IOMUXC_SetPinMux 函数将选择的复用模式值和 SION 位合并之后的内容赋值给复用寄存器,而 IOMUXC_SetPinConfig 将配置的值赋值给属性配置寄存器。
- void led_init()
- {
- IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
- IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10b0);
- GPIO1->GDIR |= (1 << 3);
- GPIO1->DR &= ~(1 << 3);
- }
- #define IOMUXC_GPIO1_IO03_GPIO1_IO03 0x020E0068U, 0x5U, 0x00000000U, 0x0U, 0x020E02F4U
- static inline void IOMUXC_SetPinMux(uint32_t muxRegister,
- uint32_t muxMode,
- uint32_t inputRegister,
- uint32_t inputDaisy,
- uint32_t configRegister,
- uint32_t inputOnfield)
- {
- *((volatile uint32_t *)muxRegister) =
- IOMUXC_SW_MUX_CTL_PAD_MUX_MODE(muxMode) | IOMUXC_SW_MUX_CTL_PAD_SION(inputOnfield);
- if (inputRegister)
- {
- *((volatile uint32_t *)inputRegister) = IOMUXC_SELECT_INPUT_DAISY(inputDaisy);
- }
- }
- static inline void IOMUXC_SetPinConfig(uint32_t muxRegister,
- uint32_t muxMode,
- uint32_t inputRegister,
- uint32_t inputDaisy,
- uint32_t configRegister,
- uint32_t configValue)
- {
- if (configRegister)
- {
- *((volatile uint32_t *)configRegister) = configValue;
- }
- }
GPIO 的控制和上面类似,是将相关 GPIO 寄存器封装在 GPIO_Type 结构体中,并且 GPIO1 变量指明了 GPIO1 组的首地址。因此,就可以直接方便的使用结构体里面定义的 GDIR 成员设置 GPIO 口是输入还是输出,DR 成员设置输出电平。
- void led_on()
- {
- GPIO1->DR &= ~(1<<3);
- }
- void led_off()
- {
- GPIO1->DR |= (1<<3);
- }
以上就是使用 SDK 头文件修改的部分,下面也将完整代码给出。可以看出使用 SDK 包里面定义的头文件,就不用对着手册多次繁琐的确认寄存器地址,并且封装的形式和函数也让人能从另一种层面理解芯片的设计。
- #include "MCIMX6Y2.h"
- #include "fsl_common.h"
- #include "fsl_iomuxc.h"
- void clk_enable()
- {
- CCM->CCGR0 = 0xFFFFFFFF;
- CCM->CCGR1 = 0xFFFFFFFF;
- CCM->CCGR2 = 0xFFFFFFFF;
- CCM->CCGR3 = 0xFFFFFFFF;
- CCM->CCGR4 = 0xFFFFFFFF;
- CCM->CCGR5 = 0xFFFFFFFF;
- CCM->CCGR6 = 0xFFFFFFFF;
- }
- void led_init()
- {
- IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
- IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10b0);
- GPIO1->GDIR |= (1 << 3);
- GPIO1->DR &= ~(1 << 3);
- }
- void led_on()
- {
- GPIO1->DR &= ~(1<<3);
- }
- void led_off()
- {
- GPIO1->DR |= (1<<3);
- }
- void delay_short(volatile unsigned int n)
- {
- while (n--) {}
- }
- void delay(volatile unsigned int n)
- {
- while (n--)
- {
- delay_short(0x7ff);
- }
- }
- int main()
- {
- clk_enable();
- led_init();
- while (1)
- {
- led_off();
- delay(2000);
- led_on();
- delay(2000);
- }
- return 0;
- }
1.3 代码编译
编译代码使用的 makefile 和链接脚本与之前使用的一致。需要注意的是,使用的 SDK 包里的三个头文件如果不加修改就编译的话,编译会产生错误,产生错误的原因是包含的头文件没有引入,或者是一些函数没有定义等等。解决编译错误的方法也比较简单,就是先放着编译,看命令行输出了什么错误提示,再进行相应的修改即可:比如缺少头文件的话,就将 include 的那行语句删除或者注释掉。
2. BSP 工程管理
虽然现在的代码量很小,但也存在一些问题:比如,如图 6 所示,工程文件看着比较杂乱;还有就是时钟使能、延迟操作等代码都杂糅在 main 文件中,不利于复用。基于以上问题,开发板附带教程中给出了一个参考目录结构,可以帮助重新组织一下文件和功能模块。

可以组织如下目录:
bsp 目录:存放驱动文件。比如第一章当中的时钟驱动、LED 驱动。
imx 目录:存放芯片相关的文件。比如第一章当中的 SDK 包中的头文件。
project 目录:存放应用相关的文件。比如第一章当中的 start.s 和 main.c 文件。
obj 目录:存放应用相关的文件。存放编译生成的 .o 文件。
2.1 重新提取 bsp 目录下文件
bsp 目录下的文件需要重新提取编写一下,以方便其他工程复用。基于第一章,提取出时钟驱动、LED驱动和延时功能(附带教程中称为 delay 驱动,但看代码跟硬件关联不大,暂且先叫做延时功能)。如下代码所示,时钟驱动对应的 bsp_clk.c 文件,延时功能对应的 bsp_delay.c 文件,LED 驱动对应的 bsp_led.c 文件。这些源文件内容都和第一章中的一样,只是按照模块功能重新提取了一下;同时源文件还对应着头文件,因为内容只是函数的简单声明,这边也就不摘录赘述了。
- #include "bsp_clk.h"
- void clk_enable(void)
- {
- CCM->CCGR0 = 0XFFFFFFFF;
- CCM->CCGR1 = 0XFFFFFFFF;
- CCM->CCGR2 = 0XFFFFFFFF;
- CCM->CCGR3 = 0XFFFFFFFF;
- CCM->CCGR4 = 0XFFFFFFFF;
- CCM->CCGR5 = 0XFFFFFFFF;
- CCM->CCGR6 = 0XFFFFFFFF;
- }
- #include "bsp_delay.h"
- void delay_short(volatile unsigned int n)
- {
- while(n--){}
- }
- void delay(volatile unsigned int n)
- {
- while(n--)
- {
- delay_short(0x7ff);
- }
- }
- #include "bsp_led.h"
- void led_init(void)
- {
- IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03,0);
- IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03,0X10B0);
- GPIO1->GDIR |= (1 << 3);
- GPIO1->DR &= ~(1 << 3);
- }
- void led_switch(int led, int status)
- {
- switch(led)
- {
- case LED0:
- if(status == ON)
- GPIO1->DR &= ~(1<<3);
- else if(status == OFF)
- GPIO1->DR |= (1<<3);
- break;
- }
- }
2.2 重写 main.c
project 目录下的 main.c 文件因为组织结构的变化需要重写一下。如下代码内容所示,可以发现内容更加简洁,同时流程也更加清晰——流程为首先使能外设时钟,接着初始化 LED 设备,最后循环控制 LED 设备亮灭。
- #include "bsp_clk.h"
- #include "bsp_delay.h"
- #include "bsp_led.h"
- int main()
- {
- clk_enable();
- led_init();
- while (1)
- {
- led_switch(LED0, ON);
- delay(500);
- led_switch(LED0, OFF);
- delay(500);
- }
- return 0;
- }
2.3 其余文件
没有编译之前,整个工程目录如下面的树形结构所示,除了 2-1 节中讲的 bsp 目录下的文件、 2-2 节中讲的 project 目录下的 main.c 文件以及 项目根目录下的 makefile 文件(在 2.4 节中进行说明)有大改动之外,其余文件基本没变。还有几个需要提及一下的文件是:imx/cc.h 头文件里面定义了一些 SDK 包头文件里需要使用到的,例如 __IO 宏,这些通用的定义;imx/imx.h 头文件是当前目录下其余头文件的封装,方便其余源文件一次性引入;因为 .o 文件输出在 obj 目录下面了,所以项目根目录下的 imx.lds 里面的 start.o 文件路径也需要改变一下(这个也不是特别需要关注的问题,因为编译的时候如果发生错误就自然会注意到)。
- │ imx.lds
- │ imxdownload
- │ makefile
- │
- ├─bsp
- │ ├─clk
- │ │ bsp_clk.c
- │ │ bsp_clk.h
- │ │
- │ ├─delay
- │ │ bsp_delay.c
- │ │ bsp_delay.h
- │ │
- │ └─led
- │ bsp_lec.c
- │ bsp_led.h
- │
- ├─imx
- │ cc.h
- │ fsl_common.h
- │ fsl_iomuxc.h
- │ imx.h
- │ MCIMX6Y2.h
- │
- ├─obj
- │
- └─project
- main.c
- start.s
2.4 新的 makefile 文件
根据工程目录结构重新编写的 makefile 文件很有学习和借鉴意义,下面就对这个 makefile 文件进行一下解析。
第 1 行至第 7 行比较容易理解,作用是设置交叉编译工具变量。
第 9 行至第 12 行将所有头文件所在目录列举在 INCDIRS 变量中。同时第 14 行使用 INCDIRS 变量设置 gcc 命令需要使用的 -I 选项,以确定头文件的搜索路径。此例子中 INCLUDE 变量展开的内容为 -I imx -I bsp/clk -I bsp/delay -I bsp/led
第 16 行至第 19 行将所有源文件所在目录列举在 SRCDIRS 变量中。源文件包括汇编文件和 C 语言文件,所有汇编文件列举在 SFILES 变量里,所有 C 语言文件列举在 CFILES 变量里。这边还要提及一下 foreach 函数,以当前这个例子举例,foreach 会依次将 SRCDIRS 变量里面的内容赋值给 dir 变量,之后第三个参数里面的 wildcard 函数会利用到每次的 dir 变量进行相关后缀名的文件匹配。
第 24 行至第 25 行得到汇编源文件和 C 语言源文件的文件名。这么做的原因是重新将源文件生成的 .o 文件放置在 obj 目录下,功能对应 makefile 中第 27 行至 29 行。此时依赖的 .o 文件都是在 obj 目录下,“丢失”了相应源文件所在的路径,那么 make 是如何定位到源文件的呢?第 31 行就是解决这个问题的,make 会在 VPATH 变量指定的目录下搜索相关的源文件。
第 33 行至第 36 行是生成 bin 文件的规则,和先前的 makefile 是一样的。
第 38 行至第 39 行是通过 .s 汇编文件生成 .o 文件的规则。这个规则使用了 静态模式,以这个例子举例,obj/%.o 模板会在 $(SOBJ) 列表中进行匹配,提取通配符匹配的内容,然后将其复制到依赖 %.s 当中。第 41 行至第 42 行是类似的,是通过 C 语言文件生成 .o 文件的规则。
静态模式具体可见 make 手册 。
最后第 44 行至第 46 行定义了 clean 伪目标,用于删除编译期间生成的中间文件。
- CROSS_COMPILE ?= arm-linux-gnueabihf-
- TARGET ?= bsp
- CC := $(CROSS_COMPILE)gcc
- LD := $(CROSS_COMPILE)ld
- OBJCOPY := $(CROSS_COMPILE)objcopy
- OBJDUMP := $(CROSS_COMPILE)objdump
- INCDIRS := imx
- INCDIRS += bsp/clk
- INCDIRS += bsp/delay
- INCDIRS += bsp/led
- INCLUDE := $(patsubst %, -I %,$(INCDIRS))
- SRCDIRS := project
- SRCDIRS += bsp/clk
- SRCDIRS += bsp/delay
- SRCDIRS += bsp/led
- SFILES := $(foreach dir,$(SRCDIRS),$(wildcard $(dir)/*.s))
- CFILES := $(foreach dir,$(SRCDIRS),$(wildcard $(dir)/*.c))
- SFILENAMES := $(notdir $(SFILES))
- CFILENAMES := $(notdir $(CFILES))
- SOBJ := $(patsubst %, obj/%,$(SFILENAMES:.s=.o))
- COBJ := $(patsubst %, obj/%,$(CFILENAMES:.c=.o))
- OBJS := $(SOBJ) $(COBJ)
- VPATH := $(SRCDIRS)
- $(TARGET).bin : $(OBJS)
- $(LD) -Timx.lds -o $(TARGET).elf $^
- $(OBJCOPY) -O binary -S $(TARGET).elf $@
- $(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis
- $(SOBJ) : obj/%.o : %.s
- $(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
- $(COBJ) : obj/%.o : %.c
- $(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
- .PHONY: clean
- clean:
- rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJ) $(SOBJ)
有了 makefile 文件就可以进行编译了,编译得到的 bin 文件的烧写步骤也与之前一样。由于 makefile 的通用性以及 BSP 工程的可复用性,所以之后都可以使用这个工程模板快速学习其他硬件模块的裸机驱动。