1. 51单片机汇编基础实验全解析
作为一名嵌入式开发工程师,我经常需要与51单片机打交道。汇编语言作为最接近硬件的编程方式,虽然学习曲线陡峭,但掌握后能让我们对硬件有更深入的理解。下面我将分享我在学习51单片机汇编过程中完成的一系列实验,包含完整的代码解析和实战经验。
1.1 基础IO控制实验
1.1.1 LED基本控制
控制LED是单片机最基础的实验,通过这个实验我们可以熟悉51单片机的IO口操作。51单片机的P1口是准双向IO口,可以直接驱动LED。
assembly复制ORG 0000H
SJMP MAIN
ORG 0030H
MAIN:
MOV P1,#11110000B
SJMP $
END
这段代码实现了P1口高4位输出高电平(LED熄灭),低4位输出低电平(LED点亮)。在实际硬件连接时需要注意:
- LED阳极通过限流电阻(通常220Ω-1kΩ)接VCC,阴极接P1口
- 51单片机IO口拉电流能力较弱(约几十μA),灌电流能力较强(几个mA),因此推荐使用低电平点亮LED的连接方式
- 如果使用共阳LED,需要将逻辑取反
提示:初学者常犯的错误是忘记加限流电阻,这会损坏IO口或LED。根据LED工作电流(通常5-20mA)计算电阻值:R = (VCC - VLED) / ILED
1.1.2 LED闪烁控制
实现LED闪烁需要引入延时程序。在12MHz晶振下,51单片机一个机器周期为1μs。
assembly复制ORG 0000H
SJMP MAIN
ORG 0030H
MAIN:
LOOP:
MOV P1,#0FFH
LCALL DELAY
MOV P1,#00H
LCALL DELAY
SJMP LOOP
DELAY:
MOV R7,#10
DELAY1:
MOV R6,#250
DELAY2:
MOV R5,#200
DELAY3:
DJNZ R5,DELAY3
DJNZ R6,DELAY2
DJNZ R7,DELAY1
RET
END
延时程序计算:
- DJNZ指令执行时间为2个机器周期(2μs)
- 总延时 ≈ R7 × R6 × R5 × 2μs = 10×250×200×2μs = 1,000,000μs = 1s
实际调试时发现延时略小于1秒,因为MOV指令和RET也消耗时间。精确延时需要使用定时器。
1.1.3 LED流水灯效果
实现LED依次点亮可以使用循环左移指令RL:
assembly复制ORG 0000H
SJMP MAIN
ORG 0030H
MAIN:
MOV A,#01H
LOOP:
MOV P1,A
LCALL DELAY
RL A
SJMP LOOP
技巧:
- 初始值01H(00000001B)表示第一个LED点亮
- 每次循环左移一位,实现LED依次点亮
- 如果需要右移效果,可以使用RR指令
- 使用累加器A作为中间变量,避免直接对P1口进行移位操作
1.2 中断系统实验
1.2.1 外部中断控制LED闪烁频率
通过外部中断0改变LED闪烁频率:
assembly复制ORG 0000H
SJMP MAIN
ORG 0003H
SJMP INTP
ORG 0030H
MAIN:
SETB IT0 ; 下降沿触发
SETB EX0 ; 允许INT0中断
SETB EA ; 开总中断
MOV A,#10 ; 初始延时参数(1s)
LOOP:
MOV P1,#0FFH
LCALL DELAY
MOV P1,#00H
LCALL DELAY
SJMP LOOP
INTP:
MOV A,#3 ; 中断服务程序,改为300ms
RETI
DELAY: ; 延时子程序,时间由A决定
MOV R7,A
DELAY1:
MOV R6,#200
DELAY2:
MOV R5,#250
DELAY3:
DJNZ R5,DELAY3
DJNZ R6,DELAY2
DJNZ R7,DELAY1
RET
END
注意事项:
- 中断服务程序要尽量短,避免影响主程序运行
- 使用RETI而不是RET返回,以恢复中断系统状态
- 在中断中修改的变量在主程序中要合理使用,避免冲突
1.2.2 按键计数与长按检测
通过外部中断实现按键计数和长按清零:
assembly复制ORG 0000H
SJMP MAIN
ORG 0003H
SJMP INTP
ORG 0030H
MAIN:
SETB IT0 ; 下降沿触发
SETB EX0 ; 允许INT0中断
SETB EA ; 开总中断
MOV A,#00H ; 计数器清零
MOV P1,#00H ; LED初始状态
LOOP:
MOV R1,#00H ; 长按检测标志
JB P3.2,LOOP ; 等待按键按下
LCALL DELAY2S ; 延时2秒检测长按
CJNE R1,#1,LOOP ; 如果不是长按则继续
LCALL ZERO ; 长按清零处理
SJMP LOOP
INTP:
INC A ; 短按计数
MOV P1,A ; 显示计数值
INC R1 ; 设置短按标志
RETI
ZERO:
CLR A ; 清零计数器
MOV P1,A ; LED全灭
MOV R1,#00H ; 清除标志
RET
DELAY2S: ; 2秒延时子程序
MOV R7,#20
DELAY1:
MOV R6,#200
DELAY2:
MOV R5,#250
DELAY3:
DJNZ R5,DELAY3
DJNZ R6,DELAY2
DJNZ R7,DELAY1
RET
END
经验分享:
- 按键消抖可以通过硬件(RC滤波)或软件(延时10-20ms)实现
- 长按检测的关键是在按键按下后开始计时,超过阈值则判定为长按
- 多个按键可以通过扫描或中断+IO判断的方式实现
1.3 定时器应用实验
1.3.1 定时器方式1实现精确延时
使用定时器0工作方式1(16位定时器)实现精确1秒延时:
assembly复制ORG 0000H
AJMP MAIN
ORG 000BH
AJMP CTM
ORG 0030H
MAIN:
MOV TMOD,#01H ; 定时器0方式1
MOV TH0,#03CH ; 50ms初值高8位
MOV TL0,#0B0H ; 50ms初值低8位
SETB ET0 ; 允许定时器0中断
SETB TR0 ; 启动定时器0
SETB EA ; 开总中断
MOV R0,#20 ; 中断次数计数器(20×50ms=1s)
SJMP $
CTM:
MOV TH0,#03CH ; 重装初值
MOV TL0,#0B0H
DJNZ R0,EXT ; 未到1秒则返回
MOV R0,#20 ; 重置计数器
CPL P1.3 ; 取反LED
EXT:
RETI
END
定时器初值计算:
- 12MHz晶振,机器周期=1μs
- 方式1最大计数值65536,50ms需要50000个周期
- 初值=65536-50000=15536=3CB0H
- 因此TH0=3CH,TL0=B0H
1.3.2 定时器方式2实现PWM波
使用定时器1工作方式2(8位自动重装)生成PWM波:
assembly复制ORG 0000H
AJMP MAIN
ORG 001BH
AJMP CTM
ORG 0030H
MAIN:
CLR P1.3 ; 初始输出低电平
MOV R0,#40 ; 周期计数器(40×100×250μs=1s)
MOV R1,#100 ; 中断次数计数器
MOV TMOD,#20H ; 定时器1方式2
MOV TH1,#06H ; 250μs初值
MOV TL1,#06H
SETB ET1 ; 允许定时器1中断
SETB TR1 ; 启动定时器1
SETB EA ; 开总中断
SJMP $
CTM:
DJNZ R1,EXIT ; 未完成100次中断则返回
MOV R1,#100 ; 重置计数器
DJNZ R0,EXIT ; 未完成40周期则返回
CPL P1.3 ; 取反输出
MOV R0,#40 ; 重置周期计数器
EXIT:
RETI
END
技巧:
- 方式2的自动重装特性适合产生固定周期的信号
- 通过多级计数器可以实现较长周期的定时
- 调整占空比可以通过改变高低电平时间实现
1.4 串口通信实验
1.4.1 定时发送与接收显示
使用串口方式3实现双机通信,A机定时发送数据,B机接收并显示:
assembly复制; A机发送程序
ORG 0000H
SJMP MAIN
ORG 000BH
SJMP INTP_T0
ORG 0023H
SJMP INTP_S
ORG 0030H
MAIN:
MOV R6,#40 ; 2秒计数器(40×50ms)
MOV A,#00H ; 数据索引
MOV R7,#00H
MOV DPTR,#DATA_T ; 数据表指针
MOV PCON,#00H ; SMOD=0
MOV TMOD,#21H ; T1方式2,T0方式1
MOV SCON,#0D0H ; 串口方式3,REN=1
MOV TH1,#0FAH ; 4800波特率初值
MOV TL1,#0FAH
MOV TH0,#3CH ; 50ms初值
MOV TL0,#0B0H
SETB ET0 ; 允许T0中断
SETB ES ; 允许串口中断
SETB PS ; 串口高优先级
SETB EA ; 开总中断
SETB TR0 ; 启动T0
SETB TR1 ; 启动T1
SJMP $
INTP_T0:
MOV TH0,#3CH
MOV TL0,#0B0H
DEC R6
CJNE R6,#00,EXIT
MOV R6,#40 ; 重置2秒计数器
MOV A,R7
MOVC A,@A+DPTR ; 查表取数据
MOV P1,A ; 本地显示
MOV SBUF,A ; 发送数据
INC R7 ; 下一个数据
CJNE R7,#06,EXIT
MOV R7,#00H ; 循环发送
EXIT:
RETI
INTP_S:
JNB TI,EXIT ; 不是发送中断则返回
CLR TI ; 清除发送中断标志
RETI
DATA_T:
DB 00H,01H,0FEH,0EEH,04H,05H
END
; B机接收程序
ORG 0000H
SJMP MAIN
ORG 0023H
SJMP INTP_S
ORG 0030H
MAIN:
MOV P1,#00H ; 初始LED全灭
MOV PCON,#00H ; SMOD=0
MOV TMOD,#20H ; T1方式2
MOV SCON,#0D0H ; 串口方式3,REN=1
MOV TH1,#0FAH ; 4800波特率初值
MOV TL1,#0FAH
SETB ES ; 允许串口中断
SETB EA ; 开总中断
SETB TR1 ; 启动T1
LOOP:
MOV P1,A ; 显示接收数据
SJMP LOOP
INTP_S:
JNB RI,EXIT ; 不是接收中断则返回
CLR RI ; 清除接收中断标志
MOV A,SBUF ; 读取接收数据
EXIT:
RETI
END
波特率计算:
- 12MHz晶振,SMOD=0,波特率=2^SMOD/32 × fosc/12/(256-TH1)
- 4800 = 1/32 × 10^6 / (256-TH1) ⇒ TH1=250=0FAH
1.4.2 带应答的串口通信
实现A机发送数据,B机接收后加1回传,A机显示:
assembly复制; A机程序
ORG 0000H
SJMP MAIN
ORG 0003H
SJMP INTP0
ORG 0023H
SJMP INTP_S
ORG 0030H
MAIN:
MOV P1,#00H ; 初始LED全灭
MOV R7,#00H ; 数据索引
MOV DPTR,#DATA_T
MOV PCON,#00H ; SMOD=0
MOV TMOD,#20H ; T1方式2
MOV SCON,#0D0H ; 串口方式3,REN=1
MOV TH1,#0FAH ; 4800波特率初值
MOV TL1,#0FAH
SETB IT0 ; 外部中断0下降沿触发
SETB EX0 ; 允许INT0中断
SETB ES ; 允许串口中断
SETB EA ; 开总中断
SETB TR1 ; 启动T1
SJMP $
INTP0:
MOV A,R7
MOVC A,@A+DPTR ; 查表取数据
MOV SBUF,A ; 发送数据
INC R7 ; 下一个数据
CJNE R7,#06,EXIT
MOV R7,#00H ; 循环发送
EXIT:
RETI
INTP_S:
JNB TI,RECEIVE ; 不是发送中断则检查接收
CLR TI ; 清除发送中断标志
RETI
RECEIVE:
JNB RI,EXIT2 ; 不是接收中断则返回
CLR RI ; 清除接收中断标志
MOV A,SBUF ; 读取接收数据
MOV P1,A ; 显示数据
EXIT2:
RETI
DATA_T:
DB 00H,01H,0FEH,0EEH,04H,05H
END
; B机程序
ORG 0000H
SJMP MAIN
ORG 0023H
SJMP INTP_S
ORG 0030H
MAIN:
MOV P1,#00H ; 初始LED全灭
MOV PCON,#00H ; SMOD=0
MOV TMOD,#20H ; T1方式2
MOV SCON,#0D0H ; 串口方式3,REN=1
MOV TH1,#0FAH ; 4800波特率初值
MOV TL1,#0FAH
SETB ES ; 允许串口中断
SETB EA ; 开总中断
SETB TR1 ; 启动T1
SJMP $
INTP_S:
JNB RI,EXIT ; 不是接收中断则返回
CLR RI ; 清除接收中断标志
MOV A,SBUF ; 读取接收数据
MOV P1,A ; 本地显示
INC A ; 数据加1
MOV SBUF,A ; 回传加1后的数据
EXIT:
RETI
END
调试经验:
- 双机通信时要确保波特率、数据格式(数据位、停止位、校验位)一致
- 可以使用示波器测量实际波特率是否准确
- 长距离通信时建议加入校验机制(奇偶校验或CRC)
- 在干扰较大环境中可以加入数据包重传机制
2. 51单片机汇编开发实用技巧
2.1 调试技巧与常见问题
2.1.1 硬件调试技巧
-
电源问题排查:
- 确保电源电压稳定(5V±10%)
- 在电源引脚附近加0.1μF去耦电容
- 检查所有GND连接是否良好
-
复位电路检查:
- 确保复位引脚在上电时有足够长的低电平时间(通常>24个时钟周期)
- 检查复位按钮是否正常工作
-
晶振不起振:
- 检查晶振频率是否正确
- 检查负载电容是否匹配(通常22pF)
- 尝试更换晶振或调整电容值
2.1.2 软件调试技巧
-
使用仿真器调试:
- 单步执行观察寄存器变化
- 设置断点检查程序流程
- 观察内存和特殊功能寄存器值
-
软件模拟调试:
- 使用Proteus等仿真软件验证程序逻辑
- 观察IO口波形和定时器工作状态
-
日志调试法:
- 通过串口输出调试信息
- 使用LED显示状态码
2.2 性能优化建议
-
代码优化:
- 使用短跳转(SJMP、AJMP)替代长跳转(LJMP)
- 将常用变量分配在直接寻址区(00H-7FH)
- 使用位操作替代字节操作
-
延时优化:
- 使用定时器替代软件延时
- 在延时循环中使用NOP指令微调时间
-
中断优化:
- 中断服务程序尽量简短
- 使用中断优先级合理分配资源
- 避免在中断中进行复杂计算
2.3 扩展应用思路
-
多任务处理:
- 使用定时器中断实现简单的时间片轮询
- 通过状态机实现多任务调度
-
外设扩展:
- 通过IO口模拟I2C、SPI等接口
- 使用串口扩展更多功能
-
低功耗设计:
- 合理使用空闲模式和掉电模式
- 动态调整主频降低功耗
- 优化外围电路降低静态功耗
在实际项目中,我发现51单片机汇编虽然效率高,但开发效率较低。对于复杂项目,建议使用C语言开发核心逻辑,关键部分用汇编优化。掌握汇编语言的最大价值在于深入理解单片机工作原理,当遇到棘手问题时,能够从底层角度分析和解决。
