c51軟復位,實在經典,分析實在透徹

2004年7月的一天,在電子BBS討論區上溜達,看到一個有趣的帖子,整個帖子內容如下:

純C51復位功能函數:一個大三學生,讓人又愛又怕

現單列復位部分如下:

main()

{

   unsigned char code rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32};  // 復位代碼

   (*((void (*)())(rst)))();  // 執行上一行代碼,將rst數組當函數調用

}

本來我告訴他嵌入如下代碼:

clr a

push acc

push acc

reti

結果他卻玩了前面哪一段,而數組rst[]中的內容恰恰是上面的彙編機器碼,他的做法是將
rst數組的數據當作代碼保存,然後採用絕對地址方式指向該數組,將該數組中的代碼當作
函數來運行。居然通過了!

我覺得有問題,我說即使如此,那絕對地址調用也應該寫成(*((void (*)())(&rst)))() 
纔對呀,結果他反駁說,那樣的話,rst的地址就會當成參數傳遞給這個絕對地址函數,而
實際LJMP調用的地址並非rst的地址,而是一個不確定的地址。於是我按照自己的說法嘗試
了一下,看看彙編結果,還真的是將rst的地址傳遞給了R1 R2,而絕對函數最終LJMP到了
一個莫名其妙的地址上去了,死翹!

看來C真是一匹不容易駕馭的野馬,這個大三學生理解力在我之上,我30多歲的人了,幹了
這麼多年還沒他的境界呢,唉,人家才學了幾天啊,翻了幾天書就這麼厲害了,服了!



l         首先分析帖子的C語言代碼

第一句定義一個數組rst[],數組內數據就是完成復位功能的彙編機器碼,具體對應關係
爲:clr a == 0xe4、push acc == 0xc0,0xe0、reti ==0x32

第二句是一個函數指針的用法,函數指針用法稍微有點複雜,可參看本人著的書,:),以
下爲快速入門講解。

定義一個返回值是空函數指針的定義形式如下:

void (*p) ( )

當把函數指針賦值後,就能通過函數指針調用函數,調用形式如下,

      (*p) ( );

或等價的簡化形式:

p ( );

假設rst就是函數指針,則如下調用形式就可以令單片機復位再起。

(*rst ) ( );  

但可惜,rst不是函數指針,而是數組名,雖然兩者都是地址,但不可直接調用數組名。

如同把char型變量a賦值給int型變量b,(int) 表示強制類型轉換:

b = (int) a

函數指針的強制類型轉換公式如下(C語言的哲學是定義形式和使用一致):

