最近准备转行做嵌入式,随弄来一块S3C44B0X的板子,准备好好研究一下。
板子便宜货,没啥特别完善的资料,都是和网上差不多的ADS环境。
因为平时还要上班,花了几个晚上的时间,总算是把u-boot和uClinux的编译和下载过程弄熟了。这不,好不容易等来一个周末,决心一定要弄出点名堂来,不能再在门外徘徊了。
先说说工作环境。
主机(自己家的,研究Linux内核的那个是公司的><):
CPU: C4 2.93G
Memory: 2G
硬盘: 很大 ~o~
网络: 电信ADSL 2M
OS: WinXP SP2
虚拟机(VirtualBox):
Memory: 512M
OS: Ubuntu 7.10 JeOS
硬盘: 2G + 4G VDI
网络: 共享主机网络
开发板:
MPU: S3C44B0X 66M
ROM: 8M Flash
RAM: 2M SDRAM
IDE: 有
USB: 1.1
LCD: 单色128x64
COM: RS232 UART x 2
JTAG: 有
网卡: RTL8019 10M
其它: 1 x 4键盘,1 x 3 LED,还有其它一些东西是啥我也不懂,再说- -
因为没多余的网卡,又不想断网,所以先用串口的蜗牛传输撑着吧。其实除了下载uClinux等待时间比较BT以外,对于自己写的程序还好,一般就几k不到,再慢的速度也无所谓倒是。
其实附送资料上的示例程序里早有了跑马灯,按着文档上的方法跑了一遍,虽然执行正常但是毫无感觉,完全搞不明白是怎么回事,换句话说,毫无成就感。我心想这可不行,这跟没学差不多,等想想办法了。
仔细想想最莫名其妙的部分就是ADS的一堆设置,以及代码里一些奇怪的外部符号比如|Image$$RO$$Base|之类的,不知道是怎么来的,后来google了老半天才知道是ADS的设置参数。这些东西一来就搞的人头晕眼花的,于是我决定换一个思路,扔掉ADS,手动搞定这一切。
说起手动,不是一件容易的事情。本来我准备在板子上裸奔的,但发现自己过于菜鸟,一时半会还弄不出一个bootloader来。搞得不好一个周末下来,马没跑成,我的热情都跑光了。
经过一阵迷茫,我突然想起了u-boot,这个之前我载完uClinux就扔掉的东西。这个开源的bootloader真是好处多多啊,既是bootloader,又能充当BIOS,甚至就像是个没有FS的DOS,可以直接跑裸机程序,但又不需要费劲去弄中断初始化之类的工作。
于是我的方案就出来了,u-boot引导后,用loadb命令装载我的程序到RAM,然后用go命令跳转到程序入口点执行!
做人不能光耍嘴皮子,这就立刻动手!
为了验证我的方案可行,决定先写一个自动重启的程序,思路很简单,直接跳转到00000000h,也就是中断向量表中reset中断的位置执行。代码只有一行:
mov pc, #0
reset.s
.text mov pc, # 0
注意,因为使用GNU工具链有着各种各样的好处,并且对我来说也是轻车熟路,因此这里的汇编语法是GNU AS的语法,不熟悉的人自己google去吧,其实和ADS的ARMASM没什么本质的不同。
因为这程序太简单了,连Makefile也没必要写,在Linux下执行:
arm-elf-as -o reset.o reset.s
arm-elf-objcopy -O binary -R .comment -R .note -S reset.o reset.bin
于是乎就得到了裸机程序reset.bin,大小只有4字节,汗,因为就只有一个指令。
OK,给板子加电,打开超级终端,进入u-boot,输入loadb 0xC0000000后把reset.bin传过去,瞬间就传完了(因为只有4字节。。汗),然后go 0xC0000000。
成功!板子立刻重启了。这证明了我的方案是可行的,剩下的活就是怎么点灯了。
因为我是极度菜鸟,完全不知道怎么下手,看示例代码又完全不知所云。无奈使出google大法,找了一大堆资料。最后我明白了问题的关键就在于I/O(ARM的可编程I/O,PGIO)上,但是由于该芯片的I/O端口是复用的,所以需要编程操作I/O控制寄存器。
顺便提一个概念。与x86机器有很大的不同,ARM是统一编址的(这个概念我以前也只有在课本上见过XD),而x86是独立编址,也就是说存储器和设备寄存器的编址是分开的,因此我们通常需要in out指令来操作IBM PC硬件寄存器,而所谓的硬件端口其实就是不同于存储器的另一套编址方式。对ARM来说,设备寄存器和存储器的编址是统一的,也就是说,直接通过mov等传送指令就可以访问硬件寄存器。独立编址和统一编址各有利弊,这里不做讨论,有兴趣的人请回去补习计算机体系结构。
S3C44B0X这款芯片的硬件寄存器被映射到了物理地址01c00000h~01ffffffh之间4M的地址空间里,对它们访问则可以控制硬件。
我看到网上某篇文章讲到,LED0和LED1通过23(nGCS4)和24(nGCS5)号引脚连接到S3C44B0X芯片,而根据S3C44B0X芯片手册,nGCS4和nGCS5引脚受PCONB控制寄存器控制,当该寄存器的第9位PB9或第10位PB10为1时,通过访问PDATB的第9位或第10 位则可以控制LED0和LED1,低电平LED亮,高电平LED灭。
看起来并不难,动手写了个C文件按照该文所说实现了一个程序led.c,并且在跑马灯结束后自动reset,Linux下:
arm-elf-gcc -nostdinc -c -o led.o led.c
arm-elf-objcopy -O binary -R .comment -R .note -S led.o led.bin
用loadb(不写参数默认载到0x0c008000)载到板上跑了下一看,无效。。。。而且死机(没有reset)。
我郁闷,经过无数的检查之后恍然大悟,直接objcopy led.o,由于led.c里并没有函数入口点,默认入口点会被当作是00000000h,而我的程序是跑在0c008000h处。这么一来,长跳转和函数调用的目标地址都会比正确值小0x0c008000,那当然会有问题了!于是需要这么干:
arm-elf-gcc -c -o led.o led.c
arm-elf-ld -nostdinc -Ttext 0x0c008000 -o led.elf led.o
arm-elf-objcopy -O binary -R .comment -R .note -S led.elf led.bin
载到板上,还是无效!不过不会死机了。
继续郁闷,突然想起来早上泡的衣服还没晾,急急忙忙去晾衣服。在晾衣服的过程中,我突然想到了是不是由于那文章所用的板和我的不一样,因此发光二极管接的位置也不同?于是洗完衣服后我急忙翻出PCB原理图看了起来。幸好大学时候没白费光阴,多少还是能看懂一些的。根据原理图,我发现我的板子LED0~2分别接在117(GPC1)、116(GPC2)、115(GPC3)三个引脚上,并且三个LED都多接了一个反相器(74HC04),如下图。看来果然问题就出在这里。
我重新翻出I/O控制寄存器表,发现这三个引脚是由PCONC控制的,哈,这下好办了。根据资料,我把先前的程序又改了一下,编译通过,载到板子,成功! LED灯终于亮了!不过亮灭是反的。。。怎么回事呢?问题就出在那三个反向器,我把它们给忘了,先前参考的文章里是没有它们的。于是把高低电平反过来,编译,下载,运行,成功!这次LED终于如预期般的亮灭了。
喜悦之余也感叹前面的路途遥远,点个灯都这么难,自己还是太菜了。
最后奉上源代码和Makefile:
led.c
喜悦之余也感叹前面的路途遥远,点个灯都这么难,自己还是太菜了。
最后奉上源代码和Makefile:
led.c
#define PCONC ((volatile int *)0x1d20010)
#define PDATC ((volatile int *)0x1d20014)
#define PUPC ((volatile int *)0x1d20018)
void init( void );
void led( int , int );
void delay( int );
void entry( void ) /* 对于裸机C程序,入口函数必须放在文件的最前面 */
{
init();
while (1) {
led(1, 1); /* 点亮LED1 */
delay(1000); /* 延时 */
led(2, 1); /* 点亮LED2 */
delay(1000);
led(3, 1); /* 点亮LED3 */
delay(1000);
led(1, 0); /* 熄灭LED1 */
delay(1000);
led(2, 0); /* 熄灭LED2 */
delay(1000);
led(3, 0); /* 熄灭LED3 */
delay(1000);
}
}
void init(void)
{ /* 下面两句将PCONC寄存器的PC1、PC2、PC3都置为01,意为将其作为LED输出 */
*PCONC &= ~0xa8;
*PCONC |= 0x54;
}
void led(int num, int light)
{
if (light)
*PDATC |= 1 << num; /* 点亮第num盏灯 */
else
*PDATC &= ~(1 << num); /* 熄灭第num盏灯 */
}
void delay(int times)
{
volatile int i; /* volatile声明防止下面的循环被编译器和谐 */
for (i = 0; i < (times << 10); ++i); /* 非精确延时,临时方法 */
}
#define PDATC ((volatile int *)0x1d20014)
#define PUPC ((volatile int *)0x1d20018)
void init( void );
void led( int , int );
void delay( int );
void entry( void ) /* 对于裸机C程序,入口函数必须放在文件的最前面 */
{
init();
while (1) {
led(1, 1); /* 点亮LED1 */
delay(1000); /* 延时 */
led(2, 1); /* 点亮LED2 */
delay(1000);
led(3, 1); /* 点亮LED3 */
delay(1000);
led(1, 0); /* 熄灭LED1 */
delay(1000);
led(2, 0); /* 熄灭LED2 */
delay(1000);
led(3, 0); /* 熄灭LED3 */
delay(1000);
}
}
void init(void)
{ /* 下面两句将PCONC寄存器的PC1、PC2、PC3都置为01,意为将其作为LED输出 */
*PCONC &= ~0xa8;
*PCONC |= 0x54;
}
void led(int num, int light)
{
if (light)
*PDATC |= 1 << num; /* 点亮第num盏灯 */
else
*PDATC &= ~(1 << num); /* 熄灭第num盏灯 */
}
void delay(int times)
{
volatile int i; /* volatile声明防止下面的循环被编译器和谐 */
for (i = 0; i < (times << 10); ++i); /* 非精确延时,临时方法 */
}
Makefile
CC = arm -elf- gcc
AS = arm-elf-as
LD = arm -elf- ld
OBJCOPY = arm -elf- objcopy
ADDRESS = 0x0c008000
.PHONY : all clean
all : led.bin
clean :
rm - f led.bin
rm - f led.o
rm - f led.elf
rm - f led.S
led.bin : led.elf
$(OBJCOPY) - O binary - R .comment - R .note - S $ < $@
chmod a - x $@
led.elf : led.o
$(LD) - Ttext $(ADDRESS) -nostdinc - o $@ $ <
chmod a - x $@
led.o : led.S
$(AS) - o $@ $ <
led.S : led.c
$(CC) -S - o $@ $ <
AS = arm-elf-as
LD = arm -elf- ld
OBJCOPY = arm -elf- objcopy
ADDRESS = 0x0c008000
.PHONY : all clean
all : led.bin
clean :
rm - f led.bin
rm - f led.o
rm - f led.elf
rm - f led.S
led.bin : led.elf
$(OBJCOPY) - O binary - R .comment - R .note - S $ < $@
chmod a - x $@
led.elf : led.o
$(LD) - Ttext $(ADDRESS) -nostdinc - o $@ $ <
chmod a - x $@
led.o : led.S
$(AS) - o $@ $ <
led.S : led.c
$(CC) -S - o $@ $ <
在u-boot下使用loadb命令,通过超级终端把led.bin下载到开发板的RAM里,然后通过go 0x0c008000命令跳转到程序入口点即可。板子和我的不同的可以根据资料自行修改。