摘要:本文的主要内容是关于PHILIP公司ARM7系列LPC2214芯片的调试报告,以及在其基础上的网络通信程序设计的调试报告和KEIL公司的开源RTL操作系统下TCPNET不开源协议栈的工作报告;在具体论述我的调试过程的时候,我会把我查询的一些我认为正确的资料附在合适的位置,以供参考。
转载请注明出处
LPC2214启动代码部分
启动代码-汇编代码--做c语言的准备工作。包括向量表定义,堆栈初始化,系统变量初始化,中断系统初始化,i/o初始化,外围初始化,地址重映射等操作。
在开始之前我建议首先把ARM的指令系统熟悉一遍,然后结合启动代码熟悉指令的含义和具体操作,尤其需要的是要比较明确的知道startup.s文件中的伪指令的含义,这将给你以后堆栈的大小等带来一个概念;
基本结构
下面是关于启动文件一个说明,模仿于UV3软件提供PHILIP 的LPC2000系列芯片的startup文件;在说明之前你有必要先熟悉ARM指令集,注意他的模块化的结构;VPBDIV、PLL、
MAM、EMC、BCG0-3等的设置比较模式化,熟悉下datasheet说明后,比较容易设置好,
要留意的就是PINSEL0-2的设置,根据自己的外部接口配置PINSEL。
LPC2114启动代码的编写主要包括:
1.异常向量表的建立
2.MCU各种模式堆栈的初始化
3.系统基本的初始化工作
下面分别进行介绍.
(一)ARM相关指令及伪指令
LDR PC,ResetAddr
将ResetAddr标号地址所指的内容传送给PC寄存器
LDR PC,=ResetAddr
将ResetAddr标号地址传送给PC寄存器
ResetAddr DCD ResetInit
为ResetAddr分配一个字的地址空间,以ResetInit初始化,即ResetAddr地址所指的内容为ResetInit标号地址
SvcStackSpace Space SVC_STACK_LENGTH*4
为SvcStackSpace分配一块SVC_STACK_LENGTH*4大小的地址区域,并以0初始化区域内容
(二)异常向量表的建立
异常是有内部或外部源产生的,以引起处理器处理的一个事件,异常出现后,CPU强制从异常类型对应的固定存储地址开始执行程序,如当IRQ中断产生后,CPU强制跳转到0x00000018出执行代码,我们要做的就是在这个代码地址出编写相应的指令,让它顺利执行IRQ中断程序.通常我们会在这里放置一条转移指令,因为0x00000018只给你一个字的编程空间.
程序如下:
AREA vectors,CODE,READONLY
ENTRY
;interrupt vectors
;中断向量表
Reset
LDR PC, ResetAddr
LDR PC, UndefinedAddr
LDR PC, SWI_Addr
LDR PC, PrefetchAddr
LDR PC, DataAbortAddr
DCD 0xb9205f80
LDR PC, [PC, #-0xff0]
LDR PC, FIQ_Addr
ResetAddr DCD ResetInit
UndefinedAddr DCD Undefined
SWI_Addr DCD SoftwareInterrupt
PrefetchAddr DCD PrefetchAbort
DataAbortAddr DCD DataAbort
Nouse DCD 0
IRQ_Addr DCD 0
FIQ_Addr DCD FIQ_Handler
;未定义指令
Undefined
B Undefined
;软中断
SoftwareInterrupt
B SoftwareInterrupt
;取指令中止
PrefetchAbort
B PrefetchAbort
;取数据中止
DataAbort
B DataAbort
;快速中断
FIQ_Handler
STMFD SP!, {R0-R3, LR}
BL FIQ_Exception
LDMFD SP!, {R0-R3, LR}
SUBS PC, LR, #4
IRQ_Addr DCD 0
FIQ_Addr DCD FIQ_Handler
**
**
ResetInit
BL InitStack ;初始化堆栈 Initialize the stack
BL TargetResetInit ;目标板基本初始化 Initialize the target board
;跳转到c语言入口 Jump to the entry point of C program
B __main
我们可以看到,每种异常都有相应的处理程序,如:当系统复位后,程序跳转到0x00000000处执行指令,那么就执行 LDR PC, ResetAddr,及执行ResetInit地址处的代码,这里放置了BL InitStack指令,负责完成各种模式下堆栈的初始化,接着执行BL TargetResetInit ,完成目标板基本初始化,最后进入c语言入口,执行main函数.
(三)MCU各种模式堆栈的初始化
由于各种异常模式下都有自身的SP堆栈指针,因此就必须先进入各自的异常模式进行SP的设置,各种模式的切换可以通过改变CPSR来实现,程序如下:
InitStack
MOV R0, LR
;Build the SVC stack
;设置管理模式堆栈
MSR CPSR_c, #0xd3
LDR SP, StackSvc
;Build the IRQ stack
;设置中断模式堆栈
MSR CPSR_c, #0xd2
LDR SP, StackIrq
;Build the FIQ stack
;设置快速中断模式堆栈
MSR CPSR_c, #0xd1
LDR SP, StackFiq
;Build the DATAABORT stack
;设置中止模式堆栈
MSR CPSR_c, #0xd7
LDR SP, StackAbt
;Build the UDF stack
;设置未定义模式堆栈
MSR CPSR_c, #0xdb
LDR SP, StackUnd
;Build the SYS stack
;设置系统模式堆栈
MSR CPSR_c, #0x5f
LDR SP, =StackUsr
MOV PC, R0
**
**
StackSvc DCD SvcStackSpace + (SVC_STACK_LEGTH - 1)* 4
StackIrq DCD IrqStackSpace + (IRQ_STACK_LEGTH - 1)* 4
StackFiq DCD FiqStackSpace + (FIQ_STACK_LEGTH - 1)* 4
StackAbt DCD AbtStackSpace + (ABT_STACK_LEGTH - 1)* 4
StackUnd DCD UndtStackSpace + (UND_STACK_LEGTH - 1)* 4
**
**
;/* 分配堆栈空间 */
AREA MyStacks, DATA, NOINIT, ALIGN=2
SvcStackSpace SPACE SVC_STACK_LEGTH * 4 ;Stack spaces for Administration Mode 管理模式堆栈空间
IrqStackSpace SPACE IRQ_STACK_LEGTH * 4 ;Stack spaces for Interrupt ReQuest Mode 中断模式堆栈空间
FiqStackSpace SPACE FIQ_STACK_LEGTH * 4 ;Stack spaces for Fast
Interrupt reQuest Mode 快速中断模式堆栈空间
AbtStackSpace SPACE ABT_STACK_LEGTH * 4 ;Stack spaces for Suspend Mode 中止义模式堆栈空间
UndtStackSpace SPACE UND_STACK_LEGTH * 4 ;Stack spaces for Undefined Mode 未定义模式堆栈
****
***
AREA Stacks, DATA, NOINIT
StackUsr
说明:MSR CPSR_c, #XX指令完成各种异常模式的切换,如切换到IRQ模式,则执行MSR CPSR_c, #0xd2,而SP的值由SvcStackSpace的地址加上SVC_STACK_LEGTH 的所指的大小而定.
(四)系统基本的初始化工作
完成堆栈初始化后执行BL TargetResetInit,它负责系统一些基本的初始化,如地址重映射,PLL时钟初始化,VIC中断初始化等,具体代码见Easy Arm系列开发版的target.c文件.
最后,执行B __main,__main是ADS的一个系统函数,它负责一些初始化环境然后执行用户的main函数.
REMAP概念
典型的Boot、Memory Map和Remap的时间顺序应该是:Memory Map-〉Boot-〉Remap。 但是,LPC2000处理器中这三个动作的顺序却有一点不同,依次为Memory Map-〉Remap-〉Boot-〉Remap,最后一个Remap过程是用户可选的,可执行也可不执行。每当系统复位以后,LPC2000处理器就顺次执行上述四个过程,下面分析这几个阶段。为简化起见,以总线不开放的LPC2104处理器为例。
LPC2106的片上存储器分类
LPC2104片内的存储器类型只有两种:Flash块和SRAM块。其中,部分Flash存储器块在芯片出厂前由Philips写入了Bootload程序和64字节的异常向量表。为方便讨论,我们称这部分Flash块为Bootload子块,其大小为8KB。如前所述,在处理器未上电之前或复位时,Flash块和SRAM块仅仅是两个没有地址编码的物理存储器,与地址编码尚未建立起实际的映射关系。
Memory Map
LPC2104处理器(上电)复位以后,Flash块和SRAM块的地址映射结果为:SRAM占据0x40000000—0x40003FFF范围的地址编码空间;Flash占据0x00000000—0x0001FFFF范围的地址编码空间。该映射结果是个中间态,只存在极短的时间,应用系统开发人员无法看到这个中间态。处理器内核外围模块的地址映射结果为0xE0000000—0xFFFFFFFF。
Memory Map完成以后,紧接着LPC2104会作一次Remap,这次Remap操作的对象是Bootload子块,由处理的内部硬件逻辑执行完成,不受开发人员的控制。经过Remap后,Bootload子块被整体Remap到了0x7FFFE000—0x7FFFFFFF的片内高地址内存空间;同时,原Memory Map后占用0x00000000—0x0000003F地址空间的那部分64 字节大小的Flash子块被暂时注销映射关系,由Bootload子块中的异常向量部分取而代之。
至此,Flash块对内存地址空间的占用情况如下:
1、除去因Remap被暂时注销了映射关系的那小部分64字节的Flash子块外,Flash块作为一个整体占用的地址编码空间为0x00000040—0x0001FFFF;
2、同时,Bootload子块又占用了0x7FFFE000—0x7FFFFFF的地址编码空间,Bootload子块中的异常向量表部分占用了0x00000000—0x0000003F。
因此,Bootload子块中的异常向量表部分实际上是占用了重复占用了三段地址编码空间:0x00000000—0x0000003F、0x0001E000—0x0001E03F以及0x7FFFE000—0x7FFFE03F。
下图中,存储器的映射顺序为:Memory Map-〉Reset Remap-〉Bootload Remap。
SRAM块和内核外围模块的映射关系在Remap之后保持不变。
LPC2104有效的异常向量表地址编码空间是0x00000000—0x0000003F(严格来说应该是0x00000000—0x0000001F)。处理器复位后的Boot动作就是从0x00000000处起始字中取出跳转指令,开始程序的执行。由于处理器复位后,映射到0x00000000—0x0000003F地址空间的异常向量表源于Bootload子块,因此CPU实际上开始执行的是Philips在芯片出厂前写入的Bootload程序。
进入Bootload后,程序首先检查看门狗溢出标志是否置位。
若看门狗溢出标志置位,则表明当前的系统复位是内部软复位,CPU下一步将对Flash块中的异常向量表进行加和校验。如果加和检验结果为零,Bootload程序将撤销Bootload子块中异常向量表部分在0x00000000—0x00000003F地址空间上的映射,恢复Flash块的异常向量表在这64字节地址空间上的映射关系(如图3),然后跳转到异常向量表地址0x00000000处转入用户程序的执行。如果加和校验结果不为零,Bootload程序将进行UART0接口的波特率自动侦测,随时响应ISP宿主机的编程请求,执行处理器芯片的ISP编程工作。
若Bootload没有发现看门狗溢出标志置位,则表明当前的系统复位是外部硬复位,CPU将采样P0.14引脚的外部逻辑电平输入。如果为0,Bootload执行UART0的自动波特率侦测,随时响应ISP宿主机的编程请求;如果为1,Bootload的后续动作将与前面检测到看门狗溢出标志置位的程序执行完全相同。
总之,startup启动代码部分是跟硬件相关的汇编语言部分,应该很好上手,但是要熟练操作,我认为非几天或者一个月就能办到的,如果不是要一直写汇编,只是用于配置CPU并进入C语言,建议不要在此纠缠过多;
RTL OS部分
RTL OS是开源的,所以之前我建议你看一下他的整个的文件结构,熟悉一下他所定义的数据类型以及他所提供的内存操作函数;这些在RTL.chm文件中都有详细的说明,虽然是英文的,但是确实很准确。
我在使用多任务的时候,使用过event flag management(os_evt_...) 、semaphore management (os_sem_...)的相关函数和时间管理函数(os_dly_... 、os_itv_...)进行任务切换;前面两个比较方便,而且KEIL UV3提供了很好的例子;在使用os_itv_set、os_itv_wait的时候出现了问题;主要是os_itv_set设置延迟时间后,os_itv_wait执行等待(把本任务送入WAIT_ITV状态), 下面是从KEIL公司主页FORUM论坛下载的说明:
ARTX os_itv_wait breaks if execution takes longer than os_itv_set
Richard Nigro
If I setup a interval wait timer for 100 clock ticks ,os_itv_set(100), and the execution from the os_itv_set(100) to the os_itv_wait() takes longer than 100 clock ticks, the os_itv_wait() waits for 65535 clock ticks.
#include <ARTX.h>
void task1 (void) __task {
.
.
os_itv_set (100);
for (;;) {
.
.
/* execution may take longer than 100 clock ticks */
os_itv_wait (); /* if 100 clock ticks have passed */
/* this will wait for 65535 clock */
/* ticks. */
}
}
基本程序结构:
void task_init (void) __task
{
os_tsk_prio_self(); 在这里设置task_init任务的优先级,保证Delete task_init 能够执行;
Create other task here; 每个任务的堆栈都可由用户自己定义,如果任务需要的堆栈空间比较大,就不使用os默认的堆栈大小,自己定义;
Delete task_init here;
}
int main (void)
{
Do initial work here;
os_sys_init (task_init);
while (1);
}
TcpNet的问题
TcpNet可以独立于操作系统工作,所以我之前的工作是没有os下调试TcpNet和PC之间的通讯,我所知道的是ping很稳定,说明协议在 链路层(ARP协议) + IP层(ip协议) 是正常工作的,所以开始了tcp协议的编程工作,主要是使用TcpNet提供的一些基本API函数;
/**************************************send_data modole*******************************/
/*******function : tcp_callback() send_data()***********/
U16 tcp_callback (U8 soc, U8 event, U8 *ptr, U16 par) {
/* This function is called on TCP event */
switch (event) {
case TCP_EVT_CONREQ:
/* Remote host is trying to connect to our TCP socket. */
/* 'ptr' points to Remote IP, 'par' holds the remote port. */
/* Return 1 to accept connection, or 0 to reject connection */
return (1);
case TCP_EVT_ABORT:
/* Connection was aborted */
break;
case TCP_EVT_CONNECT:
/* Socket is connected to remote peer. */
soc_state = 2;
break;
case TCP_EVT_CLOSE:
/* Connection has been closed */
break;
case TCP_EVT_ACK:
/* Our sent data has been acknowledged by remote peer */
bAck = __TRUE;
databuf = 0;
soc_state = 3;
break;
case TCP_EVT_DATA:
/* TCP data frame has been received, 'ptr' points to data */
/* Data length is 'par' bytes */
memset(recvbuf, 0, 1024);
memcpy(recvbuf, ptr, par);
recvlen = par;
soc_state = 4;
break;
}
return (0);
}
/************************************************************/
void send_data (void) {
U32 j;
U8 ret;
switch (soc_state) {
case 0:
tcp_soc = tcp_get_socket (TCP_TYPE_CLIENT, 0, 120, tcp_callback);
tcp_connect (tcp_soc, rem_IP, 5001,0);
soc_state = 1;
sendlen = bufsize;
return;
case 1:
return;
case 2:
if (bSend == __TRUE) {
return;
}
maxbufsize = tcp_max_dsize (tcp_soc);
if (databuf == 0) {
databuf = tcp_get_buf(maxbufsize);
}
memset(databuf, 0, maxbufsize);
memcpy(databuf,sendbuf, sendlen);
ret = tcp_send (tcp_soc, databuf, sendlen);
if(ret == __FALSE) {
soc_state = 5;//while(1); //send failed
return;
}
bSend = __TRUE;
return;
case 3:
soc_state = 5;
return;
case 4:
memset(sendbuf, 0, 1024);
memcpy(sendbuf,recvbuf,recvlen);
sendlen = recvlen;
for(j=0; j<recvlen; j++) {
sendbuf[j] = (sendbuf[j] + 1) ;
if( sendbuf[j] > 'z') sendbuf[j] = 'a';
}
soc_state = 5;
bRecv = __TRUE;
return;
case 5:
if(tcp_check_send(tcp_soc)) {
bSend = __FALSE;
soc_state = 2;
}
return;
case 6:
tcp_close (tcp_soc);
if( tcp_get_state( tcp_soc ) == TCP_STATE_CLOSED ) {
soc_state = 0;
}
bSend = __FALSE;
bRecv = __FALSE;
bAck = __FALSE;
bData = __FALSE;
return;
}
}
/**********************************end of the modole***************************************/
采用状态机的工作模式,根据这个socket的状态,逐步切换:
Case 0:向pc申请连接;
Case 1:等待连接成功;
Case 2:向pc发个数据包;
Case 3:等待pc发ack数据包;
Case 4:处理pc发过来的数据包;
Case 5:回到case 2;
当然程序有时候会死在case 1,所以设计了超时重新连接(即回到case 0);
死在case 2,一般会进入net_config.c文件的sys_error();问题尚未解决;
死在case 3, 未等到pc发过来的包,处理方式是直接进入5,把上次的包重发一次;
其实,这个状态机是在演示tcp协议的双方窗口大小为一个数据包的通信;即使正常运行
速度也不会很可观,因为窗口大小为一个包(tcp_send函数每次发送的字节数有限,
1500 bytes), 所以这时获得的速度很慢,只有不到一个1 Kbyte/s, 这与后来直接使用网卡驱动跟pc同信的数据很吻合,他窗口大小为1的时候,速度也就1.5 Kbyte/s,所以我开始理解,最能提高网速的是窗口的大小,当然这个需要很大的缓冲能力 和 处理数据包的FIFO结构(这些属于协议栈和计算机配置方面);另一方面就是网卡的速度(发包和收包放到相应的FIFO中的速度)和缓冲能力,如果只是使用网卡的缓冲能力是绝对不能获得一个大的窗口的;我假使pc网卡的缓冲能力为16 kbytes的FIFO大小,也就只能承受10个链路层的数据包,所以在此之上,pc协议栈应该还维护着一个足够长的数据包的FIFO。RTL中的协议栈没有这个FIFO结构,因此tcp_send每次发送的数据大小就是网卡的限制大小(只有一个包);所以不支持分包发送,协议栈的主要工作能力体现在哪里?
Winsock程序设计
主要是设计了pc端的客户端程序和服务器(不支持多用户),使用WIN 32 API函数提供的socket编程接口,比较简单,不值一提。
WINPCAP网卡驱动
与winsock类似,但是对象不在是很抽象的socket,而是数据包,编写收包和发包的程序也相对比较简单;这里可以自由的对包进行处理;
简易TCP协议
头文件格式(21 bytes):
0-5 :DesMAC
6-11 :SrcMAC
12 :TYPE(表明这个数据包是发送过来的(SEND_TYPE),还是应答发送的(ACK_TYPE), 或者是发送兼应答的(暂时不支持SEND_TYPE + ACK_TYPE的包));
13-14:发送序列号,表明本地向对方发送的数据包序列;
15-16:接收序列号,表明接收到对方的数据包序列;
17-18:数据包的长度,包括头文件和数据,以字节计算;
19-20:校验值,16bit校验整个数据包,如果数据包长度是奇数字节则不一个全0字节;
把19-20字节置0,对数据包字对齐进行加和,然后把该值放在19-20中;
根据这个双方统一的文件结构,进行tcp通信,完成多包发送程序和多包应答程序,同时完成了pc端的基于winpcap的多包发送程序和多包应答程序;每次收包要进行判断和校验,等待ack超时会进行重发;
当处理包数为10,测试通信速度为15 Kbyte/s;是1个包的10倍;最短稳定运行15小时;
20, 30 20 <1
30 45 30 <1
40 60 40 <1
50 75 50 <1
至于为什么在处理包数>10之后,不能稳定运行,认为是软件设计的问题,具体为什么,正在寻找,因为很难恰好捕捉到出问题的时刻;
KEIL ARM debug 报告
全局变量的初始化,好像在定义的时候初始化是无效的,我一般的方法是,先定义,然后进入main()之后初始化;
指针强制转换,从短到长转换,如果指针没有经过运算,直接转换(不论指针所指的该类型的长度是多少),都可以;如果指针运算了,所加字节个数不是长的类型的整数倍,则出现异常;
ini文件和load application at startup,如果两者都有的话,应该是ini执行在先;
scf是以area为单位进行加载的,具体就是把相应的obj文件中相应的area区域放到对应的逻辑地址上,当初的理解是开始所有的程序都在flash里面,这是加载模式,运行时会按照scf文件把相应的数据和代码,对应到ram里面;因为ram不上电就没信息,因此我估计每次上电,reset都会按照scf把flash的内容加载到相应的逻辑空间; 如果有多个region,比较麻烦,内外flash都有放程序的axf文件吧,具体我也没尝试过;
Linker Error: L6238E: foo.o(.text) contains invalid call from '~PRES8' function to 'REQ8' function foobar
REQUIRE8 和 PRESERVE8
REQUIRE8 命令指定当前文件要求栈的八字节对齐。
PRESERVE8 命令指定当前文件保持栈的八字节对齐。
语法
REQUIRE8
PRESERVE8
用法
只有当所访问的是八字节对齐地址时,LDRD 和 STRD 指令(双字传送)才能正确工作。
如果代码中包含与堆栈之间的 LDRD 或 STRD 传送,可使用REQUIRE8 来指示链接程序,以确保仅仅从保持八字节栈对齐的目标中调用该代码。如果代码保持栈的八字节对齐,可使用 PRESERVE8 来通知链接程序。链接程序确保要求栈的八字节对齐的任何代码,仅仅由保持栈的八字节对齐的代码直接或间接地调用。
error65: access violation at 0x.....的存储器读写错误提示, 我目前碰到有2种情况可能导致这个错误出现:
1. 没有为仿真环境指定读写的存储器映射(Map).
有时我们可能会用到外部Flash, SRAM, 另外, 指示灯, 键盘, 打印机等等外设都要分配地址. 如果没有为仿真环境指定Map, 仿真时就会出现错误提示. 可以在Debug - Memory Map对话框中指定上述设备的访问地址. 也可以专门写一个ini配置文件在仿真初始化时自动设置Map, 可以参考: http://www.keil/support/docs/814.htm
2. Keil CARM 编译器分配临时变量地址时超出堆栈大小.
各函数模块中的临时变量都在Heap(堆)中分配, 如果临时变量占用空间超过Startup.s中指定的堆栈空间, 就会出现上述错误. 可以在Startup.s的配置窗口中的stack configuration中重新分配堆栈空间。
更多推荐
ARM7系列LPC2214芯片的调试报告
发布评论