在单片机汇编语言编程中,CMP
(Compare)指令是一个核心且频繁使用的指令,其功能是执行两个操作数的比较操作,虽然它本身不改变操作数的值,但会根据比较结果影响处理器状态寄存器中的标志位(Flag),这些标志位是后续条件跳转(如JZ
, JNC
, JC
, JNZ
等)指令执行判断的基础,理解CMP
的工作原理、标志位变化以及应用场景,对于编写高效、可靠的单片机汇编程序至关重要。
CMP
指令的基本原理与操作
CMP
指令的核心功能是计算两个操作数(通常称为目的操作数
和源操作数
)的差值(目的操作数 源操作数
),但不将结果存回目的操作数,它仅根据这个内部计算出的差值来设置或清除状态寄存器中的关键标志位,其典型语法格式为:
CMP 目的操作数, 源操作数
- 目的操作数 (Destination Operand): 通常是寄存器(如
R0
-R7
在8051中)或内存单元(由间接寻址如@Ri
指定),它参与减法运算,但其值在指令执行后保持不变。 - 源操作数 (Source Operand): 可以是立即数(如
#30H
)、寄存器或内存单元,它也参与减法运算,其值同样保持不变。
关键点: CMP
指令执行的效果等同于执行了SUB 目的操作数, 源操作数
指令后,丢弃了减法结果,但保留了标志位的变化,这使得CMP
成为纯粹的“比较”工具,而不会意外修改数据。
CMP
指令影响的标志位
CMP
指令主要影响以下四个关键标志位(以常见的8051架构为例):
-
零标志位 (Zero Flag, Z):
- 置位 (Z=1): 当
目的操作数 源操作数
的结果等于0
时,即目的操作数 == 源操作数
。 - 清零 (Z=0): 当
目的操作数 源操作数
的结果不等于0
时,即目的操作数 != 源操作数
。 - 用途: 判断两个数是否相等,后续常跟
JZ
(若Z=1则跳转)或JNZ
(若Z=0则跳转)。
- 置位 (Z=1): 当
-
进位标志位 (Carry Flag, C):
- 置位 (C=1): 当
目的操作数 源操作数
产生借位时,这发生在无符号数比较中,当目的操作数 < 源操作数
时(因为减法需要向更高位借位)。 - 清零 (C=0): 当
目的操作数 源操作数
没有产生借位时,这发生在无符号数比较中,当目的操作数 >= 源操作数
时。 - 用途: 判断两个无符号数的大小关系(大于等于或小于),后续常跟
JC
(若C=1则跳转,即小于)或JNC
(若C=0则跳转,即大于等于)。
- 置位 (C=1): 当
-
溢出标志位 (Overflow Flag, V/OV):
- 置位 (OV=1): 当
目的操作数 源操作数
的结果超出了有符号数的表示范围(对于8位单片机,即结果不在-128到+127之间),这发生在两个符号相反的操作数相减时,且结果的符号与目的操作数的符号不同。 - 清零 (OV=0): 当减法结果在有符号数的表示范围内,或没有发生溢出。
- 用途: 判断有符号数比较是否发生溢出,是正确判断有符号数大小关系(大于、小于、等于)的必要条件,需要结合符号标志位
N
一起使用。
- 置位 (OV=1): 当
-
符号标志位 (Sign Flag, N/ACB):
- 置位 (N=1): 当
目的操作数 源操作数
的结果的最高位(MSB)为1
时,对于有符号数,这表示结果为负数。 - 清零 (N=0): 当
目的操作数 源操作数
的结果的最高位(MSB)为0
时,对于有符号数,这表示结果为正数或零。 - 用途: 反映减法结果的符号,在有符号数比较中,需要结合溢出标志位
OV
才能准确判断大小关系。
- 置位 (N=1): 当
CMP
指令的应用场景
CMP
指令是构建程序逻辑分支(如if-else
、switch-case
)和循环控制(如for
、while
)的基石,以下是一些典型应用:
-
数值比较与条件分支:
- 判断相等:
MOV A, #50H ; 将立即数50H加载到累加器A CMP A, R1 ; 比较A和寄存器R1的值 JZ EQUAL ; 如果相等(Z=1),跳转到EQUAL标签 ; 不相等时的处理代码 SJMP NEXT EQUAL: ; 相等时的处理代码 NEXT:
- 无符号数大小比较:
MOV A, #0A0H ; A = 160 (无符号) CMP A, R2 ; 比较A和R2 (假设R2=100) JNC A_GE_R2 ; 如果A >= R2 (C=0),跳转 ; A < R2 时的处理代码 SJMP NEXT A_GE_R2: ; A >= R2 时的处理代码 NEXT:
- 有符号数大小比较 (需结合OV和N):
- 判断大于 (>):
(OV == 0 AND N == 0) OR (OV == 1 AND N == 1)
-> 结果为正且未溢出,或结果为负但溢出(实际应为正)。 - 判断小于 (<):
(OV == 0 AND N == 1) OR (OV == 1 AND N == 0)
-> 结果为负且未溢出,或结果为正但溢出(实际应为负)。 - 判断大于等于 (>=):
NOT (小于)
或(等于) OR (大于)
。 - 判断小于等于 (<=):
NOT (大于)
或(等于) OR (小于)
。 - 示例 (判断A > R3, 有符号):
CMP A, R3 JLE A_LE_R3 ; 如果A <= R3, 跳转 (使用JLE指令,内部检查OV和N) ; A > R3 时的处理代码 SJMP NEXT A_LE_R3: ; A <= R3 时的处理代码 NEXT:
注意:许多汇编器提供直接的有符号比较跳转指令(如
JG
,JGE
,JL
,JLE
),它们内部封装了对OV
和N
标志位的复杂判断逻辑,简化了编程。
- 判断大于 (>):
- 判断相等:
-
范围检查: 检查一个值是否在某个范围内(如
MIN <= X <= MAX
),通常需要两次CMP
。MOV A, R4 ; 假设R4存放待检查值X CMP A, #MIN_VAL ; 比较X和最小值MIN_VAL JC X_TOO_SMALL ; 如果X < MIN_VAL (C=1), 跳转 CMP A, #MAX_VAL ; 比较X和最大值MAX_VAL JNC X_IN_RANGE ; 如果X <= MAX_VAL (C=0), 跳转 (注意:这里用JNC表示X<=MAX_VAL,因为X-MAX_VAL>=0时C=0) ; X > MAX_VAL 时的处理代码 SJMP NEXT X_TOO_SMALL: ; X < MIN_VAL 时的处理代码 SJMP NEXT X_IN_RANGE: ; MIN_VAL <= X <= MAX_VAL 时的处理代码 NEXT:
-
循环控制:
CMP
常用于循环计数器与循环终值的比较,以决定是否继续循环。MOV R5, #10 ; 设置循环计数器R5=10 LOOP_START: ; 循环体代码 DEC R5 ; 计数器减1 CMP R5, #0 ; 比较计数器是否为0 JNZ LOOP_START ; 如果不为0(Z=0),跳回LOOP_START继续循环 ; 循环结束后的代码
CMP
与SUB
的区别
虽然CMP
和SUB
都执行减法操作并影响标志位,但存在关键区别:
特性 | CMP 指令 |
SUB 指令 |
---|---|---|
操作 | 计算 Dest Src |
计算 Dest Src |
结果存储 | 不存储结果,仅影响标志位 | 存储结果到 Dest 操作数 |
目的操作数 | 值不变 | 值被更新为 Dest Src 的结果 |
主要用途 | 比较两个数的大小或相等性 | 执行算术减法运算 |
对数据影响 | 无 | 修改目的操作数的值 |
示例对比:
假设 A = 30H
, R1 = 20H
CMP A, R1
:- 内部计算
30H 20H = 10H
。 - 标志位:
Z=0
(结果非0),C=0
(无借位, 30H>=20H),OV=0
(无溢出),N=0
(结果正)。 - 执行后:
A
仍为30H
,R1
仍为20H
。
- 内部计算
SUB A, R1
:- 计算
30H 20H = 10H
。 - 标志位:
Z=0
,C=0
,OV=0
,N=0
(同上)。 - 执行后:
A
被更新为10H
,R1
仍为20H
。
- 计算
硬件实现简述
在单片机CPU内部,CMP
指令的执行通常涉及以下步骤:
- 取指: 从程序存储器读取
CMP
指令的操作码。 - 译码: 解析操作码,识别出是
CMP
指令及其操作数寻址方式。 - 取操作数: 根据寻址方式,从寄存器文件或数据存储器中读取目的操作数和源操作数的值,送入ALU(算术逻辑单元)的输入端。
- ALU操作: ALU执行减法操作(
Dest Src
)。 - 标志位更新: ALU根据减法结果(差值)的状态(是否为0、是否产生借位、是否溢出、符号位)更新状态寄存器(PSW)中的
Z
,C
,OV
,N
等标志位。 - 结果丢弃: ALU计算出的差值结果不被写回到目的操作数所在的寄存器或内存单元。
- 程序计数器更新: PC指向下一条指令。
CMP
指令是单片机汇编语言中实现程序逻辑判断的核心工具,它通过执行一次“虚拟”的减法操作(Dest Src
),仅更新状态标志位(Z
, C
, OV
, N
)而不修改操作数本身,为后续的条件跳转指令提供决策依据,熟练掌握CMP
指令对不同类型数据(无符号、有符号)比较时标志位的变化规律,以及如何结合这些标志位(或使用封装好的条件跳转指令)实现复杂的条件判断(相等、大于、小于、范围检查)和循环控制,是编写结构清晰、逻辑正确、运行高效的单片机汇编程序的基础技能,理解CMP
与SUB
的本质区别(是否存储结果)对于避免意外修改数据也至关重要。
相关问答FAQs
Q1: 在比较两个有符号数时,为什么不能像无符号数那样只看进位标志位C?
A1: 因为进位标志位C
反映的是无符号数减法是否产生借位,它只能正确判断无符号数的大小关系(C=1
表示Dest < Src
,C=0
表示Dest >= Src
),对于有符号数,其大小关系不仅取决于数值大小,还取决于符号,比较-1
(FFH
)和+1
(01H
):
- 无符号比较:
FFH
(255) >01H
(1),CMP
后C=0
(无借位)。 - 有符号比较:
-1
<+1
。 如果仅看C=0
,会错误地得出-1 >= +1
的上文归纳,有符号数的正确比较需要同时考虑溢出标志位OV
和符号标志位N
: Dest > Src
(有符号): 当减法结果为正且未溢出(OV=0, N=0
),或结果为负但发生了溢出(OV=1, N=1
,实际结果应为正)时成立。Dest < Src
(有符号): 当减法结果为负且未溢出(OV=0, N=1
),或结果为正但发生了溢出(OV=1, N=0
,实际结果应为负)时成立。 有符号数比较必须综合使用OV
和N
标志位(或使用汇编器提供的专用有符号比较跳转指令如JG
,JL
等)。
Q2: CMP
指令执行后,目的操作数的值真的绝对不会改变吗?
A2: 在绝大多数标准单片机架构(如8051, PIC, AVR, ARM Cortex-M等)中,CMP
指令的明确定义就是不修改目的操作数的值,仅影响标志位,这是它与SUB
指令的核心区别,需要特别注意一种特殊情况:当目的操作数本身是一个内存地址(间接寻址),且该地址恰好指向一个硬件特殊功能寄存器(SFR)时,某些SFR具有“读-修改-写”特性,即读取它的值可能会触发硬件状态变化(例如读取串口数据缓冲区会清除接收标志位,读取某些状态寄存器会清除中断标志),如果CMP
指令的目的操作数使用了间接寻址方式(如CMP @R0, #data
),而R0
指向了这样一个具有“读-修改-写”特性的SFR地址,那么读取该SFR值以进行比较的操作本身,就可能导致其状态被意外修改,即使CMP
指令最终没有把减法结果写回去,在使用CMP
指令比较内存(尤其是SFR)内容时,务必确认目标地址是否具有这种“读操作即有副作用”的特性,避免引发难以调试的硬件交互问题,对于普通RAM数据,CMP
指令是安全的,不会改变其值。