芯片简介
本文使用一款 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位起始位(下降沿) + 8位数据位 + 2位停止位(上升沿)的传输方式,没有校验位。
- 波特率为2M bsp,即每秒可以传输 2000000 bit 数据,也就是说 每 500纳秒(1/2M)要发送一个脉冲信号(每一个脉冲信号发出后,要维持500纳秒的时间)。
- 芯片频率为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对时间同步非常苛刻。有时多一条指令都会影响到功能的实现。
此外,按照此思路,也可以将芯片中的其他信息打印出,比如说建表中的内容,寄存器的值等。