(  (void (*)()  ) rst 

這樣經過轉換後的rst就可以當作函數指針使用了,簡單的調用形式如下:

#define  K     (  (void (*)( )  ) rst

(*K) ( )

或:

(     * (  void (*)( )  )rst      ) ( );

這樣的語句就完成復位再啓功能了。類型轉換符()的優先級跟指針運算符*的優先級相同,
二者的結合方向是自右至左,所以上述語句就能完成復位功能了。保險起見有些程序員常
常喜歡再加個括號:

#define  K     (   (  (void (*)( )  ) rst   )

(*K) ( )



(     *(   (  void (*)( )  )rst   )    ) ( );



由於沒有輸入參數,上述復位代碼更嚴謹的寫法是: 

#define  K     (   (  (void (*)(void )  ) rst   )

(*K) ( )



(     *(   (  void (*)(void )  )rst   )    ) ( );



l         關於帖子作者的解釋

千萬不要犯“&rst”形式的錯誤,對於一維數組而言,數組名rst就代表地址。以下二者等
價,更常用的是等式左邊的形式:

rst == &rst[0]

整個函數指針無所謂參數傳遞,只是把rst當作程序執行地址調用而已,那個學生的解釋也
有問題。

還有一點必須提及,不是說能通過編譯,甚至生成正確代碼,就表示某語句一定是對的。
對很複雜的語句,要考慮到編譯器不嚴格甚至出錯的可能性。



l         哈佛結構和一個蠕蟲病毒

請注意,定義數組rst[]時用了關鍵字code,這是C51特有的關鍵字,意味着把數組定義到
程序空間。標準C是沒有關鍵字code的。

哈佛結構和普林斯頓結構:

哈佛結構——程序空間和存儲空間分開的。C51算是不太嚴格的哈佛結構——雖地址線分
開,但數據線沒有分開。DSP是增強的哈佛結構。

PC電腦上奔騰CPU是普林斯頓結構——數據空間和程序空間統一編址。



如果數組rst[]數據的彙編機器碼是刪除文件的機器碼,這算不算是病毒?

曾經流行過一種蠕蟲病毒,其發作機理採取的就是將惡意代碼保存成文本文件,然後通過
指針調用執行這個文本,很多殺毒程序也不會查詢文本文件。

程序也罷,數據也罷都是二進制形式,如果數據空間和程序空間是統一編碼的, 數據當然
可以當作程序運行。

在這一點上,相對而言,哈佛結構的CPU安全性會好一點點。但嵌入式應用少有病毒,一般
不用關心。



l         單片機復位的更好方法

帖子中彙編語言解釋如下:

clr a                      //清除ACC=0

push acc               //壓0到堆棧——8位

push acc               //再壓0到堆棧——再8位

reti                        //返回到0地址,從而執行。

帖子作者的這種復位方法比較麻煩,更加簡單的復位寫法是(摘自《C缺陷與陷阱》):

(     * (  void (*)( )  )0      ) ( );

本句的分析方法同上,但更加精煉,沒有多餘的彙編語句。



上述復位的方法可稱爲軟件復位。

軟件復位跟真正上電覆位有很大差別:上電覆位時大部分寄存器都有確定的復位值;軟件
復位則只相當於從0地址開始執行而已,寄存器不會變爲確定的復位值。

如果用戶要編程實現上電覆位這種情況,在程序中不要踢看門狗即可。大部分單片機都有
看門狗吧。



l         附錄

筆者精於DSP C24xx,但不太懂C51;讀者應能從函數指針的定義和引用中看出來,C語言的
設計哲學是使用形式和定義形式一致,雖然這一點飽受質疑。

如果你覺得雞蛋好吃時,不必認識那隻母雞;但如果你覺得本文不錯,請來筆者網站坐坐
吧www.1piao.com/wlg.asp。

2004年7月看到這個有意思的帖子,也幹了一件蠢事——買了飛利浦的一款拍照手機,屏幕
有強烈閃爍感,飛利浦拒換,服務意識真是不敢恭維。

可來信免費轉載本文,請保持整篇文章的完整性,包括本句。 


----------------2---------------


彙編中的ORG 0X0000H 在C51中如何實現.
一般是在連接定位模塊中來進行地址分配
1、選擇"options fo target   "
2、選擇“BL51 Locate”
3、在code 欄填入   ?PR?MAIN?SS(0x800)

其中MAIN是你要定位的函數名,SS是函數所在的文件名,要是有多個函數需要定位,則在中間加
逗號。



功能強大的時鐘中斷
  在單片機程序設計中,設置一個好的時鐘中斷,將能使一個CPU發揮兩個CPU的功效,大大方
便和簡化程序的編制,提高系統的效率與可操作性。我們可以把一些例行的及需要定時執行的程
序放在時鐘中斷中,還可以利用時鐘中斷協助主程序完成定時、延時等操作。
  下面以6MHz時鐘的AT89C51系統爲例,說明時鐘中斷的應用。
  定時器初值與中斷週期 時鐘中斷無需過於頻繁,一般取20mS(50Hz)即可。如需要百分之一
秒的時基信號,可取10mS(100Hz)。這裏取20mS,用定時器T0工作於16位定時器方式(方式1)。
T0的工作方式爲:每過一個機器週期自動加1,當計滿0FFFFh,要溢出時,便會產生中斷,並由
硬件設置相應的標誌位供軟件查詢。即中斷時比啓動時經過了N+1個機器週期。所以,我們只要
在T0中預先存入一個比滿值0FFFFh小N的數,然後啓動定時器,便會在N個機器週期後產生中斷。
這個值便是所謂的“初值”。下面計算我們需要的初值:時鐘爲6MHz,12個時鐘週期爲一個機器
週期,20mS中有10000個機器週期。(10000)10=(2710)16,0FFFFh-2710h+1=0D8F0h。由於響應
中斷、保護現場及重裝初值還需要7~8個機器週期,把這個值再加上7,即T0應裝入的初值是
0D8F7h。每次中斷進入後,先把A及PSW的值壓入堆
棧,然後即把0D8F7h裝入T0。
  設置一個單元,每次中斷加1 我們可以取內部RAM中一個單元,取名爲INCPI(Increase 
Per Interrupt),在中斷中,裝完T0初值後,用INC INCPI指令將其加一。從這個單元中,無
論中斷程序還是主程序,都可以從中獲得20mS的1~256之間任意整數倍的信號。例如:有一段向
數碼管送顯的程序,需要每0.5秒執行一次以便刷新顯示器,便可以設一單元(稱爲等待單元)
W_DISP,用/MOV A,INCPI/ADD A,#25/MOV W_DISP,A/語句讓其比當前的INCPI值大25,然後在
每次中斷中檢查是否於INCPI值相等。若相等,說明已過了25箇中斷週期,便執行送顯程序,並
且讓W_DISP再加上25,等待下個0.5秒。我們可以設置多個等待單元,以便取出多個不同的時基
信號。讓中斷程序在每次中斷時依次查詢各個等待單元是否與INCPI相等,若相等,則執行相應
的處理,並重新設置該等待單元的值,否則跳過。例如:用0.5秒信號刷新或閃爍顯示器,用1秒
信號產生實時時鐘,或輸出一定頻率的方波,以一定間隔查詢輸入設備等。
  在中斷中讀鍵 通常,我們在主程序中讀鍵盤,步驟爲:掃描鍵盤,若有鍵按下,則延時幾
十毫秒去抖動,再次確認此鍵確實按下,然後處理該鍵對應的工作,完成後再次重上述步驟。但
這有兩點不足:1.處理相應工作時無法鎖存按鍵的輸入,即可能漏鍵。2.延時去抖時CPU無法做
其它事情,效率不高。如果把讀鍵放入時鐘中斷中,則可避免上述不足。方法爲:如果兩次相鄰
的中斷中都讀到同一個鍵按下,則這個鍵是有效的(達到了去抖目的),並將其鎖存到先入先出
(隊列)的鍵盤緩衝區,等主程序來處理。這樣,主程序處理按鍵的同時,仍可響應鍵盤的輸入。
緩衝區深度通常可設爲8級,若鎖存的鍵數多於8個,則忽略新的按鍵,並報警提示用戶新的按鍵
將無效。若鍵盤緩衝隊列停滯的時間大大長於主程序處理按鍵所需要的最大時間,說明主程序已
出錯或跑飛,可以在中斷用指令將系統復位,起到了看門狗的目的。  
    主程序中的延時 由於有常開的時鐘中斷,所以當主程序中有需要時間較短、精度較高的延
時時,應暫時把時鐘中斷關閉。而程序中需要時間較長、精度不高的延時時,便可仿照下需的寫
法,避免多層嵌套的循環延時。
  例:在P1.1輸出1秒的高電平脈衝
    MOV    A,INCPI
    INC    A
    CJNE    A,INCPI$    ;等待一次中斷處理完成
    SETB    P1.1        ;設P1.1爲H,脈衝開始
    ADD    A,#50        ;50個20mS爲1秒
    CJNE    A,INCPI,$    ;等中斷將INCPI加一50次
    CLR    P1.1        ;設P1.1爲L,脈衝結束
  結束語:從上看出,要靈活地應用時鐘中斷,將任務合理分配給中斷和主程序,並且二者要
分工明確,接口簡單。這其中的技巧還需要大家在實踐中多多摸索與體會。另外要注意:應儘量
縮短中斷處理程序的執行時間,更不要長於20mS。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章