|
Jabber: [email protected]
Email: [email protected]
Abstract:
lpc2210特性简介;
让ARMCC编译的ucosii for LPC2210在SkyEye上运行起来;
把整个工程改造成GCC版本;
关于ucosii本身的一些要点;
调试手段。
作为一个新手,抱着入门的目的,我最近做的工作主要有两部分:
1) 修改和完善SkyEye对LPC221x的支持,略微修改一个已经移植到LPC221x上的ucosii工程(compiled by armcc ),让它也能在SkyEye上面顺利运行起来;
2) 在原有的代码基础上,重新组织整个工程,用GNU arm-tools重新编译,使得生成后的目标也能在SkyEye上模拟。
一、 lpc2210特性简介
这里重点关心lpc2210的独特之处。
体系结构: ARMV
厂商: Philips
CPU核: ARM7TDMI-S
支持ARM指令集: ARM 32位指令集 和 Thumb 16位指令集
需要说明的是ARM7TDMI-S中的最后一个字母,代表“可综合”的意思,对于应用程序员来说是透明的,编程模型比起7TDMI来没有什么不同。
存储器分布:
0x4000 0000 – 0x4000 1FFF 16KB片内SRAM
0x8000 0000 – 0xE000 0000 External Memory
0xE000 0000 – 0xF000 0000 VPB 外设
0xF000 0000 – 0xFFFF FFFF AHB 外设
External Memory的配置和开发板有关。
内存重新映射:
1)启动时刻的控制:
芯片有两个引脚被称为BOOT[1],启动时刻这两个引脚的状态决定了开始从什么地方读取第一条指令。
BOOT[1]/MEMAP[1] 0x00000000 ~ 0x0000003C 映射自:
00 0x7FFFE000 ~ 0x7FFFE03C
01 0x00000000 ~ 0x0000003C
10 0x40000000 ~ 0x4000003C
11 0x80000000 ~ 0x8000003C
2)运行时刻的控制
透过对MEMMAP(0xE01FC040)的控制。
复位时刻,MEMMAP[1]由BOOT[1]的状态决定。
运行时可以写MEMMAP。
这种形式的内存重映射是SkyEye目前没有支持的,要在SkyEye里边运行依靠这个特性的程序,一个折中的办法就是配置两块内存,然后自己在ucosii代码里边手动做内存块的复制。
中断控制器相关:
ARM体系结构规定,遇到IRQ中断的时候,CPU尽量完成当前指令,然后BL到0x00000018处,读取一条指令,同时切换到Svc状态。
如果要写出通用的程序来,应该在这个地方放置到IRQ_Handler的跳转(LDR pc, #IRQ_Handler ),在IRQ_Handler里边读取中断状态控制器,区分中断源,作出相应的处理。
LPC2210的中断控制器是ARM Prime Cell,它有自己的中断处理流程。
首先,ARM PrimeCell中断控制器支持32个变量,即32个external Interrupt Requests
这32个IRQ中,可以抽出16个分配成Vector IRQ,对于Vector IRQ,有自己的优先级,有自己的VICAddr(handler地址),哪个IRQ号对应哪个Vector IRQ,完全是动态的,取决于程序员的分配;
然后,除去Vector IRQ,还有FIQ,也是从IRQ中抽出来的,它有自己独特的regs bank,响应中断更快,由于这个没有应用到,这里不多讨论,不过机制还是比较简单的;
1.How to raise a int:
外设产生中断,或者程序里边可以主动写VICSoftInt的相应位,两种效果都一样;
2.中断之后怎么跳转到IntHandler:
(注意这是Int控制器的行为)如果对应的IRQ Num已经被declare作为VectorInt中的一个,那么对应的VICVectAddrN寄存器值被写入VICVectAddr寄存器;
(这是ARM体系结构的行为)切换处理器模式,跳转到0x00000018;
0x00000018这个地方放着指令,ldr pc, [pc, #-4080],读取VICVectAddr中存放的中断处理程序的入口地址,跳转;
在原来的SkyEye对LPC系列的模拟中,没有模拟PrimeCell的行为,但是处理器模式switch和跳转动作是有的,一个折中的办法是在0x18这个地方放跳转到IRQHandler的指令,然后在IRQHandler里边读VICIRQStauts或者VICRawIntr的状态,获得中断号,即改成一个通用的实现。
3.进入IRQHandler之后,要清除中断状态,否则后边的中断进不来
办法是对VicVectAddr写,一般写0,这时VICRawIntr, VICFIQStatus, VICIRQStatus对应位被清除。
时钟和串口相关:
这两个外设都是运行ucosii的基本条件,但是它们沿袭LPC系列的特征,所以不用做太大改动。我们只需要把它们的中断源挂到中断控制器对应的Slot就可以了。
二、 让ARMCC编译的程序在SkyEye上运行起来
编译器和调试器的问题:
ARMCC是ARM公司放出的官方编译器,可以输出bin,也可以输出.axf或者.elf格式,但ARMCC对标准的elf格式作出了扩展,所以elf包含的调试信息不能被基于GDB的SkyEye正确识别,所以所有的常规调试手段都用不上了:包括断点,C语言的单步等等。
但是调试的办法还是有的,由于SkyEye支持log功能,所以我们可以获得指令执行的流程,以及每一条指令的时刻的CPU状态,配合arm-elf-objdump的反汇编代码,结合原来的C代码,还是能够做一些底层的调试。
从开发板上的ucosii到SkyEye上运行的ucosii:
这里的SkyEye,指配置到模拟LPC2210的SkyEye。
1) 对ucosii的改动
1.1 去掉一些外设初始化代码,比如内存控制模块,PLL等等。
In arch/target.c, line 293
#ifndef __SKYEYE
//wait for frequency is locked, needless by SkyEye. Rmked by linxz.
while((PLLSTAT & (1 << 10)) == 0);
#endif
line 323:
/* 设置串行口 */
#ifndef __SKYEYE
InitialiseUART0(115200); //for SkyEye, disable it. by Linxz
#endif
1.2 手动模拟内存映射。
In arch/target.c line 243:
#ifdef __SKYEYE
//add by linxz 05-3-30
//here remap 0x00000000 to 0x80000000 to access int vectors
//for skyeye without mem remap facility, we should copy it manually.
//vectors range 0x0 - 0x3c
while ( dest <= 0x3c)
{
*(int *)dest = *(int *)src; //word by word
dest += 4;
src += 4;
}
#endif //__SKYEYE
要注意复制的内存是Vectors的32Bytes加上额外的32Bytes一共是64Bytes。
1.3 初始化代码中打开Timer0的中断,自己编写Timer0_Handler(),注意清除中断的时机。
2) 对SkyEye的改动。
这部分工作主要集中在skyeye/sim/arm/skyeye_mach_lpc.c,和skyeye/sim/arm/lpc.h中。
2.1添加一些寄存器的定义到lpc_io结构中,虽然有的寄存器在SkyEye的模拟下是没有用的,但是至少可以避免运行时不时出来一堆write_io error的错。
补全后的结构如下:
typedef struct lpc_io {
ARMword syscon; /* System control */
ARMword sysflg; /* System status flags */
lpc_pll_t pll;
lpc_timer_t timer[2];
lpc_vic_t vic;
ARMword pinsel0; /*Pin Select Register*/
ARMword pinsel1;
ARMword pinsel2;
ARMword bcfg[4]; /*BCFG Extend Mem control*/
ARMword vpbdiv; /*VPB Divider control*/
int tc_prescale;
lpc_uart_t uart[2]; /* Receive data register */
ARMword mmcr; /*Memory mapping control register*/
ARMword vibdiv;
/*real time regs*/
ARMword sec;
ARMword min;
ARMword hour;
ARMword dom;
ARMword dow;
ARMword doy;
ARMword month;
ARMword year;
ARMword preint;
ARMword prefrac;
ARMword ccr;
/*mam accelerator module*/
ARMword mamcr;
ARMword mamtim;
} lpc_io_t;
这里不得不提到,原来的实现,寄存器的名字全部采用简单的写法,基本很难对照datasheet找到是哪个…于是重写了Vector Control regs的定义,如下:
typedef struct vic{
ARMword IRQStatus;
ARMword FIQStatus;
ARMword RawIntr;
ARMword IntSelect;
ARMword IntEnable;
ARMword IntEnClr;
ARMword SoftInt;
ARMword SoftIntClear;
ARMword Protection;
ARMword Vect_Addr;
ARMword DefVectAddr;
ARMword VectAddr[15];
ARMword VectCntl[15];
} lpc_vic_t;
当然,要在lpc_io_read/write_word()和lpc_io_reset里边加上对应的读写操作,毕竟时模拟嘛,也不能什么都不做。
lpc_io_reset把timer的pr(prescale)寄存器的初值改变,就能调整模拟时刻时钟中断来的快慢。为了保持和硬件一致,我还是设为0了。
2.2 lpc_io_do_cycle(),这里主要是模拟Timer0的动作
/*lpc io_do_cycle*/
void lpc_io_do_cycle(ARMul_State *state)
{
int t;
io.timer[0].pc++;
io.timer[1].pc++;
//add by linxz
//printf("SKYEYE:Timer0 PC:%d, TC:%d", io.timer[0].pc, io.timer[0].tc);
//printf(",MR0:%d,PR:%d,RISR:%d,IER:%d,ISLR:%d,ISR:%d\n", io.timer[0].mr0, io.timer[0].pr, io.vic.RawIntr, io.vic.IntEnable, io.vic.IntSelect, io.vic.IRQStatus);
if (!(io.vic.RawIntr & IRQ_TC0)) { //no timer0 int now
if (io.timer[0].pc >= io.timer[0].pr+1) {
// if (io.timer[0].pc >= 5000+1) {
io.timer[0].tc++;
io.timer[0].pc = 0;
if(io.timer[0].tc == io.timer[0].mr0){
// if(io.timer[0].tc == 20){
io.vic.RawIntr |= IRQ_TC0;
io.timer[0].tc = 0;
//add by linxz
//printf("\r\nI\r\n");
}
lpc_update_int(state);
}
}
if(io.timer[0].pc == 0){
if (!(io.vic.RawIntr & IRQ_UART0)) {
fd_set rfds;
struct timeval tv;
FD_ZERO(&rfds);
FD_SET(skyeye_config.uart.fd_in, &rfds);
tv.tv_sec = 0;
tv.tv_usec = 0;
if (select(skyeye_config.uart.fd_in+1, &rfds, NULL, NULL, &tv) == 1) {
unsigned char buf;
int n, i;
n = read(skyeye_config.uart.fd_in, &buf, sizeof(buf));
//printf("SKYEYE:get input is %c\n",buf);
if (n) {
io.uart[0].rbr = buf;
io.uart[0].lsr |= 0x1;
io.vic.RawIntr |= IRQ_UART0;
lpc_update_int(state);
}
}
}/* if (rcr > 0 && ...*/
}
}
2.3 lpc_io_update(),添加一些代码,模拟ARM Prime Cell在中断到来时的动作。主要是根据IRQStatus的中断源置位,找出分配的Slot号,从而找到对应的VectAddr,把它复制到VectAddr寄存器,供0x0000 00018这个位置的指令读取。
In Line 172:
//here only deals some important int:
//uart0 and timer0, other peripheral's int reqs are ignored.
//added and rmked by linxz
//UART0, Int src: 6
if(io.vic.IRQStatus &IRQ_UART0){
for ( i = 0; i<=15; i++ )
{
if ( ((io.vic.VectCntl & 0xf) == 6 ) && (io.vic.VectCntl & 0x20) )
break;
}
if ( ((io.vic.VectCntl & 0xf) == 6 ) && (io.vic.VectCntl & 0x20) )
io.vic.Vect_Addr = io.vic.VectAddr;
}
//TIMER0, Int src: 4
if(io.vic.IRQStatus &IRQ_TC0){
for ( i = 0; i<=15; i++ )
{
if ( ((io.vic.VectCntl & 0xf) == 4 ) && (io.vic.VectCntl & 0x20) )
break;
}
if ( ((io.vic.VectCntl & 0xf) == 4 ) && (io.vic.VectCntl & 0x20) )
{
io.vic.Vect_Addr = io.vic.VectAddr;
//printf("VicVect load vectaddr%d:%08x", i, io.vic.Vect_Addr);
}
}
2.4 清除中断,采用了对VectAddr写的机制,这个也一并模拟。
主要是找出优先级最高的中断位来,清除掉。
Lpc_io_write_word(), line 658:
case 0xfffff030: /* VAR */
//io.vic.Vect_Addr = data;
//rmk by linxz, write VAR with any data will clear current int states
//FIQ interrupt
//FIXME:clear all bits of FIQStatus?
if ( io.vic.FIQStatus )
{
io.vic.FIQStatus = 0;
break;
}
//find the current IRQ number: which has the highest priority.
mask = 1;
nHighestIRQ = 0xffff;
for ( i = 0; i<=15; i++ )
{
nIRQNum = io.vic.VectCntl & 0xf;
if ( (nIRQNum<<mask) & io.vic.IRQStatus )
{
if ( nIRQNum < nHighestIRQ )
nHighestIRQ = nIRQNum;
}
}
//If there's at least one IRQ now, clean status and raw
//status register.
if ( nHighestIRQ != 0xffff )
{
io.vic.IRQStatus &= ~( nHighestIRQ << mask );
io.vic.RawIntr &= ~( nHighestIRQ << mask);
}
break;
经历了这些工作之后,重编SkyEye,并且把armcc编译好的elf弄到SkyEye上面模拟,运行完全没有问题。只是不能调试。
三、 把整个工程改造成GCC版本。
首先把所有的源文件全部弄到linux下面,组织成下面几个目录。
Arch/
放置和体系结构有关的文件
IRQ.S Os_cpu_a.S Os_cpu_c.c STACK.S Startup.S target.c
Kernel/
放置操作系统内核实现
OS_CORE.c OS_MBOX.c OS_MUTEX.c OS_SEM.c OS_TIME.c
OS_FLAG.c OS_MEM.c OS_Q.c OS_TASK.c uCOS_II.c
Include/
放置Headers
IRQ.INC includes.h os_cfg.h queue.h uart0.h config.h lpc2294.h os_cpu.h target.h ucos_ii.h
App/
放置基于ucosii的应用
QUEUE.c UART0.c main.c
特别说明,QUEUE.c等等提供了队列的实现,在串口应用中会用到。
在GCC的规则里边,.C表示cpp source file,.c表示c source file,所以应该把不符合规则的文件名字做修改,否则按照cpp编译的模块,导出的符号会被编译器修饰,这样的符号如果被汇编代码引用,链接的时候会有一些麻烦。
同样,.s表示不被预处理的汇编代码,而.S表示被C的预处理程序处理的汇编代码,这样的汇编代码里可以用的符号有#define, //,/**/等等,能够提供一些方便。
第二步,按照arm-elf-as的语法重写汇编部分的代码。
还好,各种assembler支持的汇编语法差距并不大,只需要做一些不大的改动就可以了。
声明段属性:
.text .data .bss等等
默认的名字会有默认的属性,比如.text应该是“rx”等等
数据对齐:
.align 2/4
声明标号:
symbol:
xxx
xxx
定义数据:
连续空间 .space N 以byte为单位
常量 .word 0x0000000
条件汇编
.ifdef
.else
.endif
关于汇编代码引用C代码的符号:
不用声明,对于全局变量或者函数名字直接用,链接时刻自然会解析好;
导出汇编代码里边的符号供其他文件引用:
.global Symbol
宏的定义
.macro MyMacro Para1, Para2,…
(引用参数,用/Para1这样)
.endm
StackSvc是Software Interrupt用到的堆栈;
StackIrq是IRQ处理时用到的堆栈;
原来的代码里边关于UsrStackSpace有些问题,我觉得它声明得过小,堆栈总是越界,我把它的尺寸加大了。
第三步,修改C代码适应GCC
GCC的C语法支持内嵌汇编,用法举例:
__asm__(“LDR r1, r2/nLDRr2,r3”)
需要注意的是不能在内嵌汇编里边用宏,因为它是一个字符串常量,如果在内嵌汇编里边用了宏,传递给AS的无法解析的符号全部被默认为0,而没有任何错误提示。
Armcc里边支持__swi关键字,用于声明一个不实现的函数,调用这个函数将引起软中断。GCC不支持这个,但是可以用宏实现,如:
include/os_cpu.h:#define OS_TASK_SW() __asm__("swi 0x00")
include/os_cpu.h:#define _OSStartHighRdy() __asm__("swi 0x01")
还有就是include后边的文件名是大小写敏感的,这和很多windows下面的c编译器不一样。
最后一个问题是关于main()的问题,写习惯了应用的程序员往往容易随手把内核入口直接声明成main(),可是编译器会在main()自动包上一层运行时库的实现,比如__main()什么的,而且这个行为因GCC的版本而异,所以相当麻烦。
往往因为这个问题引起链接的失败,比如报告找不到初始化的构造函数列表等等。
所以最好把内核的入口不用main命名,用mymain,startkernel等等,什么都可以。
第四步,写Makefile和ld script
Makefile分为三类:
1. 各个目录下面的子Makefile,它们的主要作用是生成一个当前目录下面所有的.c和.S列表,由字符的替换得到对应的.o文件列表,然后定义”all”目标依赖于这些.o文件。在子Makefile的最后,都包含了工程主目录下面的rules.make;
2. Rules.make,定义了生成.o文件的默认规则,就是写出了编译的具体命令行,包括各个编译开关等等;
3. 工程主目录下面的Makefile,进入各个子目录,驱动各个子Makefile,编译各个目录下面的源代码文件,然后生成一个所有的.o文件的名字列表,进行链接工作。
关于Makefile的资料很多,不再详细描述,只是有几个比较有用的函数,提一下:
wildcard 替换式的展开;
patsubst 替换字符;
foreach 循环枚举。
注意函数的参数之间,参数和括号之间都不能有空格,这让写惯了C代码的程序员不太习惯。
Cc用到的几个开关:
-Idir +include dir before default search path
-Wall Turn on all warning options
-Wno-trigraphs ...........(?)
-mapcs-32 Generate the code for CPU with 32-bit program counter.
-mtune This option is similiar to -mcpu, specified the actual target cpu type.
ps:-mcpu也提供-arm7tdmi选项的,我觉得无所谓
-mshort-load-byte: alias for -malignment-traps 后者是什么意思?
Ld用到的几个开关:
simple example:ld -o <output> /lib/crt0.o -lc
链接到crt0.o和libc.a ( come from the standard search path )
-T script file: Use script file as the linker script. This script replaces the
linker's default script file.
就是说指定.lds....
god,还得研究lds的写法
-X discard all local symbols(?)
--start-group archives 一系列.a文件
-lgcc -lc 这里的含义是加入libgcc.a libc.a
接下来的工作是写ld script,也就是链接描述文件,在该文件里边,我们将指定各个段如何放置。本例的lds很简明,几乎最简单的lds了:
OUTPUT_ARCH(arm)
ENTRY(reset)
SECTIONS
{
. = 0x80000000;
.text :
{
arch/Startup.o(*.text)
*(.text)
}
. = 0x81000000;
.data : {*(.data)}
.rodata : {*(.rodata)}
.bss : {*(.bss)}
}
这里的.text .data .bss是搜集各个.o的.text .data .bss组装起来
注意等号前面后边的空格,冒号前后的空格,都是必须的,否则过不了
关于ENTRY的问题,我的认识是,这里指定的ENTRY是仅仅是elf-header里的,SkyEye加载的时候,会根据Skyeye.conf里边内存块的配置选择入口点。
既然我配置的Skyeye执行入口是0x80000000,那么我必须得把Startup.o强制配置在整个.text段的最前面,即从0x80000000开始。
如不这样写,各个.o装配的顺序好像是和在传递给ld的参数中顺序一致的,比较难以控制。
另外一个问题是容易忽视定位.rodata段,当然ld是不会报告任何错误的,运行的时候会发现所有需要初始化的变量、数组全部未能获得正确的值。原因就是因为lds没有定位.rodata段。
四、 关于ucosii本身的一些要点。
关于移植的问题,Labrosse已经在书里边论述得很清楚了,在LPC2210这里要注意的就是处理器状态的合理分配问题;
高优先级的任务如果不是由于中断,优先级被降低,或者是主动OSTimeDly(),是不会主动进行任务切换的;
通过swi实现任务切换的动作,主要流程如下:
call swi->fetch software int->enter svc mode->jump to software interrupt
->analyse swi num->0 or 1, process(ctx switch or highrdy)->other pass to
swi exception handler-->dispatch swi num...-->ret
注意os_cfg.h里边的配置,任务数什么的,创建Task的时候,优先级分配要恰当,最高的几个和最低的几个都是系统保留的;
五、 调试手段
前后弄了这么久,做过斗争的错误从少写了一行反汇编代码到GDB自己的bug(GDB 5.3未能正确定位我的mymain符号,差一个偏移量),积累了一些基本方法。
Elf格式GDB不支持的时候,利用SkyEye的log file的指令执行流程,加上全文查找,反汇编,源代码找错误,或者往UART的寄存器里边写字符来调试,一次一个,无论是C还是汇编都可以。
SkyEye能断点或者单步,事情就轻松多了,善于利用b, si, ni, x等等命令,可以很快定位错误,何况还有条件断点等等高级功能。
在跳转到ucosii的入口之前,由于流程都是顺序的,所以应该是读、写寄存器的问题,或者有一些在硬件板子上能过的特殊写法,比如死循环等待什么东西等等。
跳转到ucosii入口之后,能够执行第一个任务一次,一切换就死掉,要跟踪看看切换的函数OS_TASK_SW等等,由于任务切换涉及中断和处理器状态的切换,在C和汇编之间来回跳转,比较复杂,所以也是最后一个难题。跟踪的时候时刻注意SP指向的空间到底是什么地方,如果都指向了代码空间,或者在Usr Mode里边一直指着SvcStack等等,要考虑是不是状态没有正确地被切换,或者堆栈的声明,类型、大小设置是不是出了问题。其次是看看Timer的中断是不是一直在来,OSTimeTick()正确被调用了。
如果能够反复执行第一个任务,说明没有任务切换,通过OSTimeDly,低优先级的任务却并没有进入就绪态。这时可以看看ucosii的一些全局变量,比如OsSwCtr,OSRdyGrp,OSRdyTbl的状态,到底是否随着OSTimeTick的调用,任务就绪表被正确改写了。就绪表初始化是否正确也是关注的焦点。
分析到这一步,已经到内核了,以后出问题的可能比较小了。
Appendix: skyeye.conf for lpc2210
cpu: arm7tdmi
#--------------------------------------------------------------------------------
# below is the machine(develop board) config info
# machine(develop board) maybe at91 or ep7312
mach: lpc2210
#-------------------------------------------------------------------------------
mem_bank: map=M, type=RW, addr=0x00000000, size=0x00000040
mem_bank: map=M, type=RW, addr=0x40000000, size=0x00200000
mem_bank: map=M, type=RW, addr=0x80000000, size=0x00200000, boot=yes
mem_bank: map=M, type=RW, addr=0x81000000, size=0x00080000
mem_bank: map=I, type=RW, addr=0xe0000000, size=0x20000000 |
|