上一篇使用汇编语言编写LED等驱动实验,在实际工作中较少使用汇编编写嵌入式驱动,毕竟汇编过于底层,难度较大。绝大部分情况下都是使用C语言编写主体程序,只是开始部分使用汇编来初始化C语言环境,比如初始化DDR、设置堆栈指针SP等。因此可理解为下面三部分:
1)设置处理器模式
设置6ULL的处于SVC模式下,设置CPSR寄存器的bit4-0,也就是M[4:0]为10011=0X13。读写状态寄存器需要用到MRS和MSR指令。MRS将CPSR寄存器数据读出到通用寄存器里,MSR指令将通用寄存器的值写入到CPSR寄存器里面。
2)设置sp指针(C语言需要出栈和入栈)
Sp可以指向内部RAM,可以指向DDR,目前指向DDR,Sp设置到哪里?512MB的范围是0X80000000-0X9FFFFFFF。栈大小,0x200000=2MB。处理器栈增长方式,对于A7而言向下增长,设置sp指向0x80200000。
3)C语言文件编写
C语言文件是我们要完成的业务层代码,即实际例程要完成的功能。
1. 实验程序编写
新建 VScode 工程,工程名字为“ledc”,新建三个文件:start.S、main.c 和 main.h。其中 start.S 是汇编文件,main.c 和 main.h 是 C 语言相关文件。
1.1 start.S 是汇编文件编写:
.global _start
_start:
/*设置 处理器进入SVC模式*/
mrs r0, cpsr /*读取cpsr到r0 */
bic r0, r0, #0x1f /* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
orr r0, r0, #0x13 /* r0 或上 0x13,表示使用 SVC 模式 */
msr cpsr, r0 /*将r0写入到cpsr */
/*设置SP指针 */
ldr sp, 0x80200000 /*已经sp设置到ddr里面 */
b main /*跳转到C语言main函数 */
/*第1行是定义全局标号_start;
第3行是标号_start开始的地方,相当于_start函数;
第5-8行是设置处理器进入SVC模式;
第11行是通过ldr指令设置SVC模式下SP指针=0X80200000;
第12行是通过b指令,跳转到C语言函数,比如main函数;*/
1.2 main.h 文件编写
以宏定义的形式定义了要使用到的所有寄存器,后面的数字代表其地,比如 CCM_CCGR0 寄存器的地址就是 0X020C4068
#ifndef __MAIN_H
#define __MAIN_H
/* CCM 相关寄存器地址*/
#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)
/*IOMUX 相关寄存器地址*/
#define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0X020E02F4)
/*GPIO1 相关寄存器地址*/
#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
1.3 main.c文件编写
main.c文件共6个函数:
1.clk_enable 函数是使能 CCGR0~CCGR6 所控制的所有外设时钟;
2. led_init 函数是初始化 LED 灯所使用的 IO,包括设置 IO 的复用功能、IO 的属性配置和 GPIO 功能,最终控制 GPIO 输出低电平来打开 LED 灯;
3. led_on 和 led_off ,用来控制 LED 灯的亮灭的;
4.delay_short()和 delay() 这两个函数是延时函数,delay_short()函数是靠空循环来实现延时的,在396Mhz工作主频下实现约1ms的延时.
#include "main.h"
/* 使能外设时钟函数*/
void clk_enable(void)
{
CCM_CCGR1 = 0xFFFFFFFF;
CCM_CCGR2 = 0xFFFFFFFF;
CCM_CCGR3 = 0xFFFFFFFF;
CCM_CCGR4 = 0xFFFFFFFF;
CCM_CCGR5 = 0xFFFFFFFF;
CCM_CCGR6 = 0xFFFFFFFF;
}
/*初始化led函数
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 低转换率*/
void led_init(void)
{
SW_MUX_GPIO1_IO03 = 0x5; /*复用为GPIO1-IO03*/
SW_PAD_GPIO1_IO03 = 0X10B0; /*设置GPIO1-IO03电气属性*/
/*GPIO初始化*/
GPIO1_GDIR = 0X8; /*设置为输出*/
GPIO1_DR = 0X0; /*打开led灯*/
}
/*短延时*/
void delay_short(volatile unsigned int n)
{
while(n--){}
}
/*延迟,一次循环大概是1ms,n:延时ms数,在主频396MHz*/
void delay(volatile unsigned int n)
{
while(n--){
delay_short(0x7ff);
}
}
/*打开LED灯*/
void led_on(void)
{
GPIO1_DR &=~(1<<3); /*bit3清零*/
}
/*关闭LED灯*/
void led_off(void)
{
GPIO1_DR |=(1<<3); /*bit3置1*/
}
int main(void)
{
clk_enable();
led_init();
/*初始化LED*/
/*设置LED*/
while(1){
led_on();
delay(500);
led_off();
delay(500);
}
return 0;
}
2. 编译下载
2.1 编写Makefile
objs = start.o main.o
ledc.bin: $(objs)
arm-linux-gnueabihf-ld -Timx6u.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:%.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis
/*
第 1 行: 定义了一个变量 objs,objs 包含着要生成 ledc.bin 所需的材料:start.o 和 main.o, start.o 一定要放到 最前面!因为在链接的时 start.o 要在最前面,start.o 是最先要执行的文件!
第 3 行: 默认目标,目的是生成最终的可执行文件ledc.bin,ledc.bin 依赖 start.o 和 main.o 。
第 4 行: 是使用 arm-linux-gnueabihf-ld 进行链接,链接起始地址是 0X87800000,但是这一行 用到了自动变量“$^”,“$^”的意思是所有依赖文件的集合,在这里就是 objs 这个变量的值: arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^表示使用 imx6ul.lds 这个链接脚本文件
第 5 行: 使用 arm-linux-gnueabihf-objcopy 来将 ledc.elf 文件转为 ledc.bin,本行也用到了自动变量 “$@”,“$@”的意思是目标集合,在这里就是“ledc.bin”,那么本行就相当于:
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf ledc.bin
第 6 行: 使用 arm-linux-gnueabihf-objdump 来反汇编,生成 ledc.dis 文件。
第 8~13行: 针对不同的文件类型将其编译成对应的.o 文件,其实就是汇编.s(.S)和.c 文件。
第 14行: 工程清理规则,通过命令“make clean”就可以清理工程
*/
2.2 链接脚本文件
SECTIONS{
. = 0x87800000 //链接到0x87800000,从这里开始
.text :
{
start.o //第一个代码部分
*(.text)
}
.rodata ALIGN(4) : {*(.rodata*)} //指定只读数据段 4字节对齐
.data ALIGN(4) : {*(.data)} //指定读/写数据段 4字节对齐
__bss_start=. //把__bss_start定义为当前位置(起始)
.bss ALIGN(4) : {*(.bss)*(COMMON)}
__bss_end=.; //把__bss_end定义为当前位置(终地址)
}
/*. 代表当前
.text 代表代码段
.rodata 只读数据段
.data读/写数据段
ALIGN(4) 四字节对齐
.bss 未初始化的数据段
第 1 行:关键字“SECTIONS”;
第 2 行:对一个特殊符号“.”进行赋值,“.”在链接脚本里面叫做定位计数器,默认的定位计数器为0。赋值代表代码链接到以 0X10000000 为起始地址的地方开始;
第 3 行的:“.text”是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链 接到“.text”这个段里面的所有文件,“*(.text)”中的“*”是通配符,表示所有输入文件的.text 段都放到“.text”中;
第 4 行:重新调整定位计数器为 0X30000000;
第 5 行:与第3行相同,ALIGN(4)表示 4 字节对齐。也就是说段“.data”的起始地址要能被 4 整除,一般常见的都是 ALIGN(4)或者 ALIGN(8),也就是 4 字节或者 8 字节对齐;
第 6 行:定义了一个“.bss”段,所有文件中的“.bss”数据都会被放到这个里面,“.bss”数据就是那些定义了但是没有被初始化的变量;
第11-12行:__bss_start” 和“__bss_end”是符号,第 11、13 这两行其实就是对这两个符号进行赋值,其值为定位符“.”, 这两个符号用来保存.bss 段的起始地址和结束地址
2.3 make和烧写SD卡
烧写到SD卡中:
给予imxdownload可执行权限:chmod 777 imxdownload
烧写命令:./imxdownload led.bin /dev/sdb
Imxdownload会向led.bin添加一个头部,生成新的load.imx文件,这个load.imx文件就是最终烧写到SD卡里面。