暫存器 Register
暫存器用于資料的臨時存盤, 其資料可以表示為
- 用于處理的資料位元組
- 指向資料的地址
暫存器的結構
8051的暫存器幾乎都是8位暫存器, 因為8位MCU處理的主要是8位資料, 如果資料大于8位, 則需要拆成多段分別處理. 一個8位的暫存器, 從D7到D0代表起第7位到第0位, D7這端為MSB(most significant bit), D0這端為LSB(least significant bit).
常用暫存器
- A (累加器)
- B, R0, R1, R2, R3, R4, R5, R6, R7 在函式中使用的變數, R0-R7是變數, 地址并非唯一, 其絕對地址由AR0-AR7指定.
- DPTR(data pointer), PC(program counter) 這兩個都是16位雙位元組暫存器
- PC 指向下一個指令的地址, 16位寬度, 因此代碼區的最大范圍為0 - 0xFFFF, 64K位元組
- 8051啟動時, PC值為0x0000, 從代碼區0x0000開始執行第一條指令
- SP 堆疊頂指標, 其值為堆疊堆疊頂的地址, SDCC中, 堆疊的地址是向上增長的, 這個與常見的向下增長不同
- BP 基址指標暫存器BP(base pointer), 和堆疊指標SP聯合使用, 使用BP把SP的值傳遞給BP, 通過BP來尋找堆疊里資料或者地址.
程式狀態暫存器 PSW
其中的6個位是預先定義的, 分別為
- CY 最高位進位標志位, 當加法等運算結果超過0xFF, 產生進位時, 這個bit會被置1
- AC D3 至 D4 進位標志位, 當加法等運算低四位產生僅位時, 這個bit會被置1
- F0
- RS1 暫存器組選擇, RS1,RS0的組合對應的選擇為
- RS0 暫存器組選擇: 0,0:bank0, 0,1:bank1, 1,0:bank2, 1,1:bank3
- OV 溢位標志位
- --
- P 奇偶校驗位, 如果在暫存器A中的, 1的個數為偶數時這個bit為0, 1的個數為奇數時這個bit為1
記憶體結構
8051的基礎記憶體為128位元組, 地址為00H - 7FH, 這128位元組被分成三組
- 00H - 1FH, 32個位元組, 用于暫存器組和堆疊
- 這32個位元組被分成4組(4 banks), 每組8個暫存器R0-R7
- 當8051加電時, 默認使用暫存器組0(bank 0)
- 通過PWS的D4,D3選擇
- 堆疊頂地址存盤在SP暫存器
- SP暫存器只有8位, 因此其范圍只有00H-FFH
- 當8051加電時, SP暫存器的值為07, 因此記憶體地址08H就是堆疊的第一個地址, 這個地址與暫存器組1是重合的
- 可以給堆疊指定其它的地址
- PUSH操作時, SP會加1, 向上增長
- POP操作時, SP減1, 向下收縮
- 20H - 2FH, 16個位元組用于可以按位尋址記憶體訪問
- 30H - 7FH, 80個位元組屬于通用記憶體(scratch pad)
暫存器操作示例
MOV 賦值操作
格式為 MOV destination, source
賦值操作有幾種型別
直接數賦值
將值0x55賦值給暫存器A
MOV A,#55H
- 直接數可以賦值的暫存器為A, B, R0-R7
- 如果給暫存器賦值 #0 至 #F, 等價于賦值 #00H 至 #0FH
- 如果直接數超過8位數值, 會產生錯誤
暫存器賦值
將暫存器A的值賦值給R0
MOV R0,A
地址賦值
將R0中存盤的值作為地址, 這個地址存盤的值賦值給A
MOV A,@R0
加法操作
ADD A, source
- 將source的值與A相加, 結果存盤在A
- source可以是暫存器或直接數, 但是結果一定存盤在暫存器A
- ADD的第一個引數, 目標暫存器必須A
例如計算 #25H + #34H
MOV A, #25H ;load one operand
;into A (A=25H)
ADD A, #34H ;add the second
;operand 34H to A
SDCC匯編語言基礎概念
匯編語言的指令格式為
[label:] Mnemonic [operands] [;comment]
匯編語言的編譯程序
- 文本匯編程式, file.asm
- 匯編編譯, 產生lst檔案 file.lst 和 obj檔案 file.obj
- 連接器, 產生abs檔案, file.abs
- Object to Hex轉換, 產生hex檔案, file.hex
如果使用VSCode + PlatformIO開發, 可以在專案的 .pio 目錄下看到這些檔案
LST檔案
lst(list)檔案對于開發者非常有用, 在里面會按行顯示每一句匯編陳述句對應的機器指令, 及其在代碼區的偏移位置. 可以檢查語法錯誤已經debug分析.
偽指令
- DB: 用于定義資料, 可以是十進制數, 二進制數, 十六進制數, ASCII等
- ORG(origin): 用于指定起始地址
- END: 標識代碼結束
- EQU(equate): 用于定義常量
SDCC匯編函式引數傳遞
第一個引數和回傳值
編譯器總是使用全域暫存器 DPL, DPH, B 和 ACC 傳遞第一個函式引數(必須是非bit型引數)和傳遞函式回傳結果
- 1個位元組回傳值存盤在DPL
- 2個位元組: DPL(LSB)和DPH(MSB)
- 3個位元組(通用指標): DPH, DPL和 B
- 4個位元組: DPH, DPL, B 和 ACC
在 B 中存盤通用指標的型別:
- 0x00 – xdata/far, 外部資料存盤
- 0x40 – idata/near – , 內部資料存盤
- 0x60 – pdata, 外部資料存盤
- 0x80 – code, 代碼區
bit型引數, 位引數
- 在可重入函式中, 位引數在位可尋址空間中被傳遞到虛擬暫存器bits中
- 其它的情況, 直接在位記憶體中存盤
第二個之后的引數
第二個引數可以在堆疊上存盤(reentrant 或者使用 --stack-auto ), 也可以在資料/xdata存盤器中存盤(取決于存盤器型號)
可重入函式
對于通過函式指標呼叫, 并且帶兩個或兩個以上引數的函式, 必須是可重入的, 這樣編譯器才能正確地傳遞引數.
相關暫存器的說明
除非函式被定義為 _naked 或 --callee-saves/--all-callee-saves 或者使用了 callee_saves pragma, 呼叫方會在呼叫前后對暫存器 R0-R7 的值進行保護和恢復, 所以被呼叫的函式可以隨意讀寫 R0-R7.
并且如果函式未被定義為 _naked, 如果呼叫方和被呼叫函式使用了不同的暫存器組(register banks, 使用 __using 宣告), 呼叫方會在呼叫前后處理暫存器組的切換.
被呼叫的函式使用 DPL, DPH, B 和 ACC 獲取引數和存盤回傳結果
示例說明
非重入的函式呼叫
在 C 語言里, 呼叫函式時會將函式引數以及函式的區域變數放入堆疊, 但是由于8位MCS-51芯片內部堆疊空間有限, 無法像 windows/unix 那樣使用堆疊, 所以無法使用這種方式, 而是為每個函式的區域變數和引數申請一個空間來存放.
下面的例子是一個簡單的函式int test(int a, int b)用于計算 a 與 b 的和并回傳. 其中
- 第一個引數用 DPL, DPH, B, ACC 傳遞(從LSB -> MSB), a 是雙位元組, 所以存盤在 DPL, DPH
- 第二個引數用全域變數傳遞
_test_PARM_2
;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _test_PARM_2
.globl _test
;--------------------------------------------------------
; overlayable items in internal ram
;--------------------------------------------------------
.area OSEG (OVR,DATA) ; DATA area 0x00 ~ 0x80, 可重疊的空間
_test_PARM_2:
.ds 2 ; 預留的空間, 2位元組
;--------------------------------------------------------
; code
;--------------------------------------------------------
.area CSEG (CODE)
;------------------------------------------------------------
;Allocation info for local variables in function 'test'
;------------------------------------------------------------
;b Allocated with name '_test_PARM_2'
;a Allocated to registers r6 r7
;c Allocated to registers
;------------------------------------------------------------
; src/st7567_stc8h3k.c:32: int test(int a, int b)
; -----------------------------------------
; function test
; -----------------------------------------
_test:
ar7 = 0x07 ; ar0-ar7表示當前選中的暫存器組r0-r7的暫存器絕對地址,
; 這里將r0-r7的地址設定為00H到07H
ar6 = 0x06
ar5 = 0x05
ar4 = 0x04
ar3 = 0x03
ar2 = 0x02
ar1 = 0x01
ar0 = 0x00
mov r6,dpl ; 將第一個引數存入 r6, r7
mov r7,dph
; src/st7567_stc8h3k.c:34: int c = a + b;
mov a,_test_PARM_2 ; 將第二個引數的LSB存入a
add a,r6 ; 低8位相加
mov dpl,a ; 結果存入DPL
mov a,(_test_PARM_2 + 1) ; 將第二個引數的MSB存入a
addc a,r7 ; 高8位相加, 帶前一次運算的進位
mov dph,a ; 結果存入DPH
; src/st7567_stc8h3k.c:35: return c;
; src/st7567_stc8h3k.c:36: }
ret
從main中呼叫時, 第一個引數存入DPL, DPH, 第二個引數存入 _test_PARM_2和_test_PARM_2 + 1
mov _test_PARM_2,r4
mov (_test_PARM_2 + 1),r5
mov dpl,r6
mov dph,r7
lcall _test
可重入的函式呼叫
SDCC 將區域變數放到全域變數中后, 相當于成為了靜態變數, 因此無法在遞回函式中使用(無法重入), 并且在 interrupt function 中不能呼叫, 因為當中斷發生在這些函式中時就會發生重入,
造成區域變數被修改, 造成不可預期的結果. 所以對于這類場景, 需要在函式上加上__reentrant關y鍵詞. 此時編譯器會將區域變數放到堆疊上. 在這種情況下, 第二個及之后的引數將被放在堆疊中,
引數從右到左依次入堆疊, 因此第二個引數總是最后一個入堆疊, 在堆疊的頂部.
下面的例子還是上面的簡單函式int test(int a, int b), 但是加了__reentrant關鍵詞.
在函式的入口, 舊的 _bp 被入堆疊, 之后 SP 的值被復制給 _bp, 如果在堆疊上有區域變數, 也會在 SP 上存盤, 此時 _bp 指向的是堆疊上的第一個區域變數, 引數則存盤在更低的地址上.
sp是堆疊指標, 如果不保存的話就無法回傳呼叫它的程式,
因為下面要改變堆疊指標, 所以不能用入堆疊的方法保存, 只能保存在暫存器中
bp這個暫存器是專門在堆疊段操作在堆疊區的子程式的臨時變數用的, 很方便, 所以用bp保存sp的內容
;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _test ; 可以看到, 只有函式宣告, 沒有引數二的宣告
; 也沒有.area OSEG中對引數二的存盤預留
;--------------------------------------------------------
; code
;--------------------------------------------------------
.area CSEG (CODE)
;------------------------------------------------------------
;Allocation info for local variables in function 'test'
;------------------------------------------------------------
;b Allocated to stack - _bp -4 ; 引數二被放到了 _bp -4 位置,
; 如果還有引數三, 并且也是int, 會被放到 _bp -6 的位置
;a Allocated to registers r6 r7
;c Allocated to registers
;------------------------------------------------------------
; src/st7567_stc8h3k.c:32: int test(int a, int b) __reentrant
; -----------------------------------------
; function test
; -----------------------------------------
_test:
ar7 = 0x07 ; ar0-ar7表示當前選中的暫存器組r0-r7的暫存器絕對地址, 這里將r0-r7的地址設定為00H到07H
ar6 = 0x06
ar5 = 0x05
ar4 = 0x04
ar3 = 0x03
ar2 = 0x02
ar1 = 0x01
ar0 = 0x00
push _bp ; 將堆疊幀指標入堆疊, 原來堆疊頂是回傳地址, _bp入堆疊后, 堆疊頂變成了:
; _bp, 回傳地址, 引數二, 因為入堆疊動作, 堆疊頂地址增長了(SDCC中堆疊地址是往上增長的),
; SP指向了新的堆疊頂地址
mov _bp,sp ; 將此時的堆疊頂賦值給_bp, 注意, 這時候_bp里保存的變成了一個地址, 堆疊頂的地址.
mov r6,dpl ; 將引數一放入r6, r7
mov r7,dph
; src/st7567_stc8h3k.c:34: int c = a + b;
mov a,_bp ; 將_bp值賦值給a, 此時a里面存了堆疊頂地址
add a,#0xfc ; 8bit數加0xfc就等于減4, 得到最后一個引數的指標, 這里第二個引數就是最后一個引數
mov r0,a ; 結果賦值給r0
mov a,@r0 ; 將r0作為地址, 取到的值賦值給a
add a,r6 ; 與r6相加(低8位)
mov r6,a ; 結果存回r6
inc r0 ; r0++(下一個位元組的地址)
mov a,@r0 ; 將r0作為地址, 取到的值賦值給a
addc a,r7 ; 與r7相加(高8位), 帶前一步的進位
mov r7,a ; 將結果存回r7
mov dpl,r6 ; 將回傳結果存到dpl, dph
mov dph,r7
; src/st7567_stc8h3k.c:35: return c;
; src/st7567_stc8h3k.c:36: }
pop _bp ; 在回傳前, 恢復堆疊指標
ret
從main中呼叫這個函式, 在呼叫后恢復堆疊頂指標
push ar4 ; 引數二入堆疊
push ar5
mov dpl,r6 ; 引數一賦值給DPTR
mov dph,r7
lcall _test ; 呼叫(此時會將回傳地址入堆疊)
dec sp ; 此時恢復到了呼叫前的堆疊頂地址, 再dec兩次抵消掉引數二入堆疊產生的地址增長, 恢復堆疊頂位置
dec sp
在匯編的 reentrant 函式開頭, 有一個變數_bp, 這個變數在 sdcc/lib/src/_bp.c 中宣告, 是基址指標暫存器, 用來計算進入堆疊的引數和區域變數的偏移.
_bp is the stack frame pointer and is used to compute the offset into the stack for parameters and local variables.
基址指標暫存器BP(base pointer)的用途比較特殊, 是和堆疊指標SP聯合使用的, 例如在帶引數的子程序中用BP來獲取引數和訪問設在堆疊里面的臨時變數. 例如堆疊中壓入了資料或者地址, 如果想訪問這些資料或者地址, 但SP指向堆疊頂, 不能隨便亂改, 并且SP會隨著帶有堆疊操作(PUSH, CALL, INT, RETF)而變化, 這時候可以使用BP, 把SP的值傳遞給BP, 通過BP來尋找堆疊里資料或者地址.
參考
- SDCC使用手冊 http://sdcc.sourceforge.net/doc/sdccman.pdf
- 關于_BP和重入 http://jyhshin3.blogspot.com/2010/
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/419900.html
標籤:其他
上一篇:在VS code使用Remote-SSH遠程連接Linux 開發C++ 配置詳細介紹
下一篇:檔案權限問題導致plink報錯 Failed to open 21JAN.log. Try changing the --out parameter.
