SDCC 的 MCS-51 彙編基礎概念和傳參方式

寄存器 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位元組被分成三組

  1. 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, 向下收縮
  2. 20H – 2FH, 16個位元組用於可以按位尋址內存訪問
  3. 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]

彙編語言的編譯過程

  1. 文本彙編程序, file.asm
  2. 彙編編譯, 產生lst文件 file.lst 和 obj文件 file.obj
  3. 連接器, 產生abs文件, file.abs
  4. 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來尋找堆棧里數據或者地址.

參考