摘要:本文的主要內容是關於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.com/support/docs/814.htm
2. Keil CARM 編譯器分配臨時變量地址時超出堆棧大小.
各函數模塊中的臨時變量都在Heap(堆)中分配, 如果臨時變量佔用空間超過Startup.s中指定的堆棧空間, 就會出現上述錯誤. 可以在Startup.s的配置窗口中的stack configuration中重新分配堆棧空間。