寄存器 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来寻找堆栈里数据或者地址.

参考

SDCC 的 MCS-51 汇编基础概念和传参方式的更多相关文章

  1. Python基础数据类型-函数传参详解

    Python基础数据类型-函数传参详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.位置参数 #!/usr/bin/env python #_*_coding:utf-8_* ...

  2. Java基础 - 面向对象 - 类方法传参

    调用方法时可以给该方法传递一个或多个值,传给方法的值叫实参,在方法内部,接收实参的变量叫做形参,形参的声明语法与变量的声明语法一样.形参只在方法内部有效. Java中方法的参数主要有3种,分别为值参数 ...

  3. python基础:函数传参、全局变量、局部变量、内置函数、匿名函数、递归、os模块、time模块

    ---恢复内容开始--- 一.函数相关: 1.1位置参数: ef hello(name,sex,county='china'): pass #hello('hh','nv') #位置参数.默认参数 1 ...

  4. 【spring揭秘】1、关于IOC的基础概念

    1.基础概念 IOC有三种注入方式: 1.构造方法注入,就是通过构造方法进行实例化成员属性对象,优点是实现对象之后直接就可以使用,但是参数过多也是个麻烦 2.setter方法注入,实现相应的sette ...

  5. 鸿蒙内核源码分析(汇编基础篇) | CPU在哪里打卡上班? | 百篇博客分析OpenHarmony源码 | v22.01

    百篇博客系列篇.本篇为: v22.xx 鸿蒙内核源码分析(汇编基础篇) | CPU在哪里打卡上班 | 51.c.h .o 硬件架构相关篇为: v22.xx 鸿蒙内核源码分析(汇编基础篇) | CPU在 ...

  6. 【Machine Learning】机器学习及其基础概念简介

    机器学习及其基础概念简介 作者:白宁超 2016年12月23日21:24:51 摘要:随着机器学习和深度学习的热潮,各种图书层出不穷.然而多数是基础理论知识介绍,缺乏实现的深入理解.本系列文章是作者结 ...

  7. ASM:《X86汇编语言-从实模式到保护模式》5-7章:汇编基础

    第5-7章感觉是这一本书中比较奇怪的章节,可能是作者考虑到读者人群水平的差异,故意由浅入深地讲如何在屏幕上显示字符和使用mov,jmp指令等等,但是这样讲的东西有点重复,而且看了第六,第七章以后,感觉 ...

  8. linux设备驱动归纳总结(二):模块的相关基础概念【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-59415.html linux设备驱动归纳总结(二):模块的相关基础概念 系统平台:Ubuntu 10 ...

  9. linux设备驱动归纳总结(一)内核的相关基础概念【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-59413.html linux设备驱动归纳总结(一):内核的相关基础概念 xxxxxxxxxxxxxx ...

随机推荐

  1. JAVA获取本机的MAC地址

    /** * 获取本机的Mac地址 * @return */ public String getMac() { InetAddress ia; byte[] mac = null; try { // 获 ...

  2. java源码——计算大于一个数的最小素数

    根据输入数字,计算大于一个数的最小素数. 话不多说,直接贴码. package com.fuxuemingzhu.countprime.main; import java.util.Scanner; ...

  3. 【LeetCode】14. Longest Common Prefix 最长公共前缀

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 个人公众号:负雪明烛 本文关键词:prefix, 公共前缀,题解,leetcode, 力扣 ...

  4. 1266 - Points in Rectangle

    1266 - Points in Rectangle    PDF (English) Statistics Forum Time Limit: 2 second(s) Memory Limit: 3 ...

  5. 如何让 Spring Security 「少管闲事」

    记两种让 Spring Security「少管闲事」的方法. 遇到问题 一个应用对外提供 Rest 接口,接口的访问认证通过 Spring Security OAuth2 控制,token 形式为 J ...

  6. 【C/C++笔记】友元类函数

    最近学了友元,有三个用法: 1友元函数 2友元类 3友元类函数 我发现友元类函数的用法要比上两个用法要严格,不按格式写会各种出错,要把两个类都拆开来写,共分4步. 第一步: class A; //有 ...

  7. The Limitations of Deep Learning in Adversarial Settings

    目录 概 主要内容 alg2, alg3 一些有趣的实验指标 Hardness measure Adversarial distance Nicolas Papernot, Patrick McDan ...

  8. vmware虚拟IOS系统

    安装虚拟机     --以管理员的身份运行

  9. [算法笔记-题解]问题 C: 例题4-3 比较交换3个实数值,并按序输出

    问题 C: 例题4-3 比较交换3个实数值,并按序输出 [命题人 : 外部导入] 时间限制 : 1.000 sec 内存限制 : 12 MB 题目描述 从键盘输入3个实数a, b, c,通过比较交换, ...

  10. 数据可视化 gojs 简单使用介绍

    目录 1. gojs 简介 2. gojs 应用场景 3. 为什么选用 gojs: 4. gojs 上手指南 5. 小技巧(非常实用哦) 6. 实践:实现节点分组关系可视化交互图 最后 本文是关于如何 ...