51单片机汇编:用Keil C51模拟器的UART#1窗口查看串口输出[系列教程之12]
-
该系列主仓库地址:https://gitee.com/langcai1943/8051-from-boot-to-application
-
-
本工程及源码内容介绍:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 介绍: 【51单片机汇编】用Keil C51模拟器的UART#1窗口查看串口输出
; NOTE: File format: UTF-8
; 备注: 1、本工程默认使用Keil模拟器运行,无需硬件;按F7编译后,按Ctrl+F5运行,
; 调出UART #1窗口(要先运行程序):点击软件的View-->Serial Windows-->UART #1,
; 在Keil软件的UART #1窗口中看串口的输出结果为Hello world!....;
; 2、使用C语言从Keil模拟器输出串口信息的步骤详见我的另外两篇介绍:
; https://gitee.com/langcai1943/8051-from-boot-to-application#1hello-world%E8%BE%93%E5%87%BA
; https://gitee.com/langcai1943/8051-from-boot-to-application#5%E7%94%A8%E6%B1%87%E7%BC%96%E4%BB%8Ekeil%E8%B0%83%E8%AF%95%E7%AA%97%E5%8F%A3%E4%B8%AD%E8%BE%93%E5%87%BAhello-world
; 作者 将狼才鲸
; 日期 2023-06-11
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- 部分源码展示:
;;
; 包含头文件和其它源文件,也可以用#include <REGISTERS51.INC>,同样有效;
; 被包含的文件不要用END指令放在文件尾,否则编译器会报错
$INCLUDE(REGISTERS_51.INC)
$INCLUDE(START.ASM)
$INCLUDE(IRQ.ASM)
ORG 0100H ; 1、RESET函数从RAM 0x100地址开始,将0xFF之前的RAM留给堆栈,
; 堆栈之前是中断入口地址;
; 2、注意:RAM地址(包含中断入口地址)和寄存器地址是分开的,它们地址
; 重叠但含义不同,并不会相互干扰
; 标号以冒号:结尾,兼具C语言的标号和函数名的功能;可以跳转到标号处
;;
; 功能:类似于C语言的main()函数,程序主入口
; 参数:无
; 返回:无
;;
RESET:
NOP ; 空指令,消耗一个时钟周期的时间,什么也不做
; MOV IE, #00H ; 屏蔽所有中断;MOV指令类似于C语言的=赋值,IE = 0x00;
CLR RS0 ; RS1 RS0的值为00时,选择R0~7的通用寄存器为4组中的第0组;
; 清零一个位,标准C语言里没有直接的位操作,需要PSW &= ~RS0_MASK;(仅展示逻辑)
CLR RS1
; MOV SP, #0C0H ; 程序运行开始时需要对SP堆栈地址进行设置,一般设置到80H之后,
; 给堆栈留足空间,但是又不占用中断入口地址
MOV SP, #3FH ; 当前本工程用的AT89C51芯片默认工程,未设置外部RAM配置,中断占的地址少,
; 而内部RAM只有0x80大小,所以设置到0x3F
LCALL UART_INIT ; 1、调用函数,C语言中类似的用法是your_func();
; 2、LCALL和RET成对使用,跳转前会将当前地址放到堆栈,而LJMP遇到
; 遇到RET也会返回,但会返回最开始的地址,或者返回到不知道哪里去了;
; 3、单步执行到LCALL位置的话,想继续进入子函数,需要按F11,不能再按F10,
; 按F10会一次性执行完子程序,并单步跳过子程序
LCALL UART_PRINT
LOOP: ; 汇编函数不遇到RET指令的话会一直往后执行,所以RESET这个主程序会进入下面的死循环
NOP
LJMP LOOP
RET
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
UART_INIT:
; SCON: D7~D0: SM0 SM1 SM2 REN TB8 RB8 TI RI
; 通讯设置 接收使能 数据位9 收发中断
;MOV SCON, #50H ; 1、1个起始位0,8个数据位和1个停止位1,接收使能;
; 2、当前这个配置好像不对,还有波特率什么的也没配,直接用这条语句
; 反而Keil模拟器没有串口输出
; 3、Keil使用模拟器调试时,不初始化串口也能从串口收发数据,所以屏蔽这条
RET ; 汇编中函数返回
INFO_PRINT_1: ; 类似于字符串常量宏定义
DB 'Hello world!....' ; DB类似于C语言定义一个常量,当前总共16字节数据
INFO1_LEN EQU 10H ; 类似于#define INFO1_LEN 0x10
;;
; 函数功能: 串口发送字符串数据
; 参数: 无,使用全局变量INFO_PRINT_1
; 返回值: 无
;;
UART_PRINT:
; 局部变量R0,类似于C语言中for循环中的i;
; A,类似于地址中的数据buf[i];
MOV A, R0 ; 将通用寄存器R0存到ACC累加器,累加器调用时可以用名字ACC,可以用名字A,
; 写程序时常用ACC作一个变量,类似于C语言的int tmp,然后使用tmp
PUSH ACC ; 1、压栈R0,防止之前别人有用通用寄存器,先压栈报错,别破坏别的
; 中断或函数已经存过的值;
; 2、R0~R7相当于C语言定义了int tmp0; ~ int tmp7;七个全局变量,每个函数
; 都很少使用局部变量,而是使用这些全局变量,因为R0~R7速度快,因为大家都用,
; 所以函数进入和退出要压栈推栈;你只需要知道哪些会被别人用,和本函数会用
; 哪些R0~R7之间的全局变量,然后处理对应的R0~R7寄存器即可;
; 3、C语言的函数执行也有压栈弹栈操作,不过是编译器自动完成的,所以
; 嵌入式编程时还需要注意堆栈大小,不能在函数内定义超出堆栈长度的数组,
; 否则程序会跑飞;
MOV R0, #00H ; 类似于:int i = 0;
PRINT_LOOP_1:
MOV DPTR, #INFO_PRINT_1 ; 1、把字符串指针放入DPL DPH数据指针中,DPTR是两个寄存器合在一起的名字
; 2、不像C语言定义一个char *buf; 可以直接使用buf里的数据,
; 汇编使用缓存或数组地址时要先放进DPTR,DPTR就类似于buf地址;
; 3、立即数常量的使用前加#号,这种固定用法和指令集有关
MOV A, R0 ; for (int A = 0;... 此时将A寄存器作为类似C语言里循环的i变量
MOVC A, @A + DPTR ; A = *(A + DPTR), 整条语句是一条固定的指令,这里的@是一个固定的用法
; MOVC是查表指令,也是类似于C语言的=赋值,还有@取址的效果;
; 类似于C语言的for(...i++) {
A = buf[i];}
LCALL UART_SEND ; 1、从串口发送1个字节,串口收发中断都是一个字节一次,不像有些32位
; CPU的串口模块,串口中断来时会携带长度信息,一次收多字节数据;
; 2、传入参数为A: 要发送的一个字节;
INC R0 ; R0++,类似于C语言中for循环的i++
CJNE R0, #INFO1_LEN, PRINT_LOOP_1 ; 1、比较累加器和立即数,不相等则转移
; 2、立即数宏定义的使用前加#号,这种固定用法和指令集有关
POP ACC ; 弹栈
MOV R0, A ; 将压入的值还原
RET ; 函数返回
;;
; 功能:串口发送1字节数据(非中断模式,是轮询模式)
; 参数:A 1字节数据
; 返回:无
;;
UART_SEND:
MOV SBUF, A
LB_SEND_WAIT: ; 等待最后一个bit发送完成
MOV A, SCON
ANL A, #02H ; 查询发送中断标志,一个字节是否发送完成;A &= (0x1 << 1); 从0算起的第1bit
JZ LB_SEND_WAIT ; A为0则跳转,if (A == 0) continue
ANL SCON, #0FDH ; C语言:SCON &= 0xFD,也就是SCON &= ~(0x1 << 1),清除发送中断标志
RET
END