天天看點

單片機通識之UART通信協定(如何用彙編實作)晶片簡介代碼實作結語

目錄

  • 晶片簡介
  • 代碼實作
    • 封裝"IO口狀态轉換"為宏
    • 初始化IO口
    • 傳輸資料
    • 傳輸字元串
  • 結語

晶片簡介

本文使用一款 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對時間同步非常苛刻。有時多一條指令都會影響到功能的實作。

此外,按照此思路,也可以将晶片中的其他資訊列印出,比如說建表中的内容,寄存器的值等。