正点原子I.MX6U阿尔法开发板
使用C语言进行软件开发,首先需要使用汇编搭建C语言运行环境,用汇编来初始化一下 C 语言环境,比如初始化 DDR、 设置堆栈指针 SP 等等,当这些工作都做完以后就可以进入 C 语言环境,也就是运行 C 语言代 码,一般都是进入 main 函数。所以我们有两部分文件要做:
汇编文件:汇编文件只是用来完成 C 语言环境搭建
C 语言文件:C 语言文件就是完成我们的业务层代码的,其实就是我们实际例程要完成的功能
下面我们分析汇编文件的编写:
了解 I.MUX 汇编启动代码前,我们先看一下 STM32 的启动代码是如何编写的;在 STM32 中,启动文件 startup_stm32f10x_hd.s 就是完成 C 语言环境搭建的,代码主要分为三个部分:
首先设置堆和栈的大小
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; Heap Configuration
; Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
然后初始化中断向量表
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler
;...省略
中断向量表初始化完成后调用复位中断,先声明然后调用时钟初始化,
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
;寄存器版本代码,因为没有用到SystemInit函数,所以注释掉以下代码为防止报错!
;库函数版本代码,建议加上这里(外部必须实现SystemInit函数),以初始化stm32时钟等。
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
时钟初始化函数使用C语言进行编写,配置STM外设时钟
时钟初始化完成后,调用 编译器生成的 __main
函数进行运行C语言 main 函数前的一些初始化,__main()
中可以看出有两个大的函数:
__scatterload():负责把RW/RO输出段从装载域地址复制到运行域地址,并完成了ZI运行域的初始化工作。
__rt_entry(): 负责初始化堆栈,完成库函数的初始化,最后自动跳转向main()函数。
其主要功能为:
以及编写一些异常处理中断函数
; Dummy Exception Handlers (infinite loops which can be modified)
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
MemManage_Handler\
PROC
EXPORT MemManage_Handler [WEAK]
B .
ENDP
BusFault_Handler\
PROC
EXPORT BusFault_Handler [WEAK]
B .
ENDP
;...省略
以上基本就是 STM32 启动代码的执行流程
I.MUX 启动代码与 STM32 相似,我们需要初始化堆栈准备C语言运行环境,本节暂时不需要初始化堆栈向量表,编写启动汇编代码如下:
ARM 汇编指令参考这篇文章:ARM 汇编基础
@编写全局标号
.global _start
_start:
@设置CPSR寄存器使CPU进入SVC模式
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0x13
msr cpsr, r0
@设置堆栈指针
ldr sp, =0x80200000
@跳转到main函数
b main @
注意:设置 SVC 模式下的 SP 指针=0X80200000,因为 I.MX6U-ALPHA 开发板上的 DDR3 地址范围是0X80000000~0XA0000000(512MB) 或 者 0X80000000~0X90000000(256MB),不管是 512MB 版本还是 256MB 版本的,其 DDR3 起始地址都是 0X80000000。由于 Cortex-A7 的堆栈是向下增长的,所以将 SP 指针设置为 0X80200000,因此 SVC 模式的栈大小 0X80200000-0X80000000=0X200000=2MB
一般在堆栈 SP 指针直接指向 DDR 的地址之前是需要初始化 DDR 的,但是 IMUX Boot Rom 在一开始就已经读取 DCD 的配置参数进行初始化了,所以这里我们不用再初始化
以上就是汇编的初始化代码了,在汇编初始化完成后,下一步就是跳转到 main 函数运行 c语言的代码
C语言驱动程序就是直接使用C语言来对 IMUX 的底层寄存器进行操作,关于使用的寄存器地址,可以参考我之前的文章整理:汇编驱动LED实验,我们将用到的寄存器进行封装,用宏定义替换,此处我直接使用正点原子的宏定义头文件:
先看一下开发板上LED的接口,GPIO1的3脚
然后我们编写 main.c 文件代码
先编写时钟使能代码
void CLK_ENA()
{
CCM_CCGR0 = 0xffffffff;
CCM_CCGR1 = 0xffffffff;
CCM_CCGR2 = 0xffffffff;
CCM_CCGR3 = 0xffffffff;
CCM_CCGR4 = 0xffffffff;
CCM_CCGR5 = 0xffffffff;
CCM_CCGR6 = 0xffffffff;
}
再编写 LED IO 口初始化代码
void led_init()
{
//设置寄存器 SW_MUX_GPIO1_IO03_BASE 的 MODE 为5
SW_MUX_GPIO1_IO03 = 0x5;
//模式配置
//bit 16:0 HYS关闭
//bit [15:14]: 00 默认下拉
//bit [13]: 0 kepper功能
//bit [12]: 1 pull/keeper使能
//bit [11]: 0 关闭开路输出
//bit [7:6]: 10 速度100Mhz
//bit [5:3]: 110 R0/6驱动能力
//bit [0]: 0 低转换率
SW_PAD_GPIO1_IO03 = 0x10b0;
//设置GPIO为输出
GPIO1_GDIR = 0X0000008;
//初始化输出为0
GPIO1_DR = 0x0;
}
在开头添加一个宏定义用于控制 GPIO1的3脚电平,设置电平使用: 或置位,与清零
#define LED_ON() (GPIO1_DR &= ~(1<<3))
#define LED_OFF() (GPIO1_DR |= (1<<3))
编写延时函数
void delay(volatile unsigned int n)
{
while(n--)
{
volatile unsigned int i = 0x7ff;
while(i--);
}
}
编写主函数,初始化外设后,延时点亮LED灯
int main(void)
{
CLK_ENA();
led_init();
while(1)
{
LED_OFF();
delay(1000);
LED_ON();
delay(1000);
}
return 0;
}
代码编写完成,需要编写编译链接 Makefile 脚本
# 定义目标变量
objs := start.o main.o
# 生成bin文件
ledc.bin: $(objs)
# 依次读取第一个依赖文件进行链接
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^
# 链接文件转二进制
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
# 链接文件生成反汇编文件
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
#生成编译文件
%.o:%.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o:%.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o:%.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<、
#清除编译文件
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis
$ 表示执行一个 Makefile 函数, $@ 依次取出目标文件用于执行,$< 依次取出依赖文件用于执行
% 表示变量成员通配符
在上面代码进行链接的时候,使用到了imux6ul.lds 链接文件,使链接器按照其规则进行链接,我们一般编译出来的代码 都包含在 text、data、bss 和 rodata 这四个段内,链接规则就是定义如何链接代码具体的位置
链接规则如下
# 关键字
SECTIONS{
# “.”在链接脚本里面叫做定位计数器,默认的定位计数器为 0,此处我们定义起始地址为 0X87800000
. = 0X87800000;
# “.text”是代码段名,后面的冒号是语法要求
.text :
{
start.o
main.o
*(.text)
# “*(.text)”中的“*”是通配符,表示所有输入文件的.text段都放到“.text”
}
# 只读数据段 (4字节对齐)
.rodata ALIGN(4) : {*(.rodata*)}
# 数据段 (4字节对齐)
.data ALIGN(4) : { *(.data) }
# .bss 段是定义了但是没有被初始化的变量,我们需要手动
# 对.bss 段的变量清零的,因此我们需要知道 .bss 段的
# 起始和结束地址
__bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON) }
__bss_end = .;
}
关于各段的区别,可以参考我之前的文章 C语言:内存四区
到这代码基本编写完成了,下面我们进行编译
编译成功后,下载到SD卡上
详细下载细节可参考我之前的文章:ARM裸机开发:I.MX6UL 程序编译下载(SD卡)
将SD卡插到开发板上启动,可以看到 LED 在周期性闪烁,这里就不插图了