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字节被分成三组
- 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来寻找堆栈里数据或者地址.