目录
[一. C语言实现IMX6ULL点灯](#一. C语言实现IMX6ULL点灯)
[1.1 main.h 文件的编写](#1.1 main.h 文件的编写)
[1.1.1 关键字 volatile 的作用](#1.1.1 关键字 volatile 的作用)
[1.2 main.c 文件的编写](#1.2 main.c 文件的编写)
[1.3 start.S文件的编写](#1.3 start.S文件的编写)
[二. SDK实现IMX6ULL点灯](#二. SDK实现IMX6ULL点灯)
[2.1 SDK库简介](#2.1 SDK库简介)
[2.2 使用 SDK 库编写点灯程序](#2.2 使用 SDK 库编写点灯程序)
[三. 链接脚本的编写](#三. 链接脚本的编写)
[3.1 内存段的总结](#3.1 内存段的总结)
[3.2 链接脚本](#3.2 链接脚本)
[四. BSP开发](#四. BSP开发)
[4.1 工程管理](#4.1 工程管理)
[4.2 对应的Makefile与链接脚本](#4.2 对应的Makefile与链接脚本)
一. C语言实现IMX6ULL点灯
1.1 main.h 文件的编写
上篇文章实现了汇编程序点灯,这次实现使用C语言点灯
但是还需要汇编语言来设置异常向量表,设置各种工作模式的栈区等,引导程序到C语言环境下运行
使用C语言配置寄存器的思路就是先定义寄存器,然后直接使用寄存器,在点灯程序中,要定义的寄存器如下,文件名为:main.h
cpp
#ifndef __MIAN_H__
#define __MIAN_H__
/时钟相关寄存器
#define CCM_CCGR0 *((volatile unsigned int*)0x020C4068)
#define CCM_CCGR1 *((volatile unsigned int*)0x020C406C)
#define CCM_CCGR2 *((volatile unsigned int*)0x020C4070)
#define CCM_CCGR3 *((volatile unsigned int*)0x020C4074)
#define CCM_CCGR4 *((volatile unsigned int*)0x020C4078)
#define CCM_CCGR5 *((volatile unsigned int*)0x020C407C)
#define CCM_CCGR6 *((volatile unsigned int*)0x020C4080)
/引脚复用配置寄存器
#define SW_MUX_GPIO1_IO03 *((volatile unsigned int*)0x020E0068)
/引脚电气属性配置寄存器
#define SW_PAD_GPIO1_IO03 *((volatile unsigned int*)0x020E02F4)
/引脚方向寄存器
#define GPIO1_GDIR *((volatile unsigned int*)0x0209C004)
/引脚数据寄存器
#define GPIO1_DR *((volatile unsigned int*)0x0209C000)
#endif
1.1.1 关键字 volatile 的作用
1、volatile修饰变量
volatile 关键字 是一种类型修饰符 ,用它声明的类型变量表示可以被某些编译器未知的因素更改 ,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化 ,从而可以提供对特殊地址的稳定访问。
声明时语法:int volatile vInt
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
例如下面这段代码:
cpp
volatile int i=10;
int a = i;
...
/ 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;
volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。
而优化做法是,由于编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
2、vola修饰指针
**作用:**通过指针间接访问的空间的内容一定不会被优化
eg:
每次读写必须访问真实内存,禁止优化
cpp
volatile char * vpch;
1.2 main.c 文件的编写
cpp
#include "main.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;
}
/初始化LED
void led_init(void)
{
SW_MUX_GPIO1_IO03 = 0x00000005;
SW_PAD_GPIO1_IO03 = 0x000010B0;
GPIO1_GDIR = 0x00000008;
GPIO1_DR = 0x00000000;
}
/LED亮
void led_on(void)
{
GPIO1_DR &= ~(1 << 3);
}
/LED灭
void led_off(void)
{
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(void)
{
clk_enable();
led_init();
while(1)
{
led_on();
delay(500);
led_off();
delay(500);
}
return 0;
}
1.3 start.S文件的编写
主要是启动代码
cpp
.global _start
_start:
//初始化异常向量表:存放异常跳转指令
ldr pc, =_reset_handler //0x00000000 复位异常跳转指令 --- 复位默认是SVC模式
ldr pc, =_undefined_handler //0x00000004 未定义指令异常跳转指令
ldr pc, =_svc_handler //0x00000008 复位或软中断异常跳转指令
ldr pc, =_prefetch_abort_handler //0x0000000C 指令存取错误异常跳转指令
ldr pc, =_data_abort_handler //0x00000010 数据存取错误异常跳转指令
ldr pc, =_reserved_handler //0x00000014 保留
ldr pc, =_irq_handler //0x00000018 普通中断异常跳转指令
ldr pc, =_fiq_handler //0x0000001C 快速中断异常跳转指令
_fiq_handler:
ldr pc, =_fiq_handler
_irq_handler:
ldr pc, =_irq_handler
_reserved_handler:
ldr pc, =_reserved_handler
_data_abort_handler:
ldr pc, =_data_abort_handler
_prefetch_abort_handler:
ldr pc, =_prefetch_abort_handler
_svc_handler:
ldr pc, =_svc_handler
_undefined_handler:
ldr pc, =_undefined_handler
_reset_handler:
//将工作模式切换到IRQ模式下
mrs r0, cpsr
bic r0, r0, #0x1F
orr r0, r0, #0x12
msr cpsr ,r0
//设置IRQ模式下的栈底指针,大小为32M
ldr sp, =0x86000000
//将工作模式切换到System模式下
mrs r0, cpsr
bic r0, r0, #0x1F
orr r0, r0, #0x1F
msr cpsr, r0
//设置System模式的栈底指针
ldr sp, =0x84000000
b main
bl finish
finish:
b finish
二. SDK实现IMX6ULL点灯
NXP 针对 I.MX6ULL 编写了一个 SDK 包,这个 SDK 包就类似于 STM32 的 STD 库或者HAL 库。库的意义在于将寄存器和对寄存器的操作用结构体和对应的函数加以封装,大大提高开发的效率,提高程序可读性等。
2.1 SDK库简介
从SDK包中提取五个文件即可:fsl_common.h 、 core_a7.h 、fsl_iomuxc.h 、 MCIMX6Y2.h 和 cc.h。
两个比较重要的头文件:
**MCIMX6Y2.h:**放的一些外设寄存器的定义等等,eg:
cpp
typedef struct {
__IO uint32_t DR;
__IO uint32_t GDIR;
__I uint32_t PSR;
__IO uint32_t ICR1;
__IO uint32_t ICR2;
__IO uint32_t IMR;
__IO uint32_t ISR;
__IO uint32_t EDGE_SEL;
} GPIO_Type;
cpp
#define GPIO1_BASE (0x209C000u)
#define GPIO1 ((GPIO_Type *)GPIO1_BASE)
#define GPIO2_BASE (0x20A0000u)
#define GPIO2 ((GPIO_Type *)GPIO2_BASE)
#define GPIO3_BASE (0x20A4000u)
#define GPIO3 ((GPIO_Type *)GPIO3_BASE)
#define GPIO4_BASE (0x20A8000u)
#define GPIO4 ((GPIO_Type *)GPIO4_BASE)
#define GPIO5_BASE (0x20AC000u)
#define GPIO5 ((GPIO_Type *)GPIO5_BASE)
**fsl_iomuxc.h:**这里面有配置引脚常用的一些函数,eg:
**inline关键字:**内联函数,这个函数将会被就地展开,要求代码少时才可以inline
cpp
/设置引脚复用
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;
}
}
引脚复用设置要用到一个函数:IOMUXC_SetPinMux
最后一个参数:SION
设置电气属性的函数:IOMUXC_SetPinConfig
最后一个参数,就是配置的值,默认:0x10B0
注意传参方式,这两个函数传参时第一个参数会将后面四个参数带进去,故只用传第一个参数和最后一个参数即可!
使用示例:
cpp
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
2.2 使用 SDK 库编写点灯程序
流程:
CCM(Clock Control Module): 开启外设时钟
IOMUXC(Input/Output Multiplexer Controller) : 复用功能 + 电气属性
GPIO: GDIR + DR
交叉工具链: arm-linux-gnueabihf-gcc
gcc 编译器
ld 链接器
objcopy 文件格式转换器
objdump 反汇编器
cpp
#ifndef __LED_H__
#define __LED_H__
/初始化LED
extern void led_init(void);
/LED亮
extern void led_on(void);
/LED灭
extern void led_off(void);
/LED状态翻转
extern void led_nor(void);
#endif
#include"MCIMX6Y2.h"
#include"fsl_iomuxc.h"
#include"led.h"
/初始化LED
void led_init(void)
{
/配置GPIO1_IO3复用为GPIO
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
/配置引脚电气属性
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
/配置GPIO方向寄存器,设置为输出模式
GPIO1->GDIR |= (1 << 3);
/初始化LED为灭
GPIO1->DR |= (1 << 3);
}
/LED亮
void led_on(void)
{
/引脚输出低电平,LED亮
GPIO1->DR &= ~(1 << 3);
}
/LED灭
void led_off(void)
{
/引脚输出高电平,LED灭
GPIO1->DR |= (1 << 3);
}
/LED状态翻转
void led_nor(void)
{
/引脚电平翻转
GPIO1->DR ^= (1 << 3);
}
主函数:
cpp
#include"MCIMX6Y2.h"
#include"fsl_iomuxc.h"
#include"led.h"
#include"beep.h"
/延时函数
void delay(unsigned int n)
{
while(n--)
{
}
}
/使能时钟
void enable_clocks(void)
{
CCM->CCGR0 = 0xFFFFFFFF;
CCM->CCGR1 = 0xFFFFFFFF;
CCM->CCGR2 = 0xFFFFFFFF;
CCM->CCGR3 = 0xFFFFFFFF;
CCM->CCGR4 = 0xFFFFFFFF;
CCM->CCGR5 = 0xFFFFFFFF;
CCM->CCGR6 = 0xFFFFFFFF;
}
int main(void)
{
enable_clocks();
led_init();
while(1)
{
led_nor();
delay(0x7FFFF);
}
return 0;
}
三. 链接脚本的编写
之前的Makefile中是通过"-Ttext"来指定链接地址是 0X87800000 的,这样的话所有的文件都
会链接到以 0X87800000 为起始地址的区域。但是有时候很多文件需要链接到指定的区域,或者叫做 段 里面。关于段的问题,就不得不简单说明一下elf 文件格式的问题了。
elf称为可执行、可链接文件 。简单的说linux系统中的可执行文件和静态库文件都是这种
类型。事实上我们目前编译的led.elf也是这种格式。elf主要规定了一个可执行程序文件内部的格式,如下图,这里每个格子我们称为段。其中我们经常关注的有:
.text 段 保存的是机器代码;.rodata 段 保存的是只读数据;.data 段 已初始化的全局变量段; .bss 段 未初始化全局变量段。图示中仅仅列出了各个段和作用,但是这些段每个部分究竟要放在什么地址呢?这就需要链接时决定。之前的-Ttext 0x87000000其实就是告诉编译器将 .text 段链接到地址0x87000000这个位置,接下来说明使用链接脚本灵活配置这些段所在的位置。

3.1 内存段的总结
各内存段及其存放的数据类型
|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .bss | 用于存放未初始化 或初始化为0 的数据 在运行时会被自动清零 eg: static int bss_var; // 静态变量(全局或局部) int explicit_var = 0; // 显式初始化为0的全局变量 |
| COMMON | 用于存放未初始化的非静态全局变量 链接时****才确定最终大小和位置 允许多个目标文件定义同名符号 eg: int common_var |
| .data | 用于存放已初始化的全局变量和静态变量(非零值) 需要在程序文件中保存实际的初始值 eg: int global_var = 100; // 初始化为非零值 int global_array\[\] = {1,2,3}; // 初始化为非零数组 |
| .rodata | 存放程序的只读数据 运行时受保护,不可修改 包含的数据类型: 1. 字符串常量 2. const修饰的全局变量 3. 全局只读数组 4. switch跳转表 5. 浮点数常量 eg: const int MAX_VALUE = 100; // const全局变量 char* str = "Hello World"; // 字符串常量 const int lookup\[\] = {1,2,3}; // 只读数组 |
| .text | 存放代码的二进制形式 |
3.2 链接脚本
示例链接脚本文件:
cpp
SECTIONS
{
. = 0x87800000;
.text :
{
obj/start.o
obj/main.o
obj/led.o
obj/beep.o
*(.text)
}
.rodata ALIGN(4) : { *(.rodata*)}
.data ALIGN(4) : { *(.data)}
. = ALIGN(4);
__bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON)}
__bss_end = .;
}
说明:
1、第 1 行先写了一个关键字"SECTIONS",后面跟了一个大括号,这个大括号和最后一行的大括号是一对,这是必须的。
2、对一个特殊符号**"."** 进行赋值,"." 在链接脚本里面叫做定位计数器 ,默认的定位计数器为 0。要求代码链接到以0X87800000 为起始地址的地方,因此这一行给**"."**赋值0X87800000,表示以 0X87800000 开始,后面的文件或者段都会以 0X87800000 为起始地址开始链接
**注意:**因为 start.o 里面包含着第一个要执行的指令,所以一定要链接到最开始的地方。
3、下一行的**".text"** 是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链接到**".text"** 这个段里面的所有文件,"*(.text)"中的"*"是通配符,表示所有输入文件的.text段都放到".text"中
4、与 .text 同理,下面的**.rodata .data .bss**都依次链接到了.text段的后面
5、**ALIGN(4)**的作用:
这是用来对对应段的起始地址做字节对齐的, ALIGN(4)表示 4 字节对齐。也就是说对应段的起始地址要能被 4 整除,一般常见的都是 ALIGN(4)或者 ALIGN(8),也就是 4 字节或者 8 字节对齐。
6、__bss_start = . 与**__bss_end = .**的作用:
这两个标识符记录的是 .bss 与 COMMON 这两个段的起始地址,可以在start.S中进行访问,目的是为了在启动代码中对齐清零,前面说过:.bss段存放未被初始化或者初始化为0的数据,并且运行时会被自动清零; COMMON段存放的是未初始化的非静态全局变量,由C语言的知识可知,全局变量在运行时也会清零,故这两个段要在启动代码中进行清空操作,清空的汇编代码如下:
使用 blt 而不使用 ble 的原因:

cpp
_bss_clear:
ldr r0, =__bss_start
ldr r1, =__bss_end
loop:
mov r2, #0
str r2, [r0]
add r0, r0, #4
cmp r0, r1
blt loop /这里一定是小于,不是小于等于
bx lr
3.3 完善后的启动代码--汇编
cpp
.global _start
_start:
//初始化异常向量表:存放异常跳转指令
ldr pc, =_reset_handler //0x00000000 复位异常跳转指令 --- 复位默认是SVC模式
ldr pc, =_undefined_handler //0x00000004 未定义指令异常跳转指令
ldr pc, =_svc_handler //0x00000008 复位或软中断异常跳转指令
ldr pc, =_prefetch_abort_handler //0x0000000C 指令存取错误异常跳转指令
ldr pc, =_data_abort_handler //0x00000010 数据存取错误异常跳转指令
ldr pc, =_reserved_handler //0x00000014 保留
ldr pc, =_irq_handler //0x00000018 普通中断异常跳转指令
ldr pc, =_fiq_handler //0x0000001C 快速中断异常跳转指令
_fiq_handler:
ldr pc, =_fiq_handler
_irq_handler:
ldr pc, =_irq_handler
_reserved_handler:
ldr pc, =_reserved_handler
_data_abort_handler:
ldr pc, =_data_abort_handler
_prefetch_abort_handler:
ldr pc, =_prefetch_abort_handler
_svc_handler:
ldr pc, =_svc_handler
_undefined_handler:
ldr pc, =_undefined_handler
_reset_handler:
//将工作模式切换到IRQ模式下
mrs r0, cpsr
bic r0, r0, #0x1F
orr r0, r0, #0x12
msr cpsr ,r0
//设置IRQ模式下的栈底指针,大小为32M
ldr sp, =0x86000000
//将工作模式切换到System模式下
mrs r0, cpsr
bic r0, r0, #0x1F
orr r0, r0, #0x1F
msr cpsr, r0
//设置System模式的栈底指针
ldr sp, =0x84000000
//清零.bss段与COMMON段
bl _bss_clear
//import main
b main
b finish
_bss_clear:
ldr r0, =__bss_start
ldr r1, =__bss_end
loop:
mov r2, #0
str r2, [r0]
add r0, r0, #4
cmp r0, r1
blt loop //这里一定是小于,不是小于等于
bx lr
finish:
b finish
四. BSP开发
4.1 工程管理
Board Software Pack:版级软件支持包
在前面的章节中,我们都是将所有的源码文件放到工程的根目录下,如果工程文件比较少 的话这样做无可厚非,但是如果工程源文件达到几十、甚至数百个的时候,这样全部放到根目录下就会使工程显得混乱不堪。所以我们必须对工程文件做管理,将不同功能的源码文件放到不同的目录中 。另外我们也需要将源码文件中,所有完成同一个功能的代码提取出来放到一个单独的文件中,也就是对程序分功能管理。
**做法:**在一个工程中创建多个文件夹存放不同功能的文件
imx6ull: 存放库文件
bsp: 存放和外设相关,led.c led.h
project: main.c start.S 主文件
obj: 在编译过程中,中间生成的.o文件
其他的放在最顶层即可,eg:Makefile、imx6ull.lds等

4.2 对应的Makefile与链接脚本
Makefile:
简单Makefile的编写参考本文:Makefile文件的编写
cpp
target = beep
cross_compiler = arm-linux-gnueabihf-
cc = $(cross_compiler)gcc
ld = $(cross_compiler)ld
objcopy = $(cross_compiler)objcopy
objdump = $(cross_compiler)objdump
incdirs = bsp imx6ull
srcdirs = bsp project
include = $(patsubst %, -I%, $(incdirs))
cfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.c))
sfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.S))
cfilenodir = $(notdir $(cfiles))
sfilenodir = $(notdir $(sfiles))
cobjs = $(patsubst %, obj/%, $(cfilenodir:.c=.o))
sobjs = $(patsubst %, obj/%, $(sfilenodir:.S=.o))
objs = $(cobjs) $(sobjs)
VPATH = $(srcdirs)
$(target).bin : $(objs)
$(ld) -Timx6ull.lds -o$(target).elf $^
$(objcopy) -O binary -S -g $(target).elf $@
$(objdump) -D $(target).elf > $(target).dis
$(sobjs) : obj/%.o : %.S
@mkdir -p obj
$(cc) -Wall -nostdlib -c $(include) -o $@ $<
$(cobjs) : obj/%.o : %.c
@mkdir -p obj
$(cc) -Wall -nostdlib -c $(include) -o $@ $<
.PHONY : clean
clean:
rm -rf $(objs) $(target).elf $(target).bin $(target).dis
load:
./../imxdownload $(target).bin /dev/sdb
链接脚本:
在编写链接文件时注意链接的顺序:

cpp
SECTIONS
{
. = 0x87800000;
.text :
{
obj/start.o
obj/main.o
obj/led.o
obj/beep.o
*(.text)
}
.rodata ALIGN(4) : { *(.rodata*)}
.data ALIGN(4) : { *(.data)}
. = ALIGN(4);
__bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON)}
__bss_end = .;
}