請注意下列二行程式碼有啥不同?
沒錯,就是括弧位置不同,但是請看下列編譯出來的組合碼。
注意到那不同處的地方了吧,二者算出來的值卻相差十萬八千里,其實在指標使用時,這是一個很容易犯錯的地方。當你原本只是要取一個結構結尾的指標,很自然的會寫出 1 的程式碼,但那是錯誤的。因為指標加法是根據指標的變數型態去相加,比如一個 short 指標加一,是指向下一個 short 位置,也就是指數加二。而在此案例上,第一種寫法是指向第8個結構的位置,也就 8x8=64=0x40 的位置,實在是大錯特錯。正確寫法為第2,3個寫法。
最近又接了一份工作,是搞 IP210S 的案子,上頭想在 LCD 裏內建網路,可以讓使用者透過網路去控制 LCD 功能,還有能從網路更新軔體;好像是要仿造 NEC 的軟體功能。
拿到 IP210S 的 source code,看了一下,主要的函數全被封裝在 library 裏了,真不知為啥要簽 NDA,都被包起來了,有意義嗎?
今天主要是來記錄 code banking 的用法,IP210S 內含一顆 8052 的 mcu,ROM 是採用外部 flash,SST 39VF040 512KB。原本 IP210S 只有 16 條的 address line,也就是只能使用 64KB,剛好也是 8052 最大的定址位址。
那 512KB 要怎麼使用?當然就是切成八塊 64KB,使用 Keil-C 的 code banking 功能,硬體也很簡單,拉三條 GPIO 到 39VF040 的 A16~A18 即可。而 L51_BANK.A51 改成使用 mode 4,自己寫 bank switch code 即可。bank 0 就三個 GPIO 設成 0,bank 1 就 A16 那個設成 1,以此類推,夠簡單吧。
之前只使用過 winbond W79E632 的 code banking,而且還是 internal ROM,使用 mode 1,一直搞不清楚 mode 4 會用在那里。現在有了範例,就更清楚為啥要分那麼多種模式了。學 8051 還真是沒啥捷徑,真是要多看多學,才能累積經驗值。
到這裏應該算是一個總結了,後面有什麼功能,再慢慢來想吧。這次主要是加入了 rtos delay 函數,讓 delay 更為精準,也讓空出來的時間,給其它 task 去執行,並且加入了 rtos_idle 函數,利用 cpu idle 功能達到省電功能,當然如果你有用到 PWM 時,記得把這個功能換掉,免得 PWM 不動了。
主要加入的功能
- rtos_idle 函數主要是在所有 task 都在 wait 時要執行的函數。由於 rtos_idle 沒有用到任何的堆疊空間,所以在計算每一個 task 的堆疊空間時,把 idle task 的空間直接定義成 17 bytes,就造成 size = (256 - SP - 17) / MAX_TASKS;,而非 size = (256 - SP) / (MAX_TASKS + 1);。
- rtos_nexttask 函數主要是用來做切換任務的工作,並根據 rtos_tick 決定是否要執行。
- rtos_isr 函數中加入了計時功能(以 10ms 為一個單位),並把 nexttask 功能分離出來,並加上 RETI 把後面的 POP 擋掉。
- rtos_scheduling 函數主要是用在非中斷環境下執行 nexttask 功能。
- rtos_delay_10ms 函數主要是用來做時間延時用的,每 10ms 為一單位。
程式執行結果為 0.5 秒顯示數字,1 秒顯示字母,也就是看到 1a23b45c67d89e12f34g56h78i9 如此循環顯示的字串。
這次修改的內容,主要是加入 code banking 功能。
- 這次主要是參考 Keil C51 的 RTX-51 code banking 設定,利用 ?B_CURRENTBANK 及呼叫 ?B_RESTORE_BANK 來切換。
- L51_BANK.A51 除了主要的設定外,需要把 ?B_RTX 也設成 1,才能讓 ?B_RESTORE_BANK 函數可以使用。
- 在初始化過程中,也要先切換第一個 task 的 code bank。
在 tiny rtos for 8051 part2 裏,本來想把已經測試好的程式碼加入 code banking 的相容性,但經過幾天幾夜的測試,不管怎麼做都會出錯,真是莫名奇妙。後來就倒回去看看是否之前程式碼是否也有問題,結果真的是如此。part1 的程式碼執行無誤,part2 的程式碼執行有時會出錯。經過反覆檢查,發現把 interrupt 的程式碼全改成 assembly 就會出錯,即使照著 Keil C51 編出來的 assembly code 寫進去也是一樣有問題,只能猜想是否在 link 過程中,又被動了一些手腳,造成程式莫名奇妙的出錯吧。
這次主要修改的內容
- 原本 rtos_isr 函數維持加入部份 assembly code,不要全部改成 assembly code。
- ?C_IBP 儲存方式改成放入堆疊裏,以利精簡程式碼,減少 interrupt 執行時間。
- 原本的 void (* const task_func[MAX_TASKS])(void) 改成 unsigned int code task_func[MAX_TASKS],將函數指標改成一般變數放在 rom 裏,如此可以節省記憶體用量,也精簡程式碼。
- 為了避免 LCALL rtos_start 變成 LJMP rtos_start 問題,直接改變 SP 的初始化過程,藉由修改 STARTUP.A51 來增加 STACK 位址的讀取。
- 不可以將 STARTUP.A51 中的 IBPSTACK 設成 1,原來使用 reentrant 函數都需要將 IBPSTACK 設成 1,但是不知什麼原因,在用 IspWriter 燒錄完後的自動重啟執行都會出現怪怪的執行結果。
經過反覆測試,發現將 interrupt 全改寫成 assembly 會造成不明原因輸出錯誤,這個原始碼不要用喔,後面有解決方法。
原始碼下載在這次的實驗裏要解決 reentrant 函數的問題,其實也很簡單,就是把 reentrant 的 stack 變數依照每個 task 儲存起來,執行該 task 時,再回復其值就好了。程式碼如下,都是用 c 語法做說明,實際程式碼很多都改成 assemble 語法,主要是因為 Keil C51 的 reentrant stack 是存在 ?C_IBP,而 C 語法不允許變數有 ? 符號,所以就改成用 asseble 來寫。
接下來二個 test tasks 就全改成 reentrant,如此就不會有 data overlaying 的問題,而且 Code Optimization Level 也可以調到 7:Extended Index Access Optimizing。執行結果就會看到數字跟英文字母相互出現。
寫到這裏也許有人會覺得奇怪,為什麼 Code Optimization Level 沒有調到預設值 8:Reuse of Common Entry Code?主要是因為在 rtos_start 裏,我利用 LCALL 的特性把 stack 裏的返回位址改成 task1 的位址,而 level 8 會把 main 函數裏的 LCALL rtos_start 改成 LJMP rtos_start,造成程式無法執行。當然只要一個小修改就可以修正掉這個 bug 了。
最近有空去研究了一下陳明計的 small rtos for 8051,看了老半天,還是看不懂他在寫什麼;尤其是他在 stack 的處理方式,實在是有看沒有懂。所以自己就自己來寫了一套 tiny rtos for 8051。沒有 semaphore,也沒有 task priority,什麼都沒有,只有簡單的時間分配,也就是所有工作時間都一致,current task 工作固定時間後,再換 next task 工作固定時間,如此循環工作。
工作原理也很簡單,如果有 n 個 tasks 就把 stack 分成 n 等份,讓每個 task 保有自己的 stack,不要互相干擾就可以。再利用 timer interrupt 來切換 task。很簡單吧,就如下面程式碼所列,並不會很難。
寫了二個測試函數,主要就是從 rs232 印出 1~9 及 a~i,如下所示。
但是結果卻不如預期,跑出來的東西都是亂碼,傻眼。最後,經過了二天的努力,總算把結果正確無誤的弄出來了。主要有下列幾個地方要修改。
- 把 C51 的 Code Optimization Level 降成 1:Dead code elimination。因為 Data overlaying 的關係,造成 task1 的 i 變數跟 task2 的 j 變數共用同樣的位址,所以讓印出來的值都在 1~9 跳動。
- 原本 data overlaying 問題想要利用 reentrant 指令來建立可重入函數去解決,但最後發現仍然是無效。因為 keil 把變數都放在一個 ?C_IBP 的位址,仍然會造成共用的問題,這個問題真是難解啊,以後如果要寫複雜的 multi-task 函數有一定的難度,必須克服可重入函數及 data overlaying 造成的問題。
- 在 rtos_isr 函數裏加入 push 及 pop 指令,把 R0~R7,ACC,B,PSW,DPH,DPL 都丟到 stack 裏去。因為 keil 都會利用這些暫存器做一些運算,在進入 interrupt 時,也會主動 push 一些在 interrupt 會用到的暫存器到 stack 裏,但其它沒用到的暫存器就沒有主動 push,造成切換到其它 task 再切回原來 task 時,那些暫存器裏的值都被變更了,而造成運算錯誤。
- 另外在加入 assembly code 時,需把 Generate Assembler SRC File 及 Assemble SRC File 都打勾才可以,缺一不可。
一般的編譯器將函數中的區域變數動態配置在 stack,等函數結束空間就釋放出來。因為 8051 的內部記憶體很少,只有區區 128 或 256 bytes,而且 stack 也是共用這塊記憶體。為了節省 stack 空間,所以區域變數基本上是靜態配置在固定位址, 也就是變成全域變數。如此就又造成浪費記憶體的情況,為了解決這個問題,所以 8051 的編譯器基本上都採用所謂的 data overlaying 技術來克服區域變數浪費空間的問題。
所謂 data overlaying 是指沒有呼叫關係的函數,它們的區域變數區可以重疊在一起(共用一塊記憶體)。Keil C51 會分析程式中函數間呼叫的關係,產生一個呼叫樹。它就根據這個呼叫樹來決定那些函數的區域變數區可以 overlaying 在一起。一種情況是是編譯器發現某一個函數(不是 main)沒有被別的函數呼叫,這會造成編譯器的困惑。一個正常的程式,除了 main 之外,除非是垃圾程式碼(沒用處但沒有刪除),否則所有的函數應該是至少會被一個其它函數呼叫的。編譯器在安全至上的原則下,會認定它的分析無法正確的辨識這個函數呼叫關係,所以對這個函數的區域變數就會獨立配置,不會重疊配置。這樣有沒有問題?邏輯上當然不會有問題,但沒 overlaying 就是會浪費記憶體,而且也會一直產生 *** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS 的警告。
什麼情況下,函數的呼叫樹讓編譯器無法分析?真實應用是有一些情況會發生這樣的問題。最常見的就是使用函數指標來呼叫函數。因為呼叫是執行時期動態變動的,這就可以難倒編譯器了。在這種情況這些被呼叫的指標函數就會獨立配置它們的區域奱數。如果你要這些函數也能正確的使用 overlaying 的好處,那麼你就必需手動分析那些函數的呼叫樹,然後告訴編譯器就可以了。這樣你也就不會在編譯時產 UNCALLED SEGMENT 的警告了。
12.0 MHz 延時 1ms 函數
22.1184 MHz 延時 1ms 函數
原文出處:http://www.dztang.com/article/dpj/jcycx/117.html
经过电子堂网站的实测,精确到us啊,足够了吧。
| type 變數型態 | located 指向位址 | pointer | physical 變數位址 | name |
| unsigned char | xdata | * | data | p |
指標分類
- unsigned xdata char * data p;
指向外部記憶體的指標變數,變數存放在內部記憶體。
- unsigned char data * xdata p;
指向內部記憶體的指標變數,變數存放在外部記憶體。
- unsigned char code * xdata p;
指向程式記憶體的指標變數,變數存放在外部記憶體。 - unsigned char code * code p;
指向程式記憶體的指標變數,變數存放在程式記憶體,指標變數是唯讀,所以必須先設定初始值。 - unsigned char * xdata p;
指向任何記憶體的指標變數 (generic pointer),變數存放在外部記憶體。 - unsigned char * p;
指向任何記憶體的指標變數 (generic pointer),變數存放由 memory model 決定。 - char (* data fp)(void);
指向程式記憶體的函數指標變數,變數存放在內部記憶體。函數指標變數只能指向程式記憶體,無法修改喔。 - 由於內部記憶體指標讀取都是使用間接定址 (indirect addressing),所以無法存取 SFR 空間的記憶體內容。
指標變數大小
| data | 1 |
| xdata | 2 |
| code | 2 |
| generic pointer | 3 |
- generic pointer 是設計用來可以做通用指標變數使用,可以任意變換指標指向位址,但是相對的需要佔用 3 個 bytes 的位置,而且在做讀取寫入時,需要另外執行一個函數來轉換位址,沒事就少用一點吧。
- copy C:\Keil\C51\LIB\L51_BANK.A51 到專案目錄下。
- 修改 L51_BANK.A51 中的設定。
?B_NBANKS EQU 4 改成 ?B_NBANKS EQU 2。 - 在 mcu_init 中加入 ROMCON = 0x0A;,藉此設定 P1.2 as A16 並開啟 code banking 功能。
- 將 L51_BANK.A51 加入 project source file 裏。
- 進入 Project --> Manage --> Components, Environment and Books。
- 在 Groups 裏新增 bank0 及 bank1,完成後會在 Project Workspace 會出現 bank0 及 bank1 二個目錄。
- 在 bank0 按右鍵,選擇 Options for Group 'bank0',在 Code Bank 處選擇 "Bank #0"。
- 在 bank1 按右鍵,選擇 Options for Group 'bank1',在 Code Bank 處選擇 "Bank #1"。
- 在 bank0 及 bank1 各新增一個 souce file。
- 完成編譯後,原本會產生一個 xxx.HEX,現在則產生 xxx.H00 及 xxx.H01 二個檔案。
- 需要先初始化 uart 功能,TI 需設成 1,printf 輸出才有資料。
- 不需要像 SDCC 需要另外寫 putchar 及 getchar 函數,已經內建寫好了。
- 使用時要 inlcude stdio.h 標頭檔案,才不會找不到 prototype。
- printf 要輸出 8-bits 變數時,需寫成 %bd,不可以寫成 %d,會造成輸出錯誤,原因不明。
- getchar 函數在讀取到一個字元後,會自動輸出原字元做回應。(好像理論上不需要如此做,是為了仿造 PC 上有回應字元嗎?)
參考範例,使用 W79E632A,@40.0MHz,19200 bps。
當有三個函數為 A2() 位於 common 區,A0() 位於 bank0,A1() 位於 bank1,而且三個函數同時寫在 AA.c 裏,只是利用 CODE(?PR?A2?AA) bank0(?PR?A0?RAM) bank1(?PR?A1?RAM) 來分開,並且在 A2() 函數裏會呼叫 A1() 及 A0() 函數時,必須把 AA.c 放在 common 區裏,不可以放在 bank0 or bank1 這樣會造成 compiler 誤判,沒有呼叫 bank_switch 來切換 A1()/A2(),而造成不可預期的錯誤。
雜記
- 在 bank0 的函數可以互相呼叫 bank1 函數,沒有限制喔。
- 將 keil C51 內建 printf 移到 bank 裏時,最好避免一起把 code constants 一起移到 bank 裏去,不然就會發生最下面紅字警告的問題。
Common Area
common code area 主要是給所有的 bank 使用的函數或是固定資料。下面是一些會放在 common area 的函數及資料。- Reset and Interrupt Vectors 重置及中斷指標
主要是給硬體中斷時直接存取使用,所以必須放在 common area。 - Interrupt Functions 中斷函數
主要是給硬體中斷時直接存取使用,所以必須放在 common area。 - Bank Switch Code 切換區塊函數
這樣一定要放 common area,不然怎麼做切換啊。 - Library Functions 程式庫函數
因為無法預知那個區塊會用到,所以都放在 common area。 - Code Constants 程式區資料變數
這個不知為啥都放在 common area,可以經由參數設定放置在 bank 裏。
Code Constants 移至 bank 裏
code constants 主要就是經由宣告成 code 的變數,如字串,表格等。另外包含在 printf 裏面的字串也會放在 code constants,所以常常發現沒有宣告 code 變數,卻出現 ?CO?MYCODE 在 prog_name.m51 裏。- 假設專案名稱為 prog,原始碼檔案為 test.c,晶片為 winbond W79E632。
- 在 prog.m51 裏的 C O D E M E M O R Y 區塊裏放置 common area 的資料描述。
- 在 test.c 裏的 code constants 會被統一放置在 ?CO?TEST 裏。
- Project --> Options for Target 'W79E632' --> BL51 Misc。
- 在 Misc controls 裏填入 BANK0(?CO?TEST (0x8000)) 即可,0x8000 是指定放在 bank0 的位址,也可以不填,由 keil 自動調配。
- 如果有多個 code constants,則使用逗號來分別,如 BANK0(?CO?TSUMU58EJ_CONF , ?CO?MODE_CONF, ?CO?MSTAR)。
要非常注意 code constants 移到 bank0 裏時,只能給 bank0 的函數使用,如果 bank1 函數要使用同樣的資料,會造成找到錯誤的資料而發生不可預期的問題。還有也要檢查 bank0 函數使用時,有無呼叫到 bank1 的函數,該函數是否把 code constants 當成參數傳到 bank1 函數去使用,如此也會產生大問題。
SDCC
| 記憶體區塊 | 指向位址 | 變數型別 | 變數名稱 | 初始值 |
| __xdata | [__at 0xC000] | unsigned char | test_data | [=0x01] |
指向位址及初始值可以不宣告。指向位址沒有宣告的話,則由編譯器自動分配位置給變數,但由於 sfr 的特殊性,所以一定要包含 __at。
- data / near
// c code __data unsigned char test_data=0x01; // asm code mov _test_data,#0x01 - xdata / far
// c code __xdata unsigned char test_xdata=0x01; // asm code mov dptr,#_test_xdata mov a,#0x01 movx @dptr,a
未完待續......
Keil C51
新的定義方式。
| 變數型別 | 記憶體區塊 | 變數名稱 | 初始值 |
| unsigned char | xdata | test_data | [=0x01] |
舊的定義方式,不建議使用。
| 記憶體區塊 | 變數型別 | 變數名稱 | 初始值 |
| xdata | unsigned char | test_data | [=0x01] |
指向絕對位址定義方式,不可直接給初始值。
| 變數型別 | 記憶體區塊 | 變數名稱 | 指向位址 |
| unsigned char | xdata | test_data | _at_ 0xC000 |
但是如果一定要在指向絕對位址時又同時給初始值呢?? 請看http://blog.sina.com.cn/s/blog_4c5cec3b0100dmtd.html。
未完待續......
