本文接上篇《ARM裸机程序之Makefile解读》,继续研究一下imx6ull裸机程序的启动程序start.S和应用主程序main.c。
启动代码
汇编代码start.S我都做了注释,应该可以看出这段代码的作用和原理。
.text
.global _start
_start:
ldr sp, =0x80200000 // 设置栈指针
bl clean_bss // 跳转bss清零函数
bl main // 跳转到应用程序主函数
halt:
b halt
clean_bss:
/* 清除bss段,即写0 */
ldr r1, =__bss_start // bss段起始地址,赋值给r1寄存器
ldr r2, =__bss_end // bss段结束地址,赋值给r3寄存器
mov r3, #0 // r3寄存器赋值为0
clean_loop: // 清零循环
str r3, [r1] // 将r3中的值(即0)写到r1中所存地址的位置
add r1, r1, #4 // 相当于r1 = r1 + 4,即地址前移
cmp r1, r2 // 判断是否到达bss段结束地址
bne clean_loop // 上面判断=0的话,继续跳到clean_loop
mov pc, lr // clean_bss函数返回
拓展阅读:
- ldr与mov的作用与区别
ARM是RISC结构,数据从内存到CPU之间的移动只能通过L/S指令来完成,也就是ldr/str指令。比如想把数据从内存中某处读取到寄存器中,只能使用ldr,比如:
ldr r0, 0x12345678
就是把0x12345678这个地址中的值存放到r0中。而mov不能干这个活,mov只能在寄存器之间移动数据,或者把立即数移动到寄存器中,这个和x86这种CISC架构的芯片区别最大的地方。x86中没有ldr这种指令,因为x86的mov指令可以将数据从内存中移动到寄存器中。另外还有一个就是ldr伪指令,虽然ldr伪指令和ARM的ldr指令很像,但是作用不太一样。ldr伪指令可以在立即数前加上=,以表示把一个地址写到某寄存器中,比如:
ldr r0, =0x12345678
这样,就把0x12345678这个地址写到r0中了。所以,ldr伪指令和mov是比较相似的,只不过mov指令限制了立即数的长度为8位,也就是不能超过255,而ldr伪指令没有这个限制。如果使用ldr伪指令时,后面跟的立即数没有超过8位,那么在实际汇编的时候该ldr伪指令是被转换为mov指令的。
- 需要清零BSS段的原因
我们知道BSS段保存未初始化的全局变量和静态局部变量,而为什么会有BSS段呢?其实也是为了节省空间,要知道,一般对于初始化过的全局/静态变量,除了要给变量分配空间,还要给变量的值分配空间。但对于没有初始化的全局变量,如果也这样分配,由于没有初值,岂不是浪费存储空间。所以就把所有没有初值的全局/静态变量单独放到一片区域,即BSS段。
对于BSS段的变量,为了防止直接使用而引发未定义的问题,一般编译器约定俗成的会在启动代码中将BSS段置为0,所以,BSS段中的变量初值也就变成0了。
主程序
主程序分两部分,功能很简单,就是循环亮、灭一个led灯。
#include "led.h"
static volatile unsigned int *CCM_CCGR1 ;
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR ;
static volatile unsigned int *GPIO5_DR ;
/**********************************************************************
* 函数名称: led_init
* 功能描述: 初始化LED引脚,就是把它设置为输出引脚
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 无
***********************************************************************/
void led_init(void)
{
unsigned int val;
CCM_CCGR1 = (volatile unsigned int *)(0x20C406C);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = (volatile unsigned int *)(0x2290014);
GPIO5_GDIR = (volatile unsigned int *)(0x020AC000 + 0x4);
GPIO5_DR = (volatile unsigned int *)(0x020AC000);
/* GPIO5_IO03 */
/* a. 使能GPIO5
* set CCM to enable GPIO5
* CCM_CCGR1[CG15] 0x20C406C
* bit[31:30] = 0b11
*/
*CCM_CCGR1 |= (3<<30);
/* b. 设置GPIO5_IO03用于GPIO
* set IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3
* to configure GPIO5_IO03 as GPIO
* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 0x2290014
* bit[3:0] = 0b0101 alt5
*/
val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
val &= ~(0xf);
val |= (5);
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;
/* c. 设置GPIO5_IO03作为output引脚
* set GPIO5_GDIR to configure GPIO5_IO03 as output
* GPIO5_GDIR 0x020AC000 + 0x4
* bit[3] = 0b1
*/
*GPIO5_GDIR |= (1<<3);
}
/**********************************************************************
* 函数名称: led_ctl
* 功能描述: 设置LED状态
* 输入参数:
* on : 1-LED点亮, 0-LED熄灭
* 输出参数: 无
* 返 回 值: 无
***********************************************************************/
void led_ctl(int on)
{
if (on) /* on: output 0*/
{
/* d. 设置GPIO5_DR输出低电平
* set GPIO5_DR to configure GPIO5_IO03 output 0
* GPIO5_DR 0x020AC000 + 0
* bit[3] = 0b0
*/
*GPIO5_DR &= ~(1<<3);
}
else /* off: output 1*/
{
/* e. 设置GPIO5_IO3输出高电平
* set GPIO5_DR to configure GPIO5_IO03 output 1
* GPIO5_DR 0x020AC000 + 0
* bit[3] = 0b1
*/
*GPIO5_DR |= (1<<3);
}
}
#include "led.h"
void delay(volatile unsigned int d)
{
while(d--);
}
int main()
{
led_init();
while(1)
{
led_ctl(1); // 灯亮
delay(1000000); // 延时
led_ctl(0); // 灯灭
delay(1000000); // 延时
}
return 0;
}
代码比较简单,按照注释基本也能明白什么意思。过程主要是配置led所连接的GPIO管脚为输出状态,控制该GPIO的输出值即可控制led灯的亮灭。方法主要是通过配置GPIO控制器的寄存器来实现,具体参照数据手册就行,按照相应的数据位进行赋值。
需要注意的是,裸机程序包括单片机程序跟Linux驱动程序是不一样的,因为没有虚拟地址管理,裸机程序要操作寄存器必须直接读写寄存器的物理地址,而Linux驱动程序都是先将物理地址映射到虚拟地址,再进行读写的。更多的区别,后续讨论Linux驱动的时候再说。
网友评论