单片机通识之UART通信协议(如何用汇编实现)

芯片简介

本文使用一款 12MHZ、单指令周期(指令周期为1/13微秒)的芯片。在继续阅读文章之前,请先查阅如下段落并了解其指令、寄存器及其他说明。

AR:16位立即数寄存器,可赋值一个16位的数值
eg:	AR		= 0x0000

AX:16位立即数寄存器,可赋值一个16位的数值
eg:	AX		= 0x0000

CX:16位立即数寄存器,可赋值一个16位的数值,配合LOOP指令使用,实现循环
eg:	CX		= 0x0006	;; 循环7次
		Your_Label:
				AR		= 0x0001
				LOOP	Your_Label		;; 每遇到LOOP指令,CX会自动 -1,当CX < 0时,会跳过此条指令向下运行
				
				JMP		Your_Label_1

P0:16为立即数寄存器,用于获取ROM中某个地址,配合 PM[] 使用,取出该地址中的值
eg:	P0			=	#Your_Label
		AR			= PM[P0]
		;; 此时的AR将变为 0x0001, 即取出P0指向地址中的值
		
		Your_Label:
				DW	0x0001, 0x0002 

PUSH:将该寄存器数值入栈保存
eg:	PUSH	AR
POP:出栈并将数值保存至该寄存器
eg:	POP		AR

JEQ:当结果为0时跳转(本文中仅理解为AR为0时跳转)至该标签
eg:	AR		= 0x0000
		JEQ		Your_Label;; 此时跳转至 Your_Lable
		AR		= 0x0001
		JEQ		Your_Label;; 此时不跳转,向下运行

JAC:C标志位是一个系统寄存器中的某一位,其作用是判断AR等寄存器是否溢出。
	 此外左移、右移指令会将移出的一位存储在C标志位中。JAC指令根据C标志位跳转,1-跳转;0-不跳转
eg:	AR		= 0x0001
		SRA		AR				;; SRA为右移指令,将最低位移出到C标志位,其余位依次向右移动1位
		JAC		Your_Label		;; 最低位为1,右移到C标志位,C标志位为1,则跳转。

JMP:无条件跳转
eg:	JMP		Your_Label

CALL:调用该函数
eg:	CALL	Your_Label

XCHG:互换寄存器保存数值的高低位
eg:	AR		= 0x1234
		XCHG	AR				;; 此时AR的值变为 0x3412

MSTR:将字符串转变为对应的ASCII值,类似于建表的操作。MSB格式。这一指令是该款芯片方提供的操作,具体可以看自己使用IC是否有这一操作。如果没有的话,可以采用建表的方式实现字符串的转换。
eg:	MSTR	"ABCD"
		则对应 0x4142, 0x4344

在本文中仅讲解了如何理解并实现UART,UART 具体是什么请参考另一位作者的 概念理解

  1. 我采用 1位起始位(下降沿) + 8位数据位 + 2位停止位(上升沿)的传输方式,没有校验位。
  2. 波特率为2M bsp,即每秒可以传输 2000000 bit 数据,也就是说 每 500纳秒(1/2M)要发送一个脉冲信号(每一个脉冲信号发出后,要维持500纳秒的时间)。
  3. 芯片频率为12MHZ并且它是一个单指令周期芯片,即芯片运行一条指令的时间约为83纳秒,也就是说在发送一个脉冲信号后,要维持该脉冲信号约6个指令周期(下文称为Tw)后,才可以发送下一个脉冲信号。

代码实现

封装"IO口状态转换"为宏

// 将通信IO口设置为输出口
MACRO   M_UART_SET_PIN_OUTPUT
        SET         IO[IOC_PA].B0
        NOP 		// 延迟一个指令周期(80nS)
ENDM

// 设置IO口输出高电平
MACRO   M_UART_SET_PIN_HIGH
        SET         IO[PORTA].B0
ENDM

// 设置IO口输出低电平
MACRO   M_UART_SET_PIN_LOW
        CLR         IO[PORTA].B0
ENDM

初始化IO口

// 此函数通过CALL调用
UART_Init:
		// 调用宏将 IO口 设置为输出口
        M_UART_SET_PIN_OUTPUT
        
        // 调用宏将 IO口 设置为输出高
        // UART协议中,在不传输数据时,数据线一直要处于高电平状
        // 此外 开始信号是下降沿开始(即高电平转换到低电平),所以初始化时将数据线设置为高电平
        M_UART_SET_PIN_HIGH
        
        RETS 		// 退出函数调用,跳转到调用该函数的下一个地址处

传输数据

在传输数据时,一定要特别注意脉冲信号稳定时间(Tw)问题。2M的波特率决定了每个脉冲信号要维持500纳秒的时间,才可以继续传输下一个脉冲信号。而每一条指令的执行都需要花费一定时间,在计算等待时长时要特别注意!

