摘 要: 本文主要介紹凌陽16位變頻控制單片機SPMC75系列單片機地C和ASM(匯編)混合編程的應用。 關鍵詞:SPMC75 嵌入式匯編 1 引言 支持C語言幾乎是所有微控制器程序設計的一項基本要求,當然SPMC75系列單片機也不例外。μ'nSPTM 指令結構的設計就著重考慮對C語言的支持,GCC就是一種針對μ'nSPTM 操作平臺的ANSI-C編譯器。但是在應用中對于程序的設計,特別是C和ASM混合使用的時候有些地方是需要注意的,在C中如何嵌入ASM也是一個不可回避的問題。 2 芯片特性簡介 SPMC75系列單片機是由凌陽科技設計開發的16位微控制器芯片,其內核采用凌陽科技自主知識產權的μ'nSP微處理器。SPMC75系列單片機集成了能產生變頻電機驅動的PWM發生器、多功能捕獲比較模塊、BLDC電機驅動專用位置偵測接口、兩相增量編碼器接口等硬件模塊;以及多功能I/O口、同步和異步串行口、ADC、定時計數器等功能模塊,利用這些硬件模塊支持,SPMC75可以完成諸如家電用變頻驅動器、標準工業變頻驅動器、多環伺服驅動系統等復雜應用。下面介紹SPMC75系列單片機資源特性:
■ 高性能的 16 位 CPU 內核 - 凌陽 16 位 u'nSP 處理器 - 2 種低功耗模式: Wait/Standby - 片內低電壓檢測電路 - 片內基于鎖相環的時鐘發生模塊 - 最高系統頻率 Fck : 24MHz ■ 片內存儲器 - 32KW (32K × 16bit) Flash - 2KW (2K × 16bit) SRAM ■ 工作溫度: -40 ℃~ 85 ℃ ■ 10 位 ADC 模塊 - 可編程的轉換速率,最大轉換速率 100Ksps - 6 ~~ 8 個通道 - 可與 PDC 或 MCP 等定時器聯動,實現電機控制中的電參量測量 ■ 串行通 訊 接口 - 通用異步串行通訊接口 (UART) - 標準外圍接口 (SPI) ■ 可編程看門狗定時器 ■ 內嵌在線仿真電路 ICE 接口:可實現在線仿真、調試和下載 ■ 兩個 CMT 定時器 - 通用 16 位定時 / 計數器 | ■ MCP 定時器 - 能產生三相六路可編程的 PWM 波形,如三相 SPWM 、 SVPWM 等 - 提供 PWM 占空比值同步載入邏輯 - 可選擇與 PDC 的位置偵測變化同步 - 可編程硬件死區插入功能,死區時間可設定 - 可編程的錯誤和過載保護邏輯 - 16 位定時 / 計數器功能 ■ PDC 定時器 - 可同時處理三路捕獲輸入 - 可產生三路 PWM 輸出(中心對稱或邊沿方式) - BLDC 驅動的專用位置偵測接口 - 兩相增量碼盤接口,支持四種工作模式,擁有四倍頻電路 - 16 位定時 / 計數器功能 ■ TPM 定時器 - 可同時處理二路捕獲輸入 - 可產生二路 PWM 輸出(中心對稱或邊沿方式) - 16 位定時 / 計數器功能 ■ 封裝 - QFP 和 SDIP 兩種封裝, - 42 ~~ 80 腳系列 |
3 函數調用 3.1 調用協議 模塊代碼間的調用,是遵循μ'nSPTM體系的調用協議(Calling Convention)。所謂調用協議,是指用于標準子程序之間一個模塊與令一個模塊的通信約定。即使兩個模塊是以不同的語言編寫而成。 調用協議是指這樣一套法則:它使不同的子程序之間形成一種握手通信接口,并完成一個子程序到另一個子程序之間的參數傳遞和控制,以及定義出子程序調用與子程序返回值的常規規則。 調用協議包括以下相關要素: (1)調用子程序間的參數傳遞; (2)子程序返回值; (3)調用子程序過程中所用堆棧; (4)用于暫存數據的中間寄存器。 μ'nSPTM調用協議的內容如下: 1、由于C編譯器產生的所有標號都以下劃線(_)為前綴,而C程序在調用匯編程序時要求匯編程序名也以下劃線(_)為前綴。 2、參數以相反的順序(從右到左)被壓入堆棧中。必要時所有的參數都被轉換成其在函數原型中被聲明過的數據類型。但如果函數的調用發生在其聲明之前,則傳遞在調用函數里的參數不會進行任何數據類型轉換的。 3、各參數和局部變量在堆棧中的排列如圖3-1所示。 4、16-Bit的返回值放在寄存器R1中,32-Bit的返回值存入寄存器R1和R2中,其中低字節在R1中,高字節在R2中。若要返回結構或指針需要在R1中存放一個指向結構的指針。 5、編譯器會產生prolog/epilog過程動作來暫存或恢復PC、SR及BP寄存器。匯編器則通過CALL指令可將PC和SR自動壓入堆棧中,而通過RETF或RETI指令將其自動彈出堆棧。 6、編譯器所認可的指針是16-Bit的。函數指針實際上并非指向函數的入口地址,而是一個段地址的向量_function_entry,在該向量的兩個連續Word的數據單元存放的值才是函數的入口地址。 
圖3-1 程序調用的堆棧使用 3.2 舉例說明 ◆ C程序中調用ASM函數 【例3-1】 無參數傳遞的C語言調用ASM函數。 /*-------------------------------------------------------*/ /* C 程序 /*-------------------------------------------------------*/ extern void F_Function(void); main() { /*-------------------------------------------------------*/ /* C 程序調用 ASM 函數 /*-------------------------------------------------------*/ F_Function(); while(1){;} } /*-------------------------------------------------------*/ /* ASM 程序 /*-------------------------------------------------------*/ //============================================================= // ----Function: void F_Function(void); // -Description: ASM 函數 // --Parameters: none // -----Returns: none // -------Notes: none // -----Destroy: none //============================================================= .CODE .PUBLIC _F_Function _F_Function: .proc nop; RETF; .endp |
【例1-2】C 程序調用ASM函數,輸入兩個UInt16參數,返回一個UInt16參數。 /*-------------------------------------------------------*/ /* C 程序 /*-------------------------------------------------------*/ extern UInt16 F_F_Addition(UInt16 arg1,UInt16 arg2); main() { UInt16 uiErr=0; /*-------------------------------------------------------*/ /* C 程序調用 ASM 函數,輸入兩個 UInt16 參數, /* 返回一個 UInt16 參數 /*-------------------------------------------------------*/ uiErr = F_Addition(0x00F3,0x9F00); while(1){;} } /*-------------------------------------------------------*/ /* ASM 程序 /*-------------------------------------------------------*/ .CODE //============================================================= // ----Function: UInt16 F_Addition(UInt16 arg1,UInt16 arg2); // -Description: 兩數相加 // --Parameters: arg1 ,被加數; arg2 ,加數 // -----Returns: UInt16 兩數相加的和 // -------Notes: ASM 程序,示范參數傳遞及 UInt16 參數返回 // -----Destroy: R1 、 R2 //============================================================= .PUBLIC _F_Addition _F_Addition: .proc PUSH BP to [SP]; // 保護 BP 數據 ( 1 ) BP = SP+1; // 調整指針 ( 2 ) R1 = [BP+3]; // 第一個參數 ( 3 ) R2 = [BP+4]; // 第二個參數 ( 4 ) R1 += R2; // 通過 R1 返回結果 ( 5 ) POP BP from [SP]; ( 6 ) RETF; .endp |
如圖3-2所示程序調用時堆棧使用情況。通過圖可以清楚的看出在C調用ASM函數的時候,第一個參數將跟著第二個參數陸續自動的壓入堆棧;接下來是PC指針和SR寄存器在CALL指令執行后壓入堆棧,這些都是自動完成的,使用者只需要了解,是無法也沒有必要干預的。 在下來將跳入執行ASM函數,在執行語句(1)的時BP被壓入堆棧保護起來。那么可以發現ASM所需要接收的參數在堆棧中的實際位置,再執行語句(2)將當前堆棧指針加一賦給變址寄存器BP,則第一個參數的位置就應該是BP+3,第二個參數的位置為BP+4,可以通過語句(3)、(4)來取出參數。 結果的返回可以按照調用協議所講的保存在R1中來返回參數。 
圖3-2 程序調用時堆棧使用情況 【例1-3】C 程序調用ASM函數,輸入兩個UInt16參數,返回一個Uint32參數。 /*-------------------------------------------------------*/ /* C 程序 /*-------------------------------------------------------*/ extern UInt32 F_Multiplication(UInt16 arg1,UInt16 arg2); main() { UInt32 ulErr=0; /*-------------------------------------------------------*/ /* C 程序調用 ASM 函數,輸入兩個 UInt16 參數, /* 返回一個 Uint32 參數 /*-------------------------------------------------------*/ uiErr = F_ Multiplication(0xF0F3,0x0F00); while(1){;} } /*-------------------------------------------------------*/ /* ASM 程序 /*-------------------------------------------------------*/ .CODE //============================================================= // ----Function: UInt32 F_Multiplication(UInt16 arg1,UInt16 arg2); // -Description: 兩數相乘 // --Parameters: arg1 ,被乘數; arg2 ,乘數 // -----Returns: UInt32 兩數相乘的積 // -------Notes: ASM 程序,示范參數傳遞及 UInt32 參數返回 // -----Destroy: R1 、 R2 、 R3 、 R4 //============================================================= .PUBLIC _F_Multiplication _F_Multiplication: .proc PUSH BP to [SP]; BP = SP+1; R1 = [BP+3]; R2 = [BP+4]; MR = R1*R2,uu; R1 = R3; // 通過 R1 、 R2 返回一個 UInt32/Int32 數據 R2 = R4; POP BP from [SP]; RETF; .endp |
◆ ASM函數中調用C程序 在ASM函數中要調用C子函數,那么應該根據C的函數原型所要求的參數類型,分別把參數壓入堆棧后再調用C函數,以保證參數的正確傳遞。在調用調用結束后還需要進行彈棧,以恢復調用C函數前的堆棧指針。在這個過程中很容易產生bug,所以在使用的時候希望細心的處理。 【例3-4】ASM程序調用C 函數,輸入兩個UInt16參數,返回一個UInt16參數。 /*-------------------------------------------------------*/ /* ASM 程序 /*-------------------------------------------------------*/ .CODE .EXTERNAL _SP_Addition //C 函數 .PUBLIC _F_Dummy_Main _F_Dummy_Main: .proc PUSH R1,R2 to [SP]; // 寄存器保護 R2 = 0xA800; // 第二個參數 R1 = 0x00E9; // 第一個參數 // PUSH R1,R2 to [SP]; // 傳遞參數入棧 PUSH R2 to [SP]; // 第二個參數入棧 ( 1 ) PUSH R1 to [SP]; // 第一個參數入棧 ( 2 ) call _SP_Addition; // 調用 C 函數 ( 3 ) R1 = R1; // 函數返回值 ( 4 ) // SP + = 2; // 調整堆棧指針 ( 5 ) POP R1 from [SP]; ( 6 ) POP R2 from [SP]; ( 7 ) POP R1,R2 from [SP]; RETF; .endp /*-------------------------------------------------------*/ /* C 程序 /*-------------------------------------------------------*/ //============================================================= // ----Function: UInt16 SP_Addition(UInt16 i,UInt16 j) // -Description: C 函數,示范匯編調用 C 函數 // --Parameters: i , j :被加數和加數 // -----Returns: 兩數的和 // -------Notes: none //============================================================= UInt16 SP_Addition(UInt16 i,UInt16 j) { UInt16 sum = 0; sum = i+j; return(sum); } |
如圖3-3所示程序調用時堆棧使用情況。在ASM調用C的時候需要把堆棧調整成和C調用C函數的樣子,所以需要對參數的傳遞方式有個了解,按照圖3-3的形式來調整堆棧。 
圖3-3 程序調用時堆棧使用情況 4 嵌入匯編 為了使C語言程序具有更高的效率和更多的功能,需在C語言里嵌入用匯編語言寫的子程序。一方面,是為了提高子程序的執行速度和效率;另一方面,可以解決某些用C語言程序無法實現的機器語言操作。勿庸置疑,C語言代碼與匯編代碼的接口問題是任何C編譯器都要解決的問題。 通常有兩種方法可以將匯編語言代碼與C語言代碼聯合起來,一種是把獨立的匯編語言程序用C函數連接起來,通過API(Application Program Interface)的方式調用;另一種就是下面將要提到的在線匯編方法,即將直接插入匯編指令嵌入到C函數中。 采用GCC規定的在線匯編指令格式進行指令的輸入,是GCC實現將μ'nSPTM匯編指令嵌入C函數中的方法。GCC在線匯編指令格式規定如下: asm(匯編指令模板:輸出參數:輸入參數:clobbers參數); 若無clobber參數,則在線匯編指令格式可以簡化為: asm(匯編指令模板:輸出參數:輸入參數); 4.1 嵌入式匯編介紹 1、匯編指令模板 模板是在線匯編指令中的主要成分,GCC據此可以在當前產生匯編指令輸出。例如下面的一條在線匯編指令: asm("%0 += %1":"+r(foo):"r"(bar)); 其中:"%0 += %1"就是模板。操作數"%0"、 "%1"作為一種形式參數,分別會由第一個冒號后面實際的輸入、輸出參數取代。帶百分號后的數字表示的是冒號后參數的序號。例如: asm("%0 = %1 + %2":"=r(foo):"r"(bar), "i"(10)); "%0"會由參數foo取代,"%1"會由參數bar取代,而"%2"會由數值10取代。 在匯編輸出中,一個匯編指令模板里可以掛接多條匯編指令。其方法是用換行符"\n"來結束每一條指令,并可以用Tab鍵符"\t"將同一模板產生的匯編輸出中的各條指令的換行顯示時縮進到同一列,以使匯編指令顯示清晰。例如: asm("%0 += %1" \n\t "%0 += %1":"+r(foo):"i"(10)); 2、操作數 在線匯編指令格式中,第一個冒號后的參數為輸出操作數,第二個冒號后的參數為輸入操作數,第三個冒號后跟著的則是clobber操作數。在各類操作數中,引號里的字符代表的是其存儲類型約束符,括號里面的字符串表示是實際的操作數。 如果輸出參數有若干個,可以用逗號將每一個參數隔開。同樣,該法則適用于輸入參數或clobber參數。注意clobber參數只能是1、2、3和4中的一個或多個,但不能是全部。 3、操作符約束符 約束符的作用在于指示GCC,使用在匯編指令模板中的操作數的存儲類型。表1-1列出了一些約束符和它們分別代表的操作數不同的存儲類型,也列出了用在操作數約束符之間的兩個約束符前綴。 表 1-1 操作數存儲類型約束符及約束符前綴 約束符 | 操作數存儲類型 | 約束符前綴及含義解釋 | r | 寄存器中的數值 | = | + | m | 存儲器中的數值 | 為操作數賦值 | 操作數在賦值前先參加運算 | i | 立即數 | p | 全局變量操作數 |
4.2 應用舉例 【例4-1】利用嵌入式匯編實現對端口寄存器的操作。
//=================================================================== asm(".include Spmc75_regs.inc"); ( 1 ) //=================================================================== //------------------------------------- asm("[P_IOD_Attrib_ADDR] = %0 \n\t" \ ( 2 ) "[P_IOD_Dir_ADDR] = %0 \n\t" \ ( 3 ) "[P_IOD_Buffer_ADDR] = %0 \n\t" \ ( 4 ) "[P_IOD_Data_ADDR] = %1 \n\t" \ ( 5 ) : \ ( 6 ) :"r"(0xFFFF),"r"(0x0000) \ ( 7 ) :"1"); ( 8 ) //------------------------------------- |
在C的嵌入式匯編中,當使用端口寄存器時,需要在C文件中加入匯編的包含頭文件,(1)所示。那么可以使用端口寄存器的名稱,而不必去使用端口的實際地址;(2)、(3)、(4)和(5)分別對端口寄存器的各個屬性賦值初始化;(6)沒有輸出參數;(7)操作數%0=0xFFFF,%1=0x0000,操作數的存儲類型都是寄存器中的數值;(8)clobber參數,在寄存器傳遞實參的時候不能使用寄存器R1。 【例4-2】利用嵌入式匯編實現對端口寄存器的位值讀取。 A . //------------------------------------- asm("r1 = %1; \n\t" \ ( 1.a ) "tstb [r1],%2; \n\t" \ ( 2.a ) "jz 2; \n\t" \ ( 3.a ) "%0 = 0x01; \n\t" \ ( 4.a ) "jmp 1; \n\t" \ ( 5.a ) "%0 = 0x00; \n\t" \ ( 6.a ) :"=r"(result) \ ( 7 ) :"i"(P_IOD_Buffer),"i"(14) \ ( 8 ) :"1","2"); ( 9 ) //------------------------------------- B . //------------------------------------- // GCC inline ASM start r1 = 28793; ( 1.b ) tstb [r1],14; ( 2.b ) jz 2; ( 3.b ) R3 = 0x01; ( 4.b ) jmp 1; ( 5.b ) R3 = 0x00; ( 6.b ) // GCC inline ASM end |
上面A、B分別是嵌入式匯編和實際編譯出來的代碼。首先需要清楚一點%0=i、%1=P_IOD_Buffer、%2=14,通過(7)和(8)行可以了解。(1.a)將端口IOD的地址存放到R1中;(2.a)測試IOD的14位;(3.a)如果等于零跳過兩行,即跳過(4.a)和(5.a)在(6.a)中為輸出參數賦值0x00;如果不等于零則順序執行(4.a)為輸出參數賦值0x01;(5.a)跳過一行,即跳過(6.a)。通過上面的過程可以應用嵌入式匯編實現對端口位的測試,將測試的結果保存在變量result中,行(7)所示。行(9)clobber參數,約束行(7)的"r"在編譯時不能使用R1和R2,所以可以在(4.b)和(6.b)中看到使用了R3。但如果行(9)是":"1","2","3");",那么編譯出來的(4.b)和(6.b)中只能使用R4,由此可知":"1","2","3","4");"是絕對不允許的。 【例4-3】典型的應用方式。 通常的應用是用宏匯編的形式定義出來,使用的時候就象函數一樣來使用。 //================================================================ //Function: SETB Function //Example: SETB(_P_IOA_Data,0x8); //================================================================ #define SETB(Addr,Num) \ asm( \ "r1=%0;\n\t" \ "r2=%1;\n\t" \ "setb [r1],r2\n\t" \ : \ :"i"(Addr),"i"(Num) \ :"1","2" \ ); SETB ( P_IOD_Data , 14 ); // 置位 IOD14 SETB ( P_IOB_Data , 10 ); // 置位 IOB10 |
5 參考文獻 【1】SUNPLUS SPMC75編程指南 www.sunplusmcu.com 【2】北京航空航天大學 《凌陽16位單片機應用基礎》 羅亞非 |