本次实验基于Tiny4412开发板,开发板上有四个可编程控制的LED、四个按键。来实现这样一种场景:每个按键控制一盏LED,每按一次则对应的LED状态发生改变,按一次开灯,再按一次关灯。核心控制为三星的Exynos4412芯片。
一、开发板介绍
1、开发板资源介绍
Tiny4412开发板开发板分为核心板和底板,两者需要组合起来使用,核心板可以搭配标准版底板和增强版底板。本次实验中使用到的四颗LED位于核心板上,四个按键位于底板上。另外Tiny4412开发板还有一块LCD显示屏,裸机试验中暂时不会使用到LCD屏幕,可以先拆下来,方便观察现象。底板下面有螺丝,拧下来就可以把LCD拆掉。


2、芯片启动流程
在前几个GPIO实验中并没有去关注芯片的启动过程,直接就是C语言编写代码了,可是C语言编写的代码并不是芯片上电后启动的第一行代码。在使用Keil5开发AT89C51、STM32F401VE、LPC2138时,Keil5是集成开发环境,会自动将启动代码Startup.s和C语言代码进行连接,最后生成的hex可执行文件其实开始一段是汇编启动代码链接过来的,主要是进行设置C语言运行环境(初始化中断、设置栈等)、关闭看门狗(不关的话芯片会反复重启)、跳转main函数等操作。可是Keil5目前最多支持到ARM9系列芯片的开发,更高等级的芯片需要在Linux环境下,一步步从头开发。不过高等级的芯片大多不会直接使用裸机,而是会运行操作系统,如Linux、Android、QNX等,运行操作系统后会进入另一种领域——嵌入式开发。这里我们先讨论不上操作系统的裸机开发。
像Exynos4412这种高级货是有很多启动方式的,包括SD卡/EMMC/NAND启动等。以前有不少基于S3C2440的经典开发板,S3C2440是ARM9内核的,还不支持SD卡启动,需要用NOR FLash启动,一旦NOR Flash中的代码被破坏就需要使用JTAG/JLink等进行烧写。上图是SD卡启动时,SD卡中的可执行程序文件存储结构。
①、是谁把这些指令从 SD 卡读出来执行?
是固化在芯片内部ROM上的代码iROM ,iROM是三星公司事先烧写在芯片上的,无源码。iROM中的代码是4412芯片上电后首先执行的一段代码。iROM把启动设备上特定位置处的程序读入片内内存(iRAM)并执行它,这个程序被称为 BL1(Bootloader 1),BL1是三星公司提供的,无源码。接下来BL1又把启动设备上另一个特定位置处的程序读入片内内存,并执行它。这个被称为BL2(Bootloader2)。本场景中我们编写的源码编译后生成的可执行文件就放在这个位置。
②、在启动设备上哪个位置存放 BL1、BL2?把BL1、BL2读到iRAM哪个位置?
BL1位于SD卡偏移地址512字节处(即从第一个扇区(Block1)开始,前面的第0个扇区(Block0)保留,每个扇区512字节。为什么保留第0个扇区?像DOS分区表一样,第0个扇区是分区表的配置区。iROM从这个位置读入8K字节的数据,存在iRAM地址0x0202_1400位置处,所以BL1不能大于8K。BL2位于SD卡偏移地址(512+8K)字节处,BL1从这个位置读入16K字节的数据,存在iRAM地址0x0202_3400处。
③、iROM地址如何划分?BL1、BL2有效大小是多少?
安全BL1的大小为8192B。为了正确地执行iROM,应该在内部存储器的开始处保留5KB。安全BL1代码的上下文应该位于内存的0x0202_3000。BL2码的大小可由用户指定定义并依赖于BL1代码。然而,在S.LSI的参考代码BL1中,BL2代码的有效大小为小于14332B(14KB-4B),4B为校验和),如果BL2码的大小小于14332B,则为剩余区域直到14332B都应该用零填充。BL2的签名应该在内部的0x0202_6C00内存和BL2的校验和应该在S.LSI的参考代码中为0x0202_6BFC。
④、iROM执行流程图
上图是iROM的执行流程,可以看到做了关闭看门狗、关闭中断及MMU、关闭数据缓存、打开指令缓存、清除TLB等等操作,最后跳转到BL1。BL1再经过以下流程,转到执行BL2。
综上概括下:就是iROM中的程序做一些初始化操作,然后把BL1段程序从SD卡中拷贝到0x0202_1400地址处并跳转到该地址处执行,接着BL1段程序再做一些初始化操作,然后把BL2段程序从SD卡中拷贝到0x0202_3400处,并跳转到此处执行。在本实验场景中我们自己编写的程序就放在SD卡的BL2处(相当于BL2程序),BL2程序被拷贝到0x0202_3400地址处(记住这个地址,后边编译程序的时候会用到)。
二、电路图
芯片连接LED的引脚。GPM4_0 ~ GPM4_3四个引脚控制LED1 ~ LED4四个LED。
可以看到LED仍是共阳极接法,即引脚输出低电平时LED亮,输出高电平时LED熄灭。
KEY1 ~ KEY4分别连接GPX3_2 ~ GPX3_5(XEINT26 ~ XEINT)。
KEY引脚接了上拉电阻,就是默认高电平,按下的时候引脚接地,变为低电平,抬起后又恢复高电平。
三、地址映射
①、端口组GPM4配置寄存器
基地址: 0x1100_0000
GPM4CON寄存器地址:Base Address + 0x02E0
复位值:0x0000_0000
每4位控制一个引脚的配置,GPM4CON寄存器32位控制GPM4_0 ~ GPM4_7八个引脚,上图中列举出了GPM4_0 ~ GPM4_3四个引脚的控制位,其配置效果如上图所示。
②、端口组GPM4数据寄存器
基地址: 0x1100_0000
寄存器地址:Base Address + 0x02E4
复位值:Reset Value = 0x00
当端口配置为输入端口时,则对应的位为引脚状态。当配置为输出端口时,引脚状态应与相应的位一致。当端口被配置为功能引脚时,将读取未定义的值。
③、端口组GPX3配置寄存器
基地址: 0x1100_0000
寄存器地址:Base Address + 0x0C60
复位值:0x0000_0000
每4位控制一个引脚的配置,本场景中GPX3CON[2] ~ GPX3CON[5]。
④、端口组GPX3数据寄存器
基地址: 0x1100_0000
寄存器地址:Base Address + 0x0C64
复位值:0x00
当端口配置为输入端口时,则对应的位为引脚状态。当配置为输出端口时,引脚状态应与相应的位一致。当端口被配置为功能引脚时,将读取未定义的值。
四、驱动程序开发
使用Notepad++ "文件" --- "打开文件夹作为工作区",可以看到上图所示文件。总体上和前几期的GPIO实验差别不大。去掉了蜂鸣器驱动程序。APP由于较为简单,只有一个.c文件,所以APP中没有使用.h文件。驱动部分有led驱动、key驱动、延时函数。STARTUP中有个startup.s文件,是C语言程序启动前执行的汇编语言代码,在前几期中,启动程序都由Keil5自动处理了,但这里没有集成开发环境,startup.s需要自己写,其实就是一个简单的堆栈设置(C语言运行必须要有堆栈)和跳转,初始化运行环境的很多操作BL1都做过了,我们的程序是放在BL2位置的。还有个比较重要的文件“Makefile”,这个文件非常重要,以后开发大型程序,乃至跑操作系统的复杂嵌入式系统,处处都有这个文件。另外分析大型的复杂嵌入式软件系统,往往也是从这个文件开始的,后面会有更详细的解释。
1、启动代码
.text
.global _start
/* 定义常量,用于初始化堆栈指针 */
#define STACK_SIZE 0x1000
#define STACK_BASE 0x02023400
/* 定义C语言程序入口点 */
.extern main
_start:
/* 初始化堆栈指针 */
ldr sp,=0x02024400
/* 跳转到C语言程序入口点 */
bl main
halt_loop:
b halt_loop
.text .global 是arm-linux-gcc编译器的关键词。
.text 指定了后续编译出来的内容放在代码段可执行。
.global 告诉编译器后续跟的是一个全局度可见的名字可能是变量,也可以是函数名。
start是一个函数的起始地址,也是编译、链接后程序的起始地址。由于程序是通过加载器来加载的,必须要找到_start名字的函数,因此_start必须定义成全局的。
接下来这几行是为了注释,在汇编代码中并不起作用,汇编代码中/*...*/或者在某行前面加"#"。都是注释的意思。
/* 定义常量,用于初始化堆栈指针 */
#define STACK_SIZE 0x1000
#define STACK_BASE 0x02023400
后面初始化堆栈指针,可以看到sp指针指向了STACK_BASE + STACK_SIZE处。堆栈使用时是从高地址向低地址增长的,此处我们把堆栈指针移向了STACK_BASE后4K字节处,大约(因为程序本身也会占一些空间,现在程序较小,所以占的很少)给程序分配了4K的堆栈空间,对于当前程序来说,完全足够用了。
在linux中
./ 表示当前目录
../ 表示当前目录的上级目录
../../ 表示当前目录的上上级目录
2、LED驱动
led.h
#ifndef _LED_H_
#define _LED_H_
void led_init();
void led_on(unsigned char site);
void led_off(unsigned char site);
char get_led_status(unsigned char site);
void led_operate(unsigned char site,unsigned char on_off);
#endif
led.c
#include "../include/led.h"
#define GPM4CON (*(volatile unsigned long *)0x110002E0)
#define GPM4DAT (*(volatile unsigned long *)0x110002E4)
void led_init(){
//GPM4CON最后16位分别置0001 GPM4_0 ~GPM4_3为输出模式
GPM4CON = GPM4CON | 0x1111;
}
void led_on(unsigned char site){
led_init();
switch(site){
case 0:
GPM4DAT &= ~(0x01); //P0.0置0
break;
case 1:
GPM4DAT &= ~(0x01<<1); //P0.1置0
break;
case 2:
GPM4DAT &= ~(0x01<<2); //P0.2置0
break;
case 3:
GPM4DAT &= ~(0x01<<3); //P0.3置0
break;
default:
break;
}
}
void led_off(unsigned char site){
led_init();
switch(site){
case 0:
GPM4DAT |= (0x01); //P0.0置1
break;
case 1:
GPM4DAT |= (0x01<<1); //P0.1置1
break;
case 2:
GPM4DAT |= (0x01<<2); //P0.2置1
break;
case 3:
GPM4DAT |= (0x01<<3); //P0.3置1
break;
default:
break;
}
}
char get_led_status(unsigned char site){
switch(site){
case 0:
return (GPM4DAT >> 0) & (0x01);
case 1:
return (GPM4DAT >> 1) & (0x01);
case 2:
return (GPM4DAT >> 2) & (0x01);
case 3:
return (GPM4DAT >> 3) & (0x01);
default:
return -1;
}
}
//on_off 0:开灯 1:关灯
void led_operate(unsigned char site,unsigned char on_off){
if(on_off == 0){
led_on(site);
}else if(on_off == 1){
led_off(site);
}
}
3、KEY驱动
key.h
#ifndef _KEY_H_
#define _KEY_H_
void key_init();
char scan_keyboard();
#endif
key.c
#include "../include/delay.h"
#include "../include/key.h"
#define GPX3CON (*(volatile unsigned long *)0x11000C60)
#define GPX3DAT (*(volatile unsigned long *)0x11000C64)
#define GET_GPX3DAT(x) ((GPX3DAT >> x) & (0x01))
void key_init(){
//GPX3CON第8~23位置0 GPX3CON[2] ~ GPX3CON[5]为输入模式
GPX3CON = GPX3CON & 0xff0000ff;
}
char scan_keyboard(){ //返回当前操作过的按键位置
key_init();
char site = -1;
if(GET_GPX3DAT(2) == 0){
delayms(10);
if(GET_GPX3DAT(2) == 0){
while(GET_GPX3DAT(2)==0);
site = 0;
}
}else if(GET_GPX3DAT(3) == 0){
delayms(10);
if( GET_GPX3DAT(3) == 0){
while(GET_GPX3DAT(3) == 0);
site = 1;
}
}else if(GET_GPX3DAT(4) == 0){
delayms(10);
if( GET_GPX3DAT(4) == 0){
while(GET_GPX3DAT(4) == 0);
site = 2;
}
}else if(GET_GPX3DAT(5) == 0){
delayms(10);
if( GET_GPX3DAT(5) == 0){
while(GET_GPX3DAT(5) == 0);
site = 3;
}
}
return site;
}
delay.h
#ifndef _DELAY_H_
#define _DELAY_H_
void delayms(unsigned int xms);
#endif
delay.c
#include "../include/delay.h"
void delayms(unsigned int xms){ //毫秒级延时函数
unsigned int i,j;
for(i=xms;i>0;i--){
for(j=2500;j>0;j--);
}
}
五、应用程序开发
application.c
#include "../../DRIVER/include/led.h"
#include "../../DRIVER/include/key.h"
int main(void){
led_operate(0,1);
led_operate(1,1);
led_operate(2,1);
led_operate(3,1);
while(1){
unsigned char key_site = scan_keyboard(); //扫描按键状态
char led_status = -1;
switch(key_site){
case 0: //按键一被按了一次
led_status = get_led_status(0);
if(led_status == 0){
led_operate(0,1); //D1状态改变
}else if(led_status == 1){
led_operate(0,0); //D1状态改变
}
break;
case 1:
led_status = get_led_status(1);
if(led_status == 0){
led_operate(1,1); //D2状态改变
}else if(led_status == 1){
led_operate(1,0); //D2状态改变
}
break;
case 2:
led_status = get_led_status(2);
if(led_status == 0){
led_operate(2,1); //D3状态改变
}else if(led_status == 1){
led_operate(2,0); //D3状态改变
}
break;
case 3:
led_status = get_led_status(3);
if(led_status == 0){
led_operate(3,1); //D4状态改变
}else if(led_status == 1){
led_operate(3,0); //D4状态改变
}
break;
default:
break;
}
}
return 0;
}
六、Makefile编写
Exynos4412_GPIO.bin:startup.o application.o led.o key.o delay.o
arm-linux-ld -Ttext 0x02023400 -g startup.o application.o led.o key.o delay.o -o Exynos4412_GPIO_elf
arm-linux-objcopy -O binary -S Exynos4412_GPIO_elf Exynos4412_GPIO.bin
arm-linux-objdump -D -m arm Exynos4412_GPIO_elf > Exynos4412_GPIO.dis
startup.o:./STARTUP/startup.s
arm-linux-gcc -g -c -o startup.o ./STARTUP/startup.s
application.o:./APP/source/application.c ./DRIVER/include/led.h ./DRIVER/include/key.h
arm-linux-gcc -g -c -o application.o ./APP/source/application.c
led.o:./DRIVER/source/led.c ./DRIVER/include/led.h
arm-linux-gcc -g -c -o led.o ./DRIVER/source/led.c
key.o:./DRIVER/source/key.c ./DRIVER/include/key.h ./DRIVER/include/delay.h
arm-linux-gcc -g -c -o key.o ./DRIVER/source/key.c
delay.o:./DRIVER/source/delay.c ./DRIVER/include/delay.h
arm-linux-gcc -g -c -o delay.o ./DRIVER/source/delay.c
clean:
rm -f Exynos4412_GPIO.dis Exynos4412_GPIO.bin Exynos4412_GPIO_elf *.o
Makefile的编写规则类似以下过程:
包子:面粉 水 蔬菜 肉类 调料
择菜 --- 洗菜 --- 绞肉 --- 做馅儿 --- 和面 --- 包包子 --- 蒸包子 --- 出锅
面粉:小麦
耕种 --- 施肥 --- 浇水 --- 收获 --- 磨面
调料:粮食 水
粮食 --- 发酵 --- 勾兑
......
包子是我们最终要的,要得到包子需要依赖面粉、水、蔬菜、肉类、调料这些原料,这些原料经过一系列加工过程变成了包子。但这些原料也不是本来就有的,比如面粉是由小麦这种原料做的,小麦经过一系列过程变成了面粉。
对照程序Makefile,我们最终要的是Exynos4412_GPIO.bin,它依赖startup.o application.o led.o key.o delay.o这些原料,经过下面arm-linux-xxx的一系列加工过程,原料变成了我们想要的东西。接着startup.o这种原料不是本来就有的,它依赖./STARTUP/startup.s这种原料,经过arm-linux-gcc -g -c -o startup.o ./STARTUP/startup.s加工过程,得到了startup.o。其他的以此类推。最后的那个clean含义特殊,它下面是一条linux中的删除命令,删除.bin文件和编译过程中产生的文件,执行“make clean”命令时对应的clean下面的指令。Makefile文件编写好后,编译的时候直接执行"make"就可以生成我们最终想要的Exynos4412_GPIO.bin文件了,想要重新编译的话先执行“make clean”删除当前的编译成果,然后再执行"make"。
上面的Makefile中涉及到arm-linux-gcc、arm-linux-ld、arm-linux-objcopy 、arm-linux-objdump编译命令,Makefile这些命令的含义请自行学习。"arm-linux-ld -Ttext 0x02023400"指定的是程序的运行起始地址,本次编写的GPIO实验程序放置到BL2段,其运行起始地址为0x02023400。此处没有定义数据段、bss段的起始地址,它们被依次放在代码段的后面。后面程序复杂了之后,可通过.lds文件指定各段的地址,在链接的时候需要把.lds文件配置进去。
七、Linux虚拟机安装
Linux虚拟机的安装和Linux基础命令请自行学习。Linux有很多发行版,推荐使用Ubuntu。
可参考:
VMware虚拟机安装Linux教程(超详细)_虚拟机安装linux系统_七维大脑的博客-CSDN博客
Linux基础(超级无敌认真好用,万字收藏篇!!!!)_@活着笑的博客-CSDN博客
《鸟哥的Linux私房菜》
八、交叉编译与程序烧录
1、安装交叉编译工具
Tiny4412开发板或同类别开发板官方提供的资料中通常都有这些工具,从光盘中找到文件“arm-linux-gcc-4.5.1-v6-vfp-20120301.tgz”,把它拷贝到Linux根目录下("/"目录)。执行“tar -zxvf arm-linux-gcc-4.5.1-v6-vfp-20120301.tgz” 命令,将文件解压缩,解压缩之后,在Linux的/opt目录下会出现一个“FriendlyARM”文件夹,进去文件夹,一路进入下级文件夹,会跟踪到“/opt/FriendlyARM/toolschain/4.5.1/bin”。到这里可以看到这个bin文件夹下面有大量的arm-linux-xxx命令,这些就是交叉编译工具。
为什么要是用交叉编译工具,而不能直接使用linux下面的编译工具gcc呢?这个也很容易理解,我们的电脑CPU都是X86架构的,CPU指令集属于CISC指令集,而要运行程序的开发板属于ARM架构,为RISC指令集。编译的目的是最终把源码转换成CPU能理解的能执行的程序,ARM架构的CPU是看不懂X86架构的指令集的。如果直接使用gcc进行编译,就把源程序编译成了X86架构的CPU才能认识的可执行程序,而交叉编译工具则是在电脑上把源码编译成了能在开发板上运行的可执行程序。所以叫做交叉编译。
完成解压缩后,运行“ echo $PATH”查看一下现在的环境变量,可以看到路径“/opt/FriendlyARM/toolschain/4.5.1/bin”还没有加入到环境变量中。运行“vim /etc/bash.bashrc”编辑/etc/bash.bashrc文件,在最后一行添加“export PATH=/opt/FriendlyARM/toolschain/4.5.1/bin:$PATH”
注意最后的那个“:$PATH”是必须的,否则就把以前的环境变量都清空了。添加并保存后执行命令“source /etc/bash.bashrc”使配置生效。再执行“ echo $PATH”就可以看到交叉编译文件路径已经被添加进去了。运行“arm-linux-gcc -v”可以查看编译工具版本。
意味着交叉编译环境已经配置完成。交叉编译工具已生效。
2、编译源码
Exynos4412_GPIO_Project目录下直接执行make命令,生成了Exynos4412_GPIO.bin文件。
3、SD卡启动制作工具
从开发板官方提供的资料中找到“uboot_tiny4412-20130729.tgz”,这玩意是uboot,是启动操作系统用的。像Cortex-A系列这种高级货生来就是为了跑操作系统用的,在实际运用中几乎不会见到拿Cortex-A系列的芯片作为裸机使用,现在的实验是为了更熟悉它,为以后的系统移植做准备。uboot本身是跑在裸机上的,也是对应BL2。所以在uboot_tiny4412-20130729.tgz包中包含了一个sd卡启动制作工具。
把“uboot_tiny4412-20130729.tgz”拷贝到你在Linux上的工作目录(自建一个目录,或者看哪个顺眼都行,我的是“/opt/Exynos4412”)下,运行“tar -zxvf uboot_tiny4412-20130729.tgz”解压缩,之后找到"uboot_tiny4412/sd_fuse",它就是可以把sd制作成启动卡的工具。
目录下文件如图所示,先打开V310-EVT1-mkbl2.c文件,注释掉46~62行,并增加以下红框中的内容。因为在烧写sd卡的脚本uboot_tiny4412/sd_fuse/tiny4412/sd_fusing.sh中有./mkbl2 xxx(要放置到BL2位置的文件) bl2.bin 14336命令。如果不修改V310-EVT1-mkbl2.c源码,那么编译出的mkbl2工具就会按照14K大小计算校验码,Exynos4412_GPIO.bin显然没有14K这么大,会发生数据溢出,无法把我们的Exynos4412_GPIO.bin制作成bl2.bin文件。
之后,在“uboot_tiny4412/sd_fuse”目录下执行“make”命令。
可以看到生成了mkbl2、sd_fdisk工具。接着进入 tiny4412目录。
这个目录下有BL1的可执行文件E4412_N.bl1.bin 和SD卡烧录脚本sd_fusing.sh。
打开sd_fusing.sh,第44行可以看到,脚本默认使用u-boot.bin生成bl2.bin文件,这里替换成本次要逻辑运行的可执行程序Exynos4412_GPIO.bin。


4、制作SD卡
SD卡插入到电脑上,选择连接到虚拟机,这时候在Linux系统的/dev文件夹下会生成对应的设备文件,找到这个设备文件(看时间,刚生成的那个就是。或者插拔一下SD卡,看看多出来的是哪个设备文件,通常是/dev/sdb)。执行命令 “./sd_fusing.sh /dev/xxx(sd卡的设备文件)”,会看到BL1和BL2烧录进去了。
取下SD卡插回开发板,选择开关拨到从SD卡启动,重启开发板。这时候按键就可以控制对应LED的亮灭了。
九、资料下载
Exynos4412裸机开发参考资料和必备资源:https://download.csdn.net/download/qq_54140018/87706193
实验源码(基于Tiny4412开发板):https://download.csdn.net/download/qq_54140018/87706199