SPMC75系列单片机地C和ASM(汇编)混合编程的应用
2009-09-19
SPMC75系列单片机地C和ASM(汇编)混合编程的应用
本文主要介绍凌阳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