// ====================================================================================================
// Function:        Print_Byte
// Description:    	Send 8bits through the serial port 
// Input:           AR
// Output:          None
// ====================================================================================================
Print_Byte:
        PUSH        CX 			;; 代码片段中使用到了CX寄存器,为防止该片段对其他程序的影响,保存CX的值,在退出该片段时在恢复CX的值
        DSI			INT0		;; 关闭中断,由于芯片的限制,串口只能写在主循环中,为了保证时间的准确,要暂时关闭中断,每中断一次,都会影响到主循环中的串口发送
		
        ;; Start signal
        M_UART_SET_PIN_HIGH		;; 先将 IO 口输出高,等待IO稳定后再开始传输数据
		CALL		UART_Delay	;; 根据自己的IC写出的一个延时函数,这个函数实现了500ns的延时
		
        M_UART_SET_PIN_LOW		;; 开始信号,下降沿后维持500ns的时间。
        ;; 注意下方标记的(1) ~ (5),这五条指令已经占据的5个指令周期也就是500ns,所以不需要再调用 UART_Delay 函数进行延时。
        ;; 这就是上文中提到的"要注意的等待时长问题",在写各种通信协议时,一定要注意这些时序问题,哪怕是1条指令的差距都会影响到数据得传输。
        NOP						;; (1) NOP 是一条空语句,相当于等待一个指令周期的时间,什么都不做
        
        CX          = 0x0007 	;; (2)			;; 循环8次发送8位数据
        
        JMP         __Print_Var_Loop__ ;;(3)
        
__Print_Var_Loop__:
        SRC         AR 			;; (4) 右移数据,将最低位放在 C 标志位中,根据 C标志位 进行跳转
        JAC         __Print_Var_Set_High__	;; (5) 如果最低位是1,则发送高电平,跳转
		;; 否则最低位是0,则发送低电平,向下运行

__Print_Var_Set_Low__:
        M_UART_SET_PIN_LOW		;; IO口 输出低电平,同时仍要注意 "要注意的等待时长问题",查指令的个数
        
        JMP         __Print_Var_Set_Confirm__
        
__Print_Var_Set_High__:
        M_UART_SET_PIN_HIGH		;; IO口 输出高电平,同时仍要注意 "要注意的等待时长问题",查指令的个数
        
        JMP         __Print_Var_Set_Confirm__
        
__Print_Var_Set_Confirm__:
        NOP
        LOOP            __Print_Var_Loop__	;; 循环8次发送8位数据
        
        ;; 这里 NOP 也是 "要注意的等待时长问题",有兴趣的可以想一想为什么要这样做
        NOP 
        NOP 
        
        ;; 至此,发送完了8位数据,要发送一个停止信号,即将 IO拉到高电平。
        M_UART_SET_PIN_HIGH
        PCH             = UART_Delay
        CALL            UART_Delay
        PCH             = UART_Delay		;; 这里等待两个脉冲信号的时间,是因为停止位越多,容错性就越强,所以我用了两个停止位。
        CALL            UART_Delay			;; 不过这一说法可能有争议性,并不绝对
        
        JMP             __Print_Var_Exit__

__Print_Var_Exit__:
        POP         CX 		;; 恢复CX寄存器
		ENA			INT0 	;; 恢复中断的运行
		ENI       	
        RETS 

传输字符串

;; MACRO 是创建宏的指令,在这里将功能封装为宏使用更方便,可以像 C、py、java等函数一样传入参数。
MACRO	M_PRINT Content 
__Print_Str_Start__:
		MSTR		Content						;; 将传入的字符串转换为ASCII码,会自动在末尾添加 0x0000,用于判断是否发送完毕
		P0			= #__Print_Str_Start__		;; 获取字符串的首地址
		
__Print_Str_Loop__:
		AR			= PM[P0++]					;; 取出P0指向地址的值,并且将 P0 +1
		PUSH		AR 							;; 保存AR的值
		
		XCHG		AR 							;; 串口发送数据从最低位发送,假如数据是 AB,那么发送的数据顺序是BA,所以此处将AR高低位互换,保证顺序的正确
		AX			= 0x00FF					;; 判断是否是 0x00,继而判断字符串是否到了末尾
		AR			&= AX 
		JEQ			__Print_Str_Exit__			;; AR 为0表示字符串到了末尾,结束发送
		
		PCH			= Print_Byte				;; 否则调用函数发送低八位
		LCALL		Print_Byte
		
		POP			AR 							;; 取出保存的AR值,发送剩下的8位数据,同样判断是否到了字符串末尾
		AX			= 0x00FF
		AR			&= AX 
		JEQ			__Print_Str_Exit__
		
		PCH			= Print_Byte
		LCALL		Print_Byte
		
		JMP			__Print_Str_Loop__			;; 跳转,循环

__Print_Str_Exit__:
ENDM

结语

在编写串口通信时,一定要特别注意时序的问题,UART对时间同步非常苛刻。有时多一条指令都会影响到功能的实现。
此外,按照此思路,也可以将芯片中的其他信息打印出,比如说建表中的内容,寄存器的值等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值