题目
今天上课讲了汇编的条件跳转指令,然后老师出了一个例题,利用跳转指令和 NOP 指令实现延时。
指令介绍
ORG 指令
ORG
指令用于指定程序或数据在内存中的起始地址。当汇编器开始处理源代码时,ORG
指令告诉它从哪个地址开始放置代码或数据。例如,ORG 100h
表示程序的起始地址为内存地址 100h
。
MOV 指令
MOV
指令用于数据传输。它将数据从一个位置移动到另一个位置,但不影响源数据。例如,MOV AX, BX
将 BX
寄存器的内容复制到 AX
寄存器,但不会改变 BX
寄存器的内容。
DJNZ 指令
DJNZ
(递减并跳转如果不为零)指令通常用于循环结构中。它先将指定寄存器的值减一,然后检查结果是否为零。如果结果不为零,程序跳转到指定的标签或地址。例如,DJNZ BX, LABEL
将 BX
寄存器的值减一,如果减完后 BX
不为零,则跳转到 LABEL
。
NOP 指令
NOP
指令代表"无操作"(No Operation)。在大多数汇编语言中,这个指令的作用就是不执行任何操作。它占用一个指令周期,但不会改变任何寄存器或内存中的值。NOP
指令通常用来产生微小的延迟,或者作为代码中的占位符。
RET 指令
RET
指令用于从子程序返回。当执行到 RET
指令时,程序控制返回到调用子程序的下一条指令。这通常涉及弹出之前压入堆栈的返回地址。
例题中给出了晶振周期 12MHz 和 50ms 的延时,根据机器周期 T = 12/f 可得出单个机器周期为 1µs,然后 50ms/1µs = 50000,即需要延时 50000 个机器周期。
老师利用各指令的特性和占用机器周期数,通过循环和插入 NOP 指令来逼近题目要求的 50000T。
然后老师改了下题目,作为这节课的课后作业,并特别强调了要求的误差:"一般的同学只要能做到小于等于 1% 就行,优秀一点的同学可以尝试做到 0.3% 的误差。"
简单计算一下,f = 6MHz, t = 1min -> T = 30000000 (计算过程略)
要求 30000000 (3e7)个机器周期,考虑到寄存器是 8 位的,最大值是 255,那么应该需要三四层循环嵌套才能做到这么长的延时,要做到 0.3% 的误差似乎没那么容易。
但既然我发了这篇笔记,那么说明我已经做到了 0.3% 以下的误差(自豪脸)
理论分析
首先要明确多重循环和机器周期数的关系.
设 N 表示 N 重循环,Tn 表示在 N 重循环下需要的机器周期数,Rn 为第 N 重循环的寄存器初值(即循环次数)。
首先,如果没有循环,那么就只有一个 RET 语句,占用 2 机器周期,即 T₀ = 2
接着分析单层循环的情况, 有如下代码
此时 N = 1, R₁ 为寄存器初值
设循环内部代码消耗 i₁ 个机器周期, 则机器周期 T₁ = 1 + (i₁ + 2) * R₁ + 2
如果仅有单层循环, 则内部代码为空, 即 i₁ = 0, 则 T₁ = 1 + 2 * R₁ + 2
如果内部代码也是一个循环, 则 i₁ -> 1 + (i₂ + 2) * R₂
则双重循环下的机器周期 T₂ = 1 + (1 + (i₂ + 2) * R₂ + 2) * R₁ + 2
不难得出三重循环的机器周期 T₃ = 1 + (1 + (1 + (i₃ + 2) * R₃ + 2) * R₂ + 2) * R₁ + 2
整理一下有
T₁ = 2 * R₁ + 3
T₂ = (2 * R₂ + 3) * R₁ + 3
T₃ = ((2 * R₃ + 3) * R₂ + 3) * R₁ + 3
看起来很像某种递推函数, 观察易得
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T n = { 2 if n = 0 T n − 1 ⋅ R n + 3 otherwise T_n = \begin{cases} 2 & \text{if } n=0 \\ T_{n-1} \cdot R_n + 3 & \text{otherwise} \end{cases} </math>Tn={2Tn−1⋅Rn+3if n=0otherwise
根据函数可以写出对应的C语言代码
c
double calc(int *arr, int *end) {
return arr == end ? 2 : calc(arr + 1, end) * *arr + 3;
}
暴力搜索
循环计数器 RN 的取值范围为 {1, 2, 3, ..., 255, 256} (256 可以通过赋值 #0 来实现)
有 N 重循环就是求这个集合的 N 重笛卡尔积, 然后一一计算每个元素与目标 T 的 delta 误差值
遍历的部分代码如下
c
void traverse(int arr[], int index) {
if (index == N) {
check(arr);
} else {
for (int i = MIN; i <= MAX; i++) {
arr[index] = i;
traverse(arr, index + 1);
}
}
}
经过代码的计算, 可以得出在三重循环时, 循环计数初值分别为 [253, 255, 231] 时,与目标 30000000 个机器周期的误差为 0.0007899937590493036 %
而在四重循环时, 有如图所示的 61 组解可以达到完美的精度(0 误差)
汇编代码生成
观察例题代码, 发现除了数值之外, 多重循环的汇编实现也具有一定的规律, 因此可以使用模板字符串写一个代码生成器, 下面我将使用我日常使用较多的脚本语言 JavaScript 来写
JavaScript
function asmCodeGenerate(arr) {
const movs = arr.map((e, i) => `\tMOV R${i},#${e}`)
const djnzs = arr.map((_, i) => `\tDJNZ R${i},LOOP${i}`).reverse()
const mds = [...movs, ...djnzs]
for (let i = 1; i <= arr.length; ++i) {
mds[i] = `LOOP${i - 1}:` + mds[i]
}
return ['\tORG 7100H', ...mds, '\tRET'].join('\n')
}
console.log(asmCodeGenerate([239, 240, 104, 1]))
生成代码:
asm
ORG 7100H
MOV R0,#239
LOOP0: MOV R1,#240
LOOP1: MOV R2,#104
LOOP2: MOV R3,#1
LOOP3: DJNZ R3,LOOP3
DJNZ R2,LOOP2
DJNZ R1,LOOP1
DJNZ R0,LOOP0
RET
🎉完结撒花!交作业!
(如有错误和遗漏请指正 QAQ)