第一个程序,驱动LED灯亮。包含4个文件:Makefile,make命令批处理文件。write2sd是一个脚本文件。start.s汇编文件,驱动开发板亮灯的逻辑实现。mkv210_image.c文件,它的的作用就是用来给原始的bin文件添加头部。
Makefile(听说linux区分大小写,所以首M还是大写的好)
代码:
led.bin: start.o
arm-linux-ld -Ttext 0x0 -o led.elf $^
arm-linux-objcopy -O binary led.elf led.bin
arm-linux-objdump -D led.elf > led_elf.dis
gcc mkv210_image.c -o mkmini210 ./mkmini210 led.bin 210.bin
%.o : %.S
arm-linux-gcc -o $@ $< -c
%.o : %.c
arm-linux-gcc -o $@ $< -c
clean:
rm *.o *.elf *.bin *.dis mkmini210 -f
其中
“$@”可代表目标文件的完整名称
“$<”可代表第一个文件的依赖文件的名称
“$^"可代表所有不重复的依赖文件
.o文件:目标文件
.s/.S文件:汇编语言原始程序
.h文件:预处理文件(头文件)
.c:c原始程序文件
.cc/.C/.cxx:c++原始程序文件
.m:Objective.c原始程序文件
.i:已经预处理过的c原始程序
.ii:已经预处理过额c++原始程序文件
.a/.so:编译后的库文件
%.o(要生成的文件) : %.S(依赖文件)
arm-linux-gcc -o $@ $< -c(操作)
表示对所有的".S"汇编文件编译成“.o”文件。“-c”(compile)参数进行预处理、编译、汇编操作 , "-o" 参数用于指定输出的文件,后面紧接着的就是文件名
%.o : %.c
arm-linux-gcc -o $@ $< -c
表示对所有的".c"汇编文件编译成“.o”文件。
arm-linux-ld -Ttext 0x0 -o led.elf $^
arm-linux-ld 连接命令,这里表示将“所有不重复的依赖文件”连接成led.elf这个文件,此可执行文件的代码段的起始地址为0x00000000(从此处开始执行)“-Ttext 0x0”设置代码段的起始地址为0x00000000;
arm-linux-objcopy -O binary led.elf led.bin
arm-linux-objcopy 复制一个目标文件的内容到另一个文件中,可以用于不同格式文件之间的格式转换
参数:
.-l bfdname(格式名)或—input-target=bfdname
用来指明源文件的格式,bfdname是BFD库中描述的标准格式名,如果没指明,则arm-linux-objcopy自己分析
-O bfdname 输出的格式(binary二进制)
-F bfdname 同时指明源文件,目的文件的格式
-R sectionname 从输出文件中删除掉所有名为sectionname的段
-S 不从源文件中复制重定位信息和符号信息到目标文件中
-g 不从源文件中复制调试符号到目标文件中
arm-linux-objdump -D led.elf > led_elf.dis
常用来显示二进制文件信息,和查看反汇编代码。
'>' 表示将这个程序的反汇编程序写入到led_elf.dis这个文件中,在终端中不显示出来. 当你打开led_elf.dis这个文件时就会看到上面命令的输出的反汇编程序了。 至于led_elf.dis是什么文件,我也不知道,网上也没有详细的讲解
<span class="m"></span>“./” 表示当前路径 linux下 “.” 是当前目录 “ ..” 是父目录
参数:
-b bfdname 指定目标码格式
-d 反汇编可执行段
-D 反汇编所有段
-EB,-EL指定字节序
-f 显示文件的整体头部摘要信息
-h 显示目标文件中各个段的头部摘要信息
-I 显示支持的目标文件格式和CPU架构
-j name显示指定section的信息
-m machine 指定反汇编目标文件时使用的架构
start。s:
.globl _start
_start:
// 设置GPJ2CON的bit[0:15],配置GPJ2_0/1/2/3引脚为输出功能
ldr r1, =0xE0200280 //0xE0200280的地址上是三星这款芯片的GPJ2CON,外围连得是led
ldr r0, =0x00001111 //4个都设为“1”,输出模式
str r0, [r1] //str,按一个字长写入。把r0中的内容写到r1里内容表示的地址上去
mov r2, #0x1000
led_blink:
// 设置GPJ2DAT的bit[0:3],使GPJ2_0/1/2/3引脚输出低电平,LED亮
ldr r1, =0xE0200284 //0xE0200284的地址上是三星这款芯片的GPJ2DAT
mov r0, #0 //赋值“0”,让它亮
str r0, [r1]
// 延时
bl delay //bl,带返回的跳转
// 设置GPJ2DAT的bit[0:3],使GPJ2_0/1/2/3引脚输出高电平,LED灭
ldr r1, =0xE0200284
mov r0, #0xf //赋值“1”,让它灭
str r0, [r1]
// 延时
bl delay
sub r2, r2, #1 //要这样亮r2=1000次
cmp r2,#0
bne led_blink //
halt:
b halt //程序最后的死循环
delay:
mov r0, #0x100000
delay_loop:
cmp r0, #0 //cmp,比较但不改变比较的两个数的,只影响标志位。
sub r0, r0, #1 //sub,减。r0=r0-1
bne delay_loop //bne,不为零跳转
mov pc, lr //lr:链接寄存器,储存子程序返回地址(保存现场用)
mkv210_image。c:
(该段描述来至手册)从三星提供的S5PV210文档《S5PV210_iROM_ApplicationNote_Preliminary_20091126.pdf》以及芯片手册《S5PV210_UM_REV1.1.pdf》得知,S5PV210启动时,会先运行内部IROM中的固化代码进行一些必要的初始化,执行完后硬件上会自动读取NAND Flash或sd卡等启动设备的前16K的数据到IRAM中,这16K数据中的前16byte中保存了一个叫校验和的值,拷贝数据时S5PV210会统计出待运行的bin文件中含‘1’的个数,然后和校验和做比较,如果相等则继续运行程序,否则停止运行。所以所有在S5PV210上运行的bin文件都必须具有一个16byte的头部,该头部
第- 11 –页
中需包含校验和信息,mkv210_image.c由arm9论坛上的网友提供,它的的作用就是用来给原始的bin文件添加头部。 mkv210_image.c的核心工作如下:
第一步 分配16k的buffer;
第二步 将led.bin读到buffer的第16byte开始的地方;
第三步 计算校验和,并将校验和保存在buffer第8~11byte中;
第四步 将16k的buffer拷贝到210.bin中;
校验和统计的关键代码如下: a = Buf + SPL_HEADER_SIZE; for(i = 0, checksum = 0; i < IMG_SIZE - SPL_HEADER_SIZE; i++) checksum += (0x000000FF) & *a++; 代码mkv210_image.c已经包含相当丰富的注释,很容易就可以读懂,用户可以自行阅读。
/* 在BL0阶段,Irom内固化的代码读取nandflash或SD卡前16K的内容,
* 并比对前16字节中的校验和是否正确,正确则继续,错误则停止。
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BUFSIZE (16*1024) //定义buffer区大小
#define IMG_SIZE (16*1024) //定义IMG区大小
#define SPL_HEADER_SIZE 16 //定义SPL_HEADER所占大小
#define SPL_HEADER "S5PC110 HEADER " //定义SPL_HEADER的内容
int main (int argc, char *argv[])
{
FILE *fp; //
char *Buf, *a; //buf是指向buffer区的指针
int BufLen; //
int nbytes, fileLen; //fileLen文件大小
unsigned int checksum, count; //
int i; //
// 1. 3个参数
if (argc != 3)
{
printf("Usage: mkbl1 <source file> <destination file>\n");
return -1;
}
// 2. 分配16K的buffer
BufLen = BUFSIZE;
Buf = (char *)malloc(BufLen); //动态分配内存空间,malloc返回的指针赋给BUF
if (!Buf) //如果Buf没有得到结果
{
printf("Alloc buffer failed!\n");
return -1;
}
//memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
/*
void *memset(void *s, int ch, size_t n);
函数解释:将s中前n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
eg:
char buffer[]="Helloworld\n";
printf("Buffer before memset:%s\n",buffer);
memset(buffer,'*',strlen(buffer));
printf("Buffer after memset:%s\n",buffer);
结果:
Buffer before memset:Helloworld
Buffer after memset:***********
*/
memset(Buf, 0x00, BufLen); //这里只是简单的对buffer区全部清零
// 3. 读源bin到buffer
// 3.1 打开源bin
//fopen函数用来打开一个文件.file+open
/*
函数原型:FILE * fopen(const char * path,const char * mode);
返回值:文件顺利打开后,指向该流的文件指针就会被返回。如果文件打开失败则返回NULL,并把错误代码存在errno中。
eg:
FILE *fp = NULL;
char tmp[100];
fp = fopen("/opt/C_lanuage/fopen_fread/tmp.txt", "r");
if (NULL == fp)
{
printf("file open Fail!\n");
return -1;
}
fread(tmp, 1, 100, fp);
printf("%s\n", tmp);
fclose(fp);
fp = NULL;
一般而言,打开文件后会做一些文件读取或写入的动作,若打开文件失败,接下来的读写动作也无法顺利进行,所以一般在fopen()后作错误判断及处理。
参数说明:
参数path字符串包含欲打开的文件路径及文件名,参数mode字符串则代表着流形态。
mode有下列几种形态字符串:
r 以只读方式打开文件,该文件必须存在。
r+ 以可读写方式打开文件,该文件必须存在。
rb+ 读写打开一个二进制文件,允许读写数据,文件必须存在。
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留)
wb 只写打开或新建一个二进制文件;只允许写数据。
wb+ 读写打开或建立一个二进制文件,允许读和写。
ab+ 读写打开一个二进制文件,允许读或在文件末追加数据。
wx 创建文本文件,只允许写入数据.[C11]
wbx 创建一个二进制文件,只允许写入数据.[C11]
w+x 创建一个文本文件,允许读写.[C11]
wb+x 创建一个二进制文件,允许读写.[C11]
w+bx 和"wb+x"相同[C11]
以x结尾的模式为独占模式,文件已存在或者无法创建(一般是路径不正确)都会导致fopen失败.文件以操作系统支持的独占模式打开.[C11]
上述的形态字符串都可以再加一个b字符,如rb、w+b或ab+等组合,加入b 字符用来告诉函数库以二进制模式打开文件。如果不加b,表示默认加了t,即rt,wt,其中t表示以文本模式打开文件。由fopen()所建立的新文件会具有S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH(0666)权限,此文件权限也会参考umask值。
有些C编译系统可能不完全提供所有这些功能,有的C版本不用"r+","w+","a+",而用"rw","wr","ar"等,读者注意所用系统的规定。
二进制和文本模式的区别
1.在windows系统中,文本模式下,文件以"\r\n"代表换行。若以文本模式打开文件,并用fputs等函数写入换行符"\n"时,函数会自动在"\n"前面加上"\r"。即实际写入文件的是"\r\n" 。
2.在类Unix/Linux系统中文本模式下,文件以"\n"代表换行。所以Linux系统中在文本模式和二进制模式下并无区别。
打开方式总结:各种打开方式主要有三个方面的区别:
①打开是否为二进制文件,用“b”标识。
②读写的方式,有以下几种:只读、只写、读写、追加只写、追加读写这几种方式。
③对文件是否必 须存在、以及存在时是清空还是追加会有不同的响应。
*/
fp = fopen(argv[1], "rb"); //以读二进制的方式打开argv[1](main运行时传入的第一个参数)里表示的文件,成功就返回指向该流的文件指针(读文件内容的指针)
if( fp == NULL) //如果没有这个文件
{
printf("source file open error\n");
free(Buf); //释放Buf
return -1;
}
// 3.2 获取源bin长度
fseek(fp, 0L, SEEK_END); //文件指针移到结尾
/*
int fseek(FILE *stream, long offset, int fromwhere);fseek 用于二进制方式打开的文件,移动文件读写指针位置.
fseek(in,-1L,1); -- 文件流in, 零点为当前指针位置,SEEK_CUR 就是 1, -1L -- 文件指针回退1个字节int fseek( FILE *stream, long offset, int origin );
第一个参数stream为文件指针
第二个参数offset为偏移量,整数表示正向偏移,负数表示负向偏移
第三个参数origin设定从文件的哪里开始偏移,可能取值为:SEEK_CUR、 SEEK_END 或 SEEK_SET
SEEK_SET: 文件开头
SEEK_CUR: 当前位置
SEEK_END: 文件结尾
其中SEEK_SET,SEEK_CUR和SEEK_END和依次为0,1和2.
简言之:
fseek(fp,100L,0);把fp指针移动到离文件开头100字节处;
fseek(fp,100L,1);把fp指针移动到离文件当前位置100字节处;
fseek(fp,100L,2);把fp指针退回到离文件结尾100字节处。
*/
fileLen = ftell(fp); // ftell 用于得到文件位置指针当前位置相对于文件首的偏移字节数
fseek(fp, 0L, SEEK_SET); //文件指针移到开头
// 3.3 源bin长度不得超过16K-16byte
count = (fileLen < (IMG_SIZE - SPL_HEADER_SIZE))//文件大小要小于IMG_SIZE - SPL_HEADER_SIZE所剩下的大小
? fileLen : (IMG_SIZE - SPL_HEADER_SIZE);
// 3.4 buffer[0~15]存放"S5PC110 HEADER "
/*
void *memcpy(void *dest, const void *src, size_t n);
从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
eg:
char src[] ="******************************";
char dest[]="abcdefghijlkmnopqrstuvwxyz0123as6";
printf("destination before memcpy:%s\n",dest);
memcpy(dest,src,strlen(src));
printf("destination after memcpy:%s\n",dest);
结果:
destination before memcpy:abcdefghijlkmnopqrstuvwxyz0123as6
destination after memcpy: ******************************as6
*/
memcpy(&Buf[0], SPL_HEADER, SPL_HEADER_SIZE);//从SPL_HEADER拷贝SPL_HEADER_SIZE个字节,到Buf中
// 3.5 读源bin到buffer[16]
nbytes = fread(Buf + SPL_HEADER_SIZE, 1, count, fp);//从fp中读入count个数据到Buf + SPL_HEADER_SIZE处
/*
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
从一个stream文件流中读数据,最多读取count个元素,每个元素size字节,如果调用成功返回实际读取到的元素个数,如果不成功或读到文件末尾返回 0。
参 数
buffer 用于接收数据的内存地址
size 要读的每个数据项的字节数,单位是字节
count 要读count个数据项,每个数据项size个字节.
stream 输入流
返回值
实际读取的元素个数。如果返回值与count不相同,则可能文件结尾或发生错误。从ferror和feof获取错误信息或检测是否到达文件结尾。
*/
if ( nbytes != count )//如果实际读取的数据没有count个时
{
printf("source file read error\n");
free(Buf);
fclose(fp);
return -1;
}
fclose(fp); //关闭文件fp
// 4. 计算校验和
// 4.1 从第16byte开始统计buffer中共有几个1
a = Buf + SPL_HEADER_SIZE;
for(i = 0, checksum = 0; i < IMG_SIZE - SPL_HEADER_SIZE; i++)
checksum += (0x000000FF) & *a++;
// 4.2 将校验和保存在buffer[8~15]
a = Buf + 8;
*( (unsigned int *)a ) = checksum;
// 5. 拷贝buffer中的内容到目的bin
// 5.1 打开目的bin
fp = fopen(argv[2], "wb");
if (fp == NULL)
{
printf("destination file open error\n");
free(Buf);
return -1;
}
// 5.2 将16k的buffer拷贝到目的bin中
a = Buf;
/*
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
注意:这个函数以二进制形式对文件进行操作,不局限于文本文件
返回值:返回实际写入的数据块数目
(1)buffer:是一个指针,对fwrite来说,是要获取数据的地址;
(2)size:要写入内容的单字节数;
(3)count:要进行写入size字节的数据项的个数;
(4)stream:目标文件指针;
(5)返回实际写入的数据项个数count。
说明:写入到文件的哪里? 这个与文件的打开模式有关,如果是w+,则是从file pointer指向的地址开始写,替换掉之后的内容,文件的长度可以不变,stream的位置移动count个数;如果是a+,则从文件的末尾开始添加,文件长度加大。
fseek对此函数有作用,但是fwrite[1] 函数写到用户空间缓冲区,并未同步到文件中,所以修改后要将内存与文件同步可以用fflush(FILE *fp)函数同步。
eg:
FILE *fp;
char msg[] = "file content";
char buf[20];
fp = fopen("d:\\a\\a.txt","w+");
if (NULL == fp)
{
printf("The file doesn't exist!\n");
return -1;
}
fwrite(msg,strlen(msg),1,fp);//把字符串内容写入到文件
fseek(fp,0,SEEK_SET);//定位文件指针到文件开始位置
fread(buf,strlen(msg),1,fp);//把文件内容读入到缓存
buf[strlen(msg)] = '\0';//删除缓存内多余的空间
printf("buf = %s\n",buf);
printf("strlen(buf) = %d\n",strlen(buf));
*/
nbytes = fwrite( a, 1, BufLen, fp);
if ( nbytes != BufLen )
{
printf("destination file write error\n");
free(Buf);
fclose(fp);
return -1;
}
free(Buf);
fclose(fp);
return 0;
}
write2sd文件:
#!/bin/sh
sudo dd iflag=dsync oflag=dsync if=210.bin of=/dev/sdb seek=1
将sd卡插入PC,在linux终端执行如下命令:
# cd 1.led_s
# make
# chmod 777 write2sd
# ./write2sd
执行make后会生成210.bin文件,执行./write2sd后210.bin文件会被烧写到sd卡的扇区1中,sd卡的起始扇区为0,一个扇区的大小为512byte,sd启动时,IROM里的固化代码是从扇区1开始拷贝代码的。
write2sd是一个脚本文件,内容如下: #!/bin/sh sudo dd iflag=dsync oflag=dsync if=210.bin of=/dev/sdb seek=1 dd是一个读写命令,if是输入,of是输出,seek表示从扇区1开始读写。
注:sdb为本文档编写时sd卡的设备节点,请根据自己的实际情况进行修改,后面将不再提及该注意事项。
------------------------------后面有可能会遇到的问题------------------------------------------
首先make前,你要保证你安装好了arm-linux-gcc。手册有装的方法
如果是用别人提供的代码,请注意权限问题
执行write2sd前,注意你的sd卡是否已挂载,挂载设备名称是不是/dev/sdb,如不是,请注意修改write2sd