一、引言
考虑到工作效率,嵌入式驱动开发很少用汇编,大部分是用C语言进行开发。
嵌入式驱动开发开始部分就可以用C语言吗?
当然不是!在开始部分用汇编来初始化一下 C 语言环境,比如初始化 DDR、设置堆栈指针 SP 等等,当这些工作都做完以后就可以进入 C 语言环境,也就是运行 C 语言代码,一般都是进入 main 函数。有两部分文件需要完成:
1、汇编文件
汇编文件用来完成C语言环境搭建。
2、C语言文件
C语言用来完成业务代码。
二、原理概述
1、程序状态寄存器
所有的处理器模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问。CPSR 是当前程序状态寄存器,该寄存器包含了条件标志位、中断禁止位、当前处理器模式标志等一些状态位以及一些控制位。
M[4:0]:处理器模式控制位,含义如下表所示:
2、常用汇编指令
(1)MRS指令
MRS 指令用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器,要读取特殊寄存器的数据只能使用 MRS 指令。
MRS R0, CPSR @将特殊寄存器 CPSR 里面的数据传递给 R0,即 R0=CPSR
(2)MSR指令
MSR 指令和 MRS 刚好相反,MSR 指令用来将普通寄存器的数据传递给特殊寄存器,也就是写特殊寄存器,写特殊寄存器只能使用 MSR。
MSR CPSR, R0 @将 R0 中的数据复制到 CPSR 中,即 CPSR=R0
(3)LDR指令
LDR 主要用于从存储加载数据到寄存器 Rx 中,LDR 也可以将一个立即数加载到寄存器 Rx中,LDR 加载立即数的时候要使用“=”,而不是“#”。
LDR R0, =0x0209C004 @将寄存器地址 0x0209C004 加载到 R0 中,即 R0=0x0209C004 LDR R1, [R0] @读取地址 0x0209C004 中的数据到 R1 寄存器中
(4)STR指令
LDR 是从存储器读取数据,STR 就是将数据写入到存储器中。
LDR R0, =0x0209C004 @将寄存器地址 0x0209C004 加载到 R0 中,即 R0=0x0209C004 LDR R1,=0x20000002 @R1 保存要写入到寄存器的值,即 R1=0x20000002 STR R1, [R0] @将 R1 中的值写入到 R0 中所保存的地址中
3、栈初始化
栈初始化是C语言环境初始必不可少的部分。ARM采用降栈的方式,SP为堆栈指针。
通过 ldr 指令设置 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,2MB 的栈空间已经很大了,如果做裸机开发的话绰绰有余。
三、代码编写
1、汇编文件(start.s)
.global _start _start: mrs r0,cpsr bic r0,#0x1f orr r0,#0x13 msr cpsr,r0 ldr sp,=0x80200000 b main
通过b main命令跳转到 main 函数,main 函数就是 C 语言代码了。
2、C语言文件
(1)main.h
#ifndef __MAIN_H #define __MAIN_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_DR *((volatile unsigned int *)0x0209C000) #define GPIO1_GDIR *((volatile unsigned int *)0x0209C004) #define GPIO1_PSR *((volatile unsigned int *)0x0209C008) #define GPIO1_ICR1 *((volatile unsigned int *)0x0209C00C) #define GPIO1_ICR2 *((volatile unsigned int *)0x0209C010) #define GPIO1_IMR *((volatile unsigned int *)0x0209C014) #define GPIO1_ISR *((volatile unsigned int *)0x0209C018) #define GPIO1_EDGE_SEL *((volatile unsigned int *)0x0209C01C) #endif
(2)main.c
#include "main.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() { SW_MUX_GPIO1_IO03=0x05; SW_PAD_GPIO1_IO03=0x10B0; GPIO1_GDIR=0x01<<3; GPIO1_DR=0x00; } void led_on() { //将 GPIO1_DR 的 bit3 清零 GPIO1_DR&=~(0x01<<3); } void led_off() { //将 GPIO1_DR 的 bit3 置 1 GPIO1_DR|=0x01<<3; } void delay_short(volatile unsigned int n) { while(n--); } void delay(volatile unsigned int n) { while(n--) { delay_short(0x07ff); } } int main() { clk_enable(); led_init(); while(1) { led_off(); delay(500); led_on(); delay(500); } return 0; }
我们需要将start.o文件设置链接到开始位置,因为 start.o 里面包含着第一个要执行的指令,所以一定要链接到最开始的地方。main 函数就是我们的主函数了,在 main 函数中先调用函数 clk_enable()和 led_init()来完成时钟使能和 LED 初始化,最终在 while(1)循环中实现 LED 循环亮灭,亮灭时间大约是 500ms。
四、编译代码
1、链接脚本
arm-linux-gnueabihf-ld -Ttext 0x87800000 led.o -o led.elf
上面语句中我们是通过“-Ttext”来指定链接地址是 0x87800000 的,这样的话所有的文件都会链接到以 0x87800000 为起始地址的区域。但是有时候我们很多文件需要链接到指定的区域,或者叫做段里面,比如在 Linux 里面初始化函数就会放到 init 段里面。因此我们需要能够自定义一些段,这些段的起始地址我们可以自由指定,同样的我们也可以指定一个文件或者函数应该存放到哪个段里面去。要完成这个功能我们就需要使用到链接脚本,看名字就知道链接脚本主要用于链接的,用于描述文件应该如何被链接在一起形成最终的可执行文件。其主要目的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件中的内存排布。比如我们编译生成的文件一般都包含 text 段、data 段等等。
SECTIONS { . = 0x87800000; .text : { start.o main.o *(.text) } .rodata ALIGN(4) : {*(.rodata*)} .data ALIGN(4) : { *(.data) } __bss_start = .; .bss ALIGN(4) : { *(.bss) *(COMMON) } __bss_end = .; }
.bss 段的起始地址和结束地址就保存在了“__bss_start”和“__bss_end”中,我们就可以直接在汇编或者 C 文件里面使用这两个符号。
2、Makefile
objs := start.o main.o .PHONY: clean 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 -o $@ $< %.o:%.S arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $< %.o:%.c arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $< clean: rm -rf *.o ledc.bin ledc.elf ledc.dis
$^:所有依赖文件的集合
$@:所有目标文件的集合
$<:第一个依赖文件
五、下载验证
使用 imxdownload 将编译出来的 ledc.bin 烧写到 SD 卡中,命令如下:
chmod +x imxdownload //给予 imxdownload 可执行权限,一次即可 ./imxdownload ledc.bin /dev/sdc //烧写到 SD 卡中,不能烧写到/dev/sda 或 sda1 设备里面
烧写成功以后将 SD 卡插到开发板的 SD 卡槽中,然后复位开发板,如果代码运行正常的话 LED0 就会以 500ms 的时间间隔亮灭。
LED闪烁视频