3.1概述
所谓裸机程序就是指嵌入式系统的软件在没有操作系统的支持下运行,有时也称谓“裸跑”。一般来说裸机程序涉及到汇编语言程序的编写,以及汇编语言与C语言的混合编程等。但是一般来说,汇编语言部分的内容较少,主要是编写系统启动代码,为后面的C语言程序设置运行环境等(主要是设置堆栈),有51系列微处理器开发经验者对这点是应该有印象的。一般51系列处理器主要采用keil开发工具,其本身提供了汇编语言的启动代码。目前对ARM系列处理器来说,keil也进行了支持。但是实际上我们完全可以用gcc进行开发。
本部分主要涉及两部分内容:1)纯汇编语言程序的编写;2)汇编语言与C语言混合编写。因为汇编语言难学,可移植性差,真正的产品中是很少采用这种方式的,一般裸机程序都采用第2种方式,并且汇编语言编写的主要是启动代码,这部分内容完全可以作为模板,一次编写以后不再编写,将开发者的主要精力集中到系统功能的实现上来。所以对ARM处理器的汇编指令我们不做讲解,对汇编程序的编写规则也不做介绍。
本章我们将采用这两种不同的方式实现通过GPIO端口控制LED,实现LED的闪烁。另外至于芯片的具体硬件功能的详细分析,我们将在后续章节关于外设部分介绍。
3.2硬件原理
本教程采用的开发板是Tiny6410,也摘录了其教程部分内容,其他的开发板也是类似的。
在原理图中搜索引脚“nLED_1”,可得:
可见,LED1,2,3,4 分别使用的 CPU 端口资源为 GPK_4,5,6,7。
3.3汇编语言实现
.global _start
_start:
// 把外设的基地址告诉CPU
//对于6410来说,内存(0x00000000~0x60000000)
//外设(0x70000000-0x7fffffff)
ldr r0, =0x70000000
//外设大小:256M
orr r0, r0, #0x13
//把r0的值(包括了外设基地址+外设大小)告诉cpu
mcr p15,0,r0,c15,c2,4
// 关看门狗
ldr r0, =0x7E004000
mov r1, #0
str r1, [r0]
// 设置GPKCON0
ldr r1, =0x7F008800
ldr r0, =0x11110000
str r0, [r1]
mov r2, #0x1000
led_blink:
// 设置GPKDAT,使GPK_4/5/6/7引脚输出低电平,LED亮
ldr r1, =0x7F008808
mov r0, #0
str r0, [r1]
// 延时
bl delay
// 设置GPKDAT,使GPK_4/5/6/7引脚输出高电平,LED灭
ldr r1, =0x7F008808
mov r0, #0xf0
str r0, [r1]
// 延时
bl delay
sub r2, r2, #1
cmp r2,#0
bne led_blink
halt:
b halt
delay:
mov r0, #0x1000000
delay_loop:
cmp r0, #0
sub r0, r0, #1
bne delay_loop
mov pc, lr
led.bin: start.o
arm-linux-ld -Ttext 0x50000000 -o led.elf $^
arm-linux-objcopy -O binary led.elf led.bin
arm-linux-objdump -D led.elf > led_elf.dis
%.o : %.S
arm-linux-gcc -o $@ $< -c
%.o : %.c
arm-linux-gcc -o $@ $< -c
clean:
rm *.o *.elf *.bin *.dis -rf
3.3汇编语言与C语言混合编程
1.start.S
// 启动代码
.global _start
_start:
// 把外设的基地址告诉CPU
ldr r0, =0x70000000
orr r0, r0, #0x13
mcr p15,0,r0,c15,c2,4 @ 256M(0x70000000-0x7fffffff)
// 关看门狗
ldr r0, =0x7E004000
mov r1, #0
str r1, [r0]
// 设置栈
ldr sp, =8*1024
// 跳转
ldr pc, =main
halt:
b halt
此处的功能和汇编程序基本相同,在关闭了看门狗,设置了栈之后,然后跳转到main函数,main函数也就是我们编写的应用程序。从这里我们实际上也可以看出C语言程序实际上并不一定非要从main函数开始执行的。
2.main.c
#include"common.h"
void delay(){
volatile int i = 0x10000;
while (i--);
}
int main(){
int i = 0;
clock_init();// 初始化时钟
sdram_init();// 初始化内存
copy2ddr();// 重定位
mmu_init();// 初始化mmu
// 虚拟地址
volatile unsigned long *gpkcon0 = (volatile unsigned long *)0x10008800;
volatile unsigned long *gpkdat = (volatile unsigned long *)0x10008808;
*gpkcon0 = (*gpkcon0 & ~(0xffff<<16)) | (0x1111<<16);
while (1){
*gpkdat = (*gpkdat & ~(0xf<<4)) | (i<<4);
i++;
if (i == 16)
i = 0;
delay(0x10000);
}
return 0;
}
从代码中我们可以看出系统首先进行了初始化,然后设置了MMU(这一步也并不是必须的),接着进入一个死循环,不停地改变端口的值。这里详细的初始化代码不再列出,具体可查看如下https://pan.baidu.com/s/1CSPa_I-obpSylrHIeKMUNg 提取码:pf99
3.makefile
led.bin: start.o clock.o sdram.o mmu.o main.o
arm-linux-ld -T led.lds -o led.elf $^
arm-linux-objcopy -O binary led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
%.o : %.S
arm-linux-gcc -o $@ $< -c
%.o : %.c
arm-linux-gcc -o $@ $< -c
clean:
rm *.o *.elf *.bin *.dis -f
我们可以看到本节的makefile和上一节的几乎完全相同。但是因为涉及到多段代码,所以还必须要有连接文件,不能使用缺省连接文件。
4.led.lds
SECTIONS {
. = 0x50000000;
.text : {
start.o
sdram.o
mmu.o
clock.o
* (.text)
}
.rodata : {
* (.rodata)
}
.data : {
* (.data)
}
bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON) }
bss_end = .;
}
3.4编译代码和烧写运行
cd ~/led
make
loady 0x50000000 led.bin
go 0x50000000
我们采用Ymodem协议,将主机编译好的二进制文件下载到0x5000000位置(在makefile和lds文件中已经进行了设定)然后运行。