寄存器用于数据的临时存储, 其数据可以表示为
8051的寄存器几乎都是8位寄存器, 因为8位MCU处理的主要是8位数据, 如果数据大于8位, 则需要拆成多段分别处理. 一个8位的寄存器, 从D7到D0代表起第7位到第0位, D7这端为MSB(most significant bit), D0这端为LSB(least significant bit).
其中的6个位是预先定义的, 分别为
8051的基础内存为128字节, 地址为00H - 7FH, 这128字节被分成三组
格式为 MOV destination, source
赋值操作有几种类型
将值0x55赋值给寄存器A
MOV A,#55H
将寄存器A的值赋值给R0
MOV R0,A
将R0中存储的值作为地址, 这个地址存储的值赋值给A
MOV A,@R0
ADD A, source
例如计算 #25H + #34H
MOV A, #25H ;load one operand
;into A (A=25H)
ADD A, #34H ;add the second
;operand 34H to A
汇编语言的指令格式为
[label:] Mnemonic [operands] [;comment]
汇编语言的编译过程
如果使用VSCode + PlatformIO开发, 可以在项目的 .pio 目录下看到这些文件
lst(list)文件对于开发者非常有用, 在里面会按行显示每一句汇编语句对应的机器指令, 及其在代码区的偏移位置. 可以检查语法错误已经debug分析.
编译器总是使用全局寄存器 DPL, DPH, B 和 ACC 传递第一个函数参数(必须是非bit型参数)和传递函数返回结果
在 B 中存储通用指针的类型:
第二个参数可以在堆栈上存储(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 的和并返回. 其中
_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)而变化, 这时候可以将SP赋值给BP, 通过BP来读取堆栈里数据.