松翰C语言编程指导C+Program+Guide

更新时间:2023-09-14 11:21:02 阅读量: 初中教育 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

TDxxxV1.0

如何用C来完成SN8系列芯片的程序设计

松翰 易爱华

松翰科技(深圳)有限公司 SONIX TECHNOLOGY CO.,LTD.

1

TDxxxV1.0

内容概览

一、C 与Assembly简述

二、SN8 C studio 的安装与使用

2.1 SN8 C studio 的安装 2.2 SN8 C studio应用实例

三、数据类型与运算

3.1 专有数据类型

3.2 常量与变量

3.3 数据的存储类型与存储结构

四、程序流程控制

4.1 顺序结构

4.2 分支判断结构 4.3 循环控制结构

五、函数

5.1 函数的定义

5.2 函数参数传递与返回值 5.3 函数参数与全局变量

六、结构体、联合在SN8 C程序中的应用

6.1 结构体

6.2 联合

七、中断

7.1 中断函数的定义 7.2 中断过程的分析 7.3 中断函数的结构

八、位操作

8.1 位的定义 8.2 位的运算

8.3 位比较在程序流程控制中的应用 九、内嵌汇编

9.1 如何内嵌汇编

9.2 内嵌汇编时变量的传递

十、程序结构

10.1 主函数与子函数 10.2 构建可复用文件

10.3 构建具有实用性的程序

2

TDxxxV1.0

一、C 与Assembly简述

Assembly(汇编)的历史已经有半个多世纪了,从计算机的发明那天开始,汇编就注定要与其结缘,它作为第一种真正意义上的编程语言,在计算机的发展过程中具有无可替代的地位。

汇编具有与机器语言一一对应的高效率,就是由于汇编的高效率,和其紧贴硬件的特性,使其在半个世纪之后的今天依然是目前开发单片机程序的主流工具之一。但是由于编写汇编代码的工作量和难度都比较高,并且要求程序员对硬件必须有较深刻的了解,其入门就相对较难。而汇编的写法各异,不同的程序员编写的代码有着巨大的差异,这就带来了学习的难度,可读性差也成为了其推广的瓶颈,程序的维护更加艰难。随着程序复杂性的增加,开发团队的形成,应用而生的高级语言就逐渐取代了其位置。

C就是高级语言中针对硬件操作最优秀的一种,C也是基于汇编的,其相互关系如图所示。随着人们不断的对C进行改善,使C的效率得到了很大的提升,其编译效率逐渐的逼近汇编。而C以其模块化的编程模式,简洁的代码,良好的可读性和可维护性,成为了另一种单片机开发的主流开发工具,相比汇编而言,其具有无可比拟的优异性。

C代码 汇编代码 机器语言

图1、C与汇编的关系

SN8 C 是基于标准C而又加入SONIX SN8 系列芯片特征的专用C,它面对的是SONIX SN8 系列芯片程序开发,它能产生非常简洁的能直接运行于SN8 系列芯片的优化代码。具有良好的兼容性,易学易用,既方便客户开发,又继承了原有汇编的卓越性能。

在本文当中,我们将结合汇编来讲述C开发的各方面知识,以期通过对比来加深对芯片的编程应用的了解,同时,对熟悉C的程序员来说,可以更加深入了解芯片;而对于熟悉汇编的程序员来说,我们可以从对比中找到从汇编到C的转换方法。

3

TDxxxV1.0

二、SN8 C studio 的安装与使用

SONIX SN8 系列的芯片是RISC内核的高性能芯片。目前,由SONIX提供的SN8 Assembly仅有59条指令,是一种高效的汇编语言,有S8ASM和M2ASM编译器分别支持其1系和2系芯片。在此基础上的SN8 C studio整合了汇编和C的编译器,

1.1、 SN8 C studio 的安装

在得到SN8 C studio的安装包后,按照标准的Windows程序安装方法安装就可以将软件安装到机器上。

1.2、SN8 C studio应用实例

一、 创建一个工作区

与VC类似,我们的工程管理模式是以工作区方式来管理,在你准备一个项目的时候, 你首先创建一个属于这个项目的工作区:

1、打开IDS->文件->新建,单击出现如下界面:

4

TDxxxV1.0

图2-1 新建对话框

2、单击workSpace 图标,进入workspace创建窗口,单击Location 编辑框后的按钮,选择你需要的工作目录,然后输入新建Workspace的name,在这里如下:

图2-2 新建Workspace对话框

3、单击OK,IDS 的界面生成了工作区的工作界面,出现了Workspace窗口和Output窗口。同时,打开目标文件夹,你会发现在你选定的文件夹下面,生成了一个以Workspace Name 命名的新文件夹,打开文件夹你可以看到一个新生成的.wsp文件,这就是新生成的Workspace 的配置文件:

图2-3 新生成的Workspace配置文件

二、 新建一个工程(Project)

在我们成功创建一个工作区后,你会发现我们的Workspace 窗口中的Projiect数目为0,接着我们就要依据项目所用的芯片母体来创建一个工程项目了,跟创建Workspace相同,打开菜单中的 文件->新建,单击出现新建窗口,此时默认为新建Project。在右边的Project List窗口中选择你要使用的芯片母体和要创建的工程类型(一般为普通项目),此时的目标路径Location 中已经显示为你刚才建立的Workspace目录,建议将工程保存在这个目录下,不修改默认路径。给新建工程取一个有意义的Name,在框中输入。这里我们取与Workspace相同的名称,新建一个2700系列芯片的普通项目工程,设置如下图所示: 1、单击文件->新建,选择合适的选项:

5

TDxxxV1.0

图2-4 新建Project

2、单击OK。

3、新工程选项配置,在确认建立工程后,出现Project Setting 对话框,左边的Project列表中默认选中我们刚刚建立的工程,在这里我们只修改Chip 和Code Option项目下的选项,如图所示:

图2-5 Project Setting 对话框

6

TDxxxV1.0

4、选择正确的芯片: 在Chip页中,Definition文本框中显示系统依据你选定的芯片母体系列而确定的需要调用的芯片定义文件。Chip List 表列出了当前版本的IDS所支持的这一系列的各个芯片,选中其中你需要使用的芯片母体,Selected 文本框和Description文本框相应会自动改变。如图:

图2-6 选择芯片

5、设置正确的Code Option选项和ICE MODE:

ICE MODE的设置在SN8P1xxx芯片中是用于选择是否是ICE仿真模式,在Code Option列表中自动显示选定芯片母体的Code Option 选项,在Option Value项的默认值上单击,出现一个下拉列表,单击选择合适的选项。如图:

图2-7 设置正确的Code Option

7

TDxxxV1.0

6、单击OK,系统生成一个没有文件的工程,在Workspace 管理窗口中我们可以看见当前生成的工程为Working project,正处被激活状态(工程名称为加粗字体):

图2-8 新建Project的状态被激活

打开相应的文件夹,我们会发现系统生成了很多个新的文件,其中的.prj文件即为工程文件,其他的文件为根据我们刚才的设置和不同的芯片生成的配置文件及头文件。

三、 新建文件

完成项目的新建后,我们发现项目管理器source文件夹下是空的,没有任何文件,文件就是我们编程的主要工作了!

1、打开文件->新建菜单,单击出现我们前面看过很多次的New对话框,不过比前面多了一个选项——Files,选择New列表中的Files选项,单击按钮,对话框如图所示:

图2-9 新建文件对话框

2、在File List中选中需要创建的文件类型,这里我们创建一个C Source File,给文件命名从Name 编辑框中输入,存放的地点为刚才的设定位置,默认不做修改。

3、单击OK按钮,IDS打开一个编辑窗口,最大化,显示为刚刚建立的文件名的页,系统允许我们在这里进行程序的编写。

四、 编写程序

8

TDxxxV1.0

在新建一个文件后,系统自动打开编辑器,并打开一个由用户命名的空文档,它与普通文档相比较没有什么不同的之处,只不过它可以对C的关键字进行高亮显示。在新的文档里编辑程序。

C本身就是一种模块化的编程语言,SN8C的编程保持C 语言的编程风格。 SN8C所具有的Non-ANSI C的特性请参看其他章节,在此不再做描述。 下面是一个简单的C程序举例。 例2:

/****************************************************************** *

* File Name : SN8C_Ex.c * Post By : Dragon.Yi * Date : 2005/09/23 * Test History : V1.00.220 * describe:test 2708 interrupt

*

*******************************************************************/ #include struct word{ unsigned fint:1; unsigned :7; }intword;

unsigned int tc0cvalue=0x64; unsigned int accbuf = 0x00; unsigned int pflagbuf = 0; __interrupt intserv(void) {

_bCLR(&INTRQ,5); TC0C = tc0cvalue;

//The data will auto store!

intword.fint = 1; }

void initIO(void); void initINT(void);

void main(void) { STKP=0x07;

initIO(); initINT(); while(1) {

if(intword.fint!=0) {

P1=0x00;

9

TDxxxV1.0

}

} else { }

P0=0xff; P1=0xff; P2=0xff; P3=0xff; P4=0xff; P5=0xff; P2=0x00; P3=0x00; P4=0x00; P5=0x00; P0=0x00;

}

void initIO(void) {

P0M=0xFF; P1M=0xFF; P2M=0xFF; P3M=0xFF; P4M=0xFF; P5M=0xFF;

}

void initINT(void) {

INTRQ=0x00; INTEN=0x00; TC0M=0x00; TC0M=0x20; TC0C=0x64; _bCLR(&INTRQ,5); _bSET(&INTEN,5); _bSET(&TC0M,7); _bSET(&STKP,7);

}

注:程序只作为程序架构提供参考!

五、 调试程序

10

TDxxxV1.0

dw 0000h dw 0ae1fh dw 0ae2fh dw 0ae3fh dw 0ae4fh dw 0ae5fh dw 0ae6fh

;A-1 ; ; ; ; ;

dw 0ae7fh ;

我们可以看到,汇编的表是用DW关键字定义一个word,它是存放在.code段里面的,通过表头地址来得到每一个相对应的值。

那么在SN8 C 里面又如何来处理这些表呢?

在讲到表的处理之前,必须先提一提变量定义关键字的问题。SN8 C定义一个变量时,可以指明它所放置的地方(RAM或ROM),分别用关键字__RAM和__ROM来指定存放的地点,如:

Unsigned int __RAM ramVeriable; __RAM unsigned int ramVeriable2; Unsigned int __ROM romVeriable; __ROM unsigned int romVeriable2;

我们可以知道,当一个量放到了ROM当中就没法改变它的值了,其实就是我们说的常量。 在C当中,可以通过一个头名称来访问的变量类型比较多,其中数组是比较方便的一种,我们可以通过定义一个数组来存储这些表的数值,然后通过对数组的访问来查询对应的值。

如:

unsigned long __ROM disp_automenu[]= {

0x0000,0x0ae1f,0x0ae2f,0x0ae3f,0x0ae4f, 0x0ae5f,0x0ae6f,0x0ae7f

//显示菜单用第二数字表格

//将变量存放在RAM中 //将变量存放在ROM中

};

这是一个与上面的汇编表完全相同的表,我们将它存放在__ROM中,通过调用数组来查表,这在后面将详细介绍。

而在程序中还有另外一类的量是会在程序中不断被改变的,比如程序中的计数器,状态寄存器等等都会随着程序的运行而改变。我们将这一类量称之为变量。 我们先来看看汇编的定义变量的方法:

.DATA

org temp1 temp2 temp3 temp4 led_dp step

0h

ds 1 ds 1 ds 1 ds 1 ds 1 ds 1

;当前状态

16

TDxxxV1.0

上面的代码定义了temp1、temp2、temp3、temp4、led_dp、step6个变量,它们分别占用一个Byte的RAM空间,那么程序当中就可以通过变量名对该变量的空间进行读写。当然在汇编中你也可以用一个变量名来访问两个或多个RAM空间,这类似于查表的操作,其定义如下:

Job_mode

Power_mode

ds 2 ds 4

对上面的job_mode 变量可以通过job_mode 和job_mode+1来读写定义的两个存储单元,以此类推power_mode 或其他多个RAM空间定义的变量可以通过相同的方法来定义。可见,SN8ASM定义变量的方法主要是通过DS关键字来申请需要的变量空间,空间一旦被申请,就在整个程序流程里面被占用,也就是说定义一个变量就少一个空间,这对于RAM本身就很少的单片机而言,不能不说是一种浪费。

那么SN8C又是如何来定义变量的呢?

要说到C的变量,就不能不提一提变量的有效作用域。

我们知道,汇编当中定义的变量都是在整个程序中有效的,在程序中任何地方都可以改变变量的值,这样程序员就会经常遇到这样的情况:在调试程序时发现变量的值有误,却无法判断变量到底在什么地方被错误赋值或被赋予了错误的值,从而不得不在整个程序中到处设置断点,运行多次才找出问题的所在。这就是汇编当中变量定义造成不方便的地方!

而C的变量的定义区分了作用域,分为全局变量和局部变量,而区分定义这两种变量的方法很简单,只是在不同的位置定义就行了,这与标准C所规定的方法是一样的,后面再详细讨论。

下面是SN8 C定义的几个变量:

unsigned int temp1; unsigned int temp2; unsigned int temp3; unsigned int temp4; unsigned int led_dp; unsigned int step;

//当前状态

unsigned long job_mode; unsigned long power_mode1; float powerValue; int temp1_1; long temp2_2;

//有符号的

从上面的定义可以看到C的变量不仅区分了作用域,还有不同长度的变量类型,这样就方便了程序员的使用。

与面向数学运算的计算机相比,单片机的编程对变量类型或数据类型的选择更具有关键性意义。SN8 系列单片机是8-bit处理器的单片机,只有Byte型的数据是处理器直接支持的。对于C这样的高级语言,不管使用何种数据类型,虽然某一行程序从字面上看,其操作十分简单,然而,实际上系统的C编译器需要用一系列机器指令对其复杂的变量类型、数据类型的进行处理。相同的一行语句,变量选择的类型不同,处理时就会产生很大的差别,产生的代码更是差别很多。特别是当使用浮点变量时,将明显地赠加运算时间和程序的长度。当程序必须保证运算精度时,C编译器将调用相应的子程序库,把它们加到程序中。然而许多不熟练的程序员,

17

TDxxxV1.0

在编写C程序时往往会使用大量的、不必要的变量类型。这就导致C编译器相应地增加所调用的库函数以处理大量增加的变量类型,并最终导致程序变得过于庞大,运行速度减慢,甚至因此会在Link时,出现因程序过大而装不进ROM的情况。所以必须特别慎重的选择变量和数据类型。

而对于有符号与无符号的变量类型。在编写程序时,如果使用signed和unsigned两种数据类型,那么就得使用两种格式类型的库函数。这将使得占用的存储空间成倍增长。因此在编程时,如果只强调程序的运算速度而又不进行负数运算时,最好采用无符号(unsigned)格式。

Note: 1、选择数据类型的时候,在能够顺利完成功能的情况下,请尽量选择占空间少的数据类型,这样不管是在RAM空间使用上还是在产生代码效率上都是有益的! 2、能使用无符号数的都使用无符号数,以免处理出错,因为芯片内部是以无符号数处理的。 3、 切记!C语言对大小写敏感,在开始定义变量的时候就要注意要有相应的规范可循,用驼峰式是一个好的选择,这对于习惯于汇编的程序员来说,可能不大习惯!

当然,在标准C编程当中,经常会有为书写方便,对数据类型进行缩写定义,这在SN8 C也是允许的。如:

#define uchar unsigned char

#define uint unsigned int

这样,在以后的编程中,就可以用uchar代替unsigned char,用uint 代替 unsigned int 来定义变量。

3.3 数据的存储类型与存储结构

在前面分析查表类型数据定义的时候已经提到了数据在单片机里的存储,会分为ROM和RAM两部分,我们分别称之为程序存储器(ROM)和数据存储器(RAM)。它们在我们编写汇编的时候,会分别用关键字.code和.data来预先声明。

通常SN8系列单片机不用扩展存储器,它提供种类丰富的机型,你完全可以根据你的需求来选择合适的机型。因此,数据只存储在片内,寻址也不存在片内片外的区分。

SN8P单片机的数据存储器(RAM)的结构如下:

18

TDxxxV1.0

Bank0

0000H 007FH 0080H 00FFH Bank1

00100 01FFh 通用存储区

图3-2、RAM区结构

SN8P系列单片机的通用RAM区的大小依不同的芯片而不同,但都是按BANK来划分,每一个BANK内的地址为00H~FFH。而所有芯片的Bank0 的80H~ FFH的空间都是留给系统专用的系统寄存器区。

其实我们上面定义的变量都是放在RAM区的数据结构,在定义一个变量时,我们前面已经提到可以用__RAM和__ROM关键字来指定存储的地方,而变量定义一般都放在RAM当中,因而__RAM关键字是缺省项。看下面的例子:

unsigned int temp1; unsigned int __RAM temp2; __RAM unsigned int temp3;

unsigned long job_mode;

unsigned long __RAM job_mode2; __RAM unsigned long job_mode3; float powerValue; float __RAM powerValue; __RAM float powerValue; int temp1_1; int __RAM temp1-2;

__RAM temp1-3; long temp2_2;

其实上面的定义都是相同的效果,就是定义变量,并将其放置在数据存储区(RAM)内。也就是说,我们在定义一个变量,按我们C常用的方法定义就自动地放到RAM里了,缺省项给我们带来了很大的方便。

上面已经说明了,在RAM存储区内,80H~FFH是系统寄存器区。

那么我们又是如何来定义系统寄存器区呢? 我们先来看看系统寄存器都有哪些内容:

19

128Byte 用户存储区

通用存储区 80H~FFH为系统寄存器区域

更多的用户存储区

系统寄存器区 //有符号的

TDxxxV1.0

8 L 0 H 1 R - - ADB P2M P2 2 Z - - ADR - - 3 Y - - - P4M P4 4 X - - - P5M P5 5 6 7 8 - - - - 9 - - - - A - - - - B - - - - C - - - - D - - E - - F PFLAG RBANK - - - - - - - - - - - @YZ - - - 9 AMPM - A - - P4CON - - PEDGE PCH B DAM ADM C - D P0 - - INTRQ INTEN OSCM - T0M - - - WDTR TC0R PCL TC0M TC0C TC1M TC1C TC1R STKP - - - - - - E P0UR - P2UR - P4UR P5UR - F STK7L STK7H STK6L STK6H STK5L STK5H STK4L STK4H STK3L STK3H STK2L STK2H STK1L STK1H STK0L STK0H

表3-2、SN8P系列单片机系统寄存器列表

根据不同的芯片的资源,寄存器的内容会随之改变,但是他们定义的区域都不变。这些寄存器都分别对应了芯片内部的资源,SN8 C针对这部分的系统资源,对这些寄存器进行了定义,其定义的形式如下:

#define L #define H #define R #define Z #define Y

(*((__RAM unsigned int*)0x80)) (*((__RAM unsigned int*)0x81)) (*((__RAM unsigned int*)0x82)) (*((__RAM unsigned int*)0x83)) (*((__RAM unsigned int*)0x84))

#define X (*((__RAM unsigned int*)0x85))

#define PFLAG (*((__RAM unsigned int*)0x86))

这些定义都被包含在相应芯片的头文档里(.h),因此并不需要用户自己去定义寄存器的相应名称,只需要在程序的开头包含相应的头文档就行了。如下所示:

#include

Note: 这些系统寄存器都是以大写字母进行定义的,在编写程序时要注意这一点。

20

TDxxxV1.0

__SelectBANK _captrueZero_arg0 MOV _captrueZero_arg0, A ;End push arg....

call _captrueZero ;end of function call L451:

.stabn 0x44,0,587,L452-_procADC L452:

jmp L431

而对系统寄存器的位判断则允许有下面三种方式: 一是用上面提到的专用函数:

if(_bTest1(&INTRQ,4)) {

_bCLR(&INTRQ,4);

//FT0IRQ)

//FT0IRQ=0;

T0INT(); }

一是用系统定义的位名称:

if(FT0IRQ) {

//(_bTest1(&INTRQ,4))

//FT0IRQ=0;

_bCLR(&INTRQ,4); T0INT();

}

还有就是用位运算的方法:

if(INTRQ&0x10) { _bCLR(&INTRQ,4);

//FT0IRQ //FT0IRQ=0;

T0INT(); }

但是,遗憾的是上面3种方式产生代码的效率并不一样。在编程过程中比较你会发现第三种方式产生的代码最短,所以建议使用。

当然,它们的运行效果是一样的,都能起到分支判断的作用。

九、 内嵌汇编

C语言和别的语言一样,虽然具有非常多的优点,但也免不了存在一些局限性。有的时候,对硬件操作的功能无法实现,或者实现的代码所花费的时间空间代价太大。这时,我们还是不得不考虑使用汇编,毕竟其具有高效率和紧贴硬件的特性。正出于这样的考虑,SN8 C支持在C程序中内嵌汇编。

46

TDxxxV1.0

9.1 如何内嵌汇编

和一般的C语言一样,SN8 C 提供专门的关键字__asm(两个下划线)用于在C的源代码内嵌入汇编。

__asm关键词有以下两种用法:

__asm(“code\\n”)

__asm { asm_text }

这两种方式都可以在C中内嵌汇编,在C 中是作为一条语句来处理,当它处在顶层时(也就是说没有被任何函数所包含)就可以内嵌任意的汇编码到输出的汇编码中。

在asm_text中的Macros和其他的C指示性语句在预处理中不被处理,除非选择cpp_noskip_asmblock编译选项。cpp_noskip_asmblock选项会对插入到asm_text 中的cpp指示性语句和Macros进行处理。在asm_text 中的C和C++ 类型的注释仍然会被忽略。在asm_text里面,不能带有右括符(i.e., “)”),除非这个符号是字符串附带的。

我们来看看在C中嵌入汇编的例子:

if(step == ONE_PRESS_CLOCK_KEY_C) { //调节时钟时时间闪烁 disp_blink ^= 0x03; //闪烁小时位

disp_blink &=0x03; __asm {

mov mov

a,0x01 _led_dp,a

//两点常亮

};

//led_dp = 1;

}

我们来看看其产生的汇编码,看看内嵌汇编语句是怎么被处理的:

L125:

__SelectBANK _step MOV A, (_step+0)

SUB A, #0x10 JNZ L93 L128:

__SelectBANK _disp_blink MOV A, #3

XOR _disp_blink, A L129:

;__SelectBANK _disp_blink MOV A, #3

AND _disp_blink, A

47

TDxxxV1.0

L130: mov L131: mov L132: L133:

jmp L94

a,0x01 _led_dp,a

我们看到内嵌的汇编被原样转换嵌入生成的汇编码当中(L130,L131),那么其运行的结果自然就是我们所需要的了。

9.2 内嵌汇编时变量的传递

我们知道,在汇编当中与在C当中定义变量的方法是不同的。但是,我们在内嵌汇编当中也需要对由C定义的变量进行操作。该如何来处理这样的一个矛盾呢?

如下C程序代码:

void func(void) {

int x;

__asm { …

;; 存取局部变量 x. … }

return; }

经编译的结果:

C:\\sn8cc>sn8cc +w prog.c SN8CCWARN@(`prog.c' 3): source_warning: local `int x' is not referenced prog.c: 0 error(s), 1 warning(s)

上例中, 因为sn8cc认为变量x没有用到(sn8cc不检查内嵌式汇编码), +w编译选项会让sn8cc报告警告讯息。且不会为变量x配置空间。这样内嵌式组语码内所存取的位置是错误的。

为了解决这样的问题,SN8 C 专门引入“#pragma ref id”预处理指令,上述问题可用“#pragma ref id”通知sn8cc, 变数x是有用到的:

void func(void) {

int x;

#pragma ref x __asm { …

;; 存取局部变量 x. … }

return; }

48

TDxxxV1.0

这样sn8cc才不会报告警告讯息。

而我们还会面对的一种情况是在内嵌汇编当中要对全局变量进行读写操作,这又是一个什么情况呢?

假如我们直接应用C定义的名称,如:

Unsigned int Ver1; Void func(void) { }

编译则系统会提示出错!警告用户没有Ver1这样一个变量!这时候很奇怪了,明明有定义这样一个变量,可是系统就是提示Error!

其实,这就是由于在编译当中,编译器会对内嵌汇编进行原样转换所引发的错误。我们来看前面嵌入汇编的例子中的全局变量在产生的.Asm档中,对全局变量是怎么转换的。

C源程序中对全局变量的定义如下:

union flagWord2 {

unsigned int flagByte; struct bitdefine2 {

unsigned bit0:1; unsigned bit1:1; unsigned bit2:6;

__asm{ Mov … }; …

ver1,#0x5a;

}flagBit;

}led_dp;

unsigned int door_cnt; unsigned int door_cnt1; unsigned int door_cnt2;

而这些定义在生成的汇编中被转换成如下形式:

.stabs \

.stabs \.stabs \.stabs \

原来它们将每一全局变量的前面都增加了一个“_”来标识它们!

那么我们就可以找到解决的方法了,我们在嵌入的汇编中,如果要读写全局变量,就在它们的前面加上一个“_”,其实我们提到的例子中的led_dp就是这样子的!回过头去看看这条语

49

TDxxxV1.0

句:

mov _led_dp,a

上面的led_dp就是一个全局变量,而在内嵌汇编码中被增加了一条下划线。

需要提醒用户的是,变量转换的语法并不属于程序员掌握的范畴,这里也没有详细介绍,使用当中尽量少用这方面的转换,以免引起程序查错上的麻烦。这些应用必须是对生成.asm档有一定的了解才行。

十、SN8 C程序的结构组织

在程序的结构上,C同样与汇编有很大的区别。在汇编程序中,大多的程序员喜欢应用跳转,在程序中会安排许多的跳转指令,以此来控制程序的流程。而C则反对在程序中进行跳转,因为这样会破坏C 的模块特性,影响其结构化编程语言的优势。而单片机的C语言可以说应该介于他们之间。

10.1 主函数与子函数

C程序都会有且只有一个main()函数,并且将它作为整个程序的入口。这是一个非常重要的函数,我们都称之为主函数。而相对于主函数而言完成相对模块功能的直接或间接被主函数调用的这些功能函数,我们一般都把它称之为子函数。在C 当中,main()函数在整个的程序流程当中起着主控作用。

我们来看看怎么样来构建一个程序才能使其完全符合我们的需求呢?

我们是针对单片机的C语言,因此实时性是对程序一个非常普遍的要求,也是不同于运行于通用计算机上的C程序的特别的要求,我们的程序必须在最快的情况下,检测到外部条件的改变,在最短的时间内作出正确的响应,并且响应的同时又不能落下任何外部的请求。这样才能使我们的系统适用瞬息万变的环境,并发挥优异的性能,这才是我们需要的系统。

怎么样用好我们的芯片有限的处理能力,来完成如此复杂的任务? 对于整个程序而言,安排好main()函数对其他子函数的调用起着至关重要的作用。我们知道,大多任务我们没法在1毫秒内完成,有的甚至需要很长时间的延时,而在很短的时间内环境有可能发生改变,外部可能产生很多个请求。怎么样让程序既能完成任务(响应)又能及时接受外部请求及时处理?面对这样的情况我们就很自然会想起问题的优先分配,和任务的分时处理。当然我们没必要去开发一个操作系统来管理他们,我们只要调整我们的程序架构就可以做到这一点。

我们把所有的任务都分别用一个标志位来标示他们是不是完成,对于完成的任务会产生什么样新的需求也用一个标志来标识它。而对于正在进行的任务我们每次只去完成应该完成的一部分,将这些正在完成的任务轮换进行处理直到完成。改换这一思想是实现我们的编程方案的关键。我们来看看我们熟悉的按键扫描吧,传统上我们会按下面的方法来扫描判断按键:

50

TDxxxV1.0

四、程序流程控制

程序流程控制是程序的精华所在,正确的程序流程才能实现正确的程序功能。安排精巧的程序流程控制才可能使程序具有高效率。程序流程控制不管是在C还是在汇编中都是程序设计者最值得思考的地方。

4.1顺序结构

顺序结构是程序最基本的流程,其语句顺序执行,PC指针依次下移,是CPU内部处理指令的初始方法。这也是编程思维的最初级方式,也是程序的最基本方式和流程。

顺序结构流程:

A操作 B操作

图4-1 顺序结构流程

其实,其描述的是一个向量方式发展的问题,是所有问题发展和描述的基本方法,具有明确的方向性和时间性,如图中的A操作的发展方向只有一个,即接下来就是B操作,中间既没有反复也没有曲折变化,B一定发生在A的后面。

我们平时可以看到的C语言的实现:(微波炉开机初始化)

key_bibi_f = 1; menu_disp_h = 0xf0U; menu_disp_l = 0;

21

//上电响一声 //上电显示0:00

TDxxxV1.0

disp5 = 0xffU;

上述简短的几条语句分别完成一定目的的初始化,虽然在这里没有强制要求什么样的顺序,但是执行过程中是严格按语句的先后来开关目标的。

SN8ASM的实现:

b0bset key_bibi_f ;上电响一声

mov mov mov mov mov mov

a,#0f0h menu_disp_h,a a,#00h menu_disp_l,a a,#11111111b disp5,a

;上电显示0:00

这是一段功能完全与上面这段C相同的汇编实现,我们可以看见在顺序结构的汇编语句当中没有任何的跳转和判断,都是按原功能的顺序来描述。

从对比当中我们完全可以找到它们的逻辑上的一一对应关系。

4.2选择结构

事实上,完全顺序结构的流程的程序很少,因为很多事情并不是一帆风顺,顺流而下。很多情况下,事件的发生都需要具备一定的条件,只有一定条件下才有可能实现。这就得使用选择结构来描述。

选择结构流程图:

P为真? A操作 B操作

22

TDxxxV1.0

图4-2 选择结构流程图

简单选择结构只判断一个条件的真假来决定下面的需要执行的操作,这是一个最简单的判断。

C语言实现:(BCD码调整)

if(result_buf > 0x0a) {

result_buf = input + 6;

} else { result_buf = input; }

我们看见在C语言当中,我们用判断语句来实现这样一个判断逻辑,不管是什么样的条件,我们只计算其真假,做出判断。If??else??是一个非常好的判断组合,很容易用它来完成下列我们用汇编来实现的功能。

SN8 汇编实现:

cmprs nop b0bts0

a,#0ah fc

jmp $+3

b0mov a,y ret

b0mov a,y add a,#6h

;调整后的数放在a

事实上,在汇编程序中,我们会遇到几种情况的条件选择:

一是当条件是一个位变量时,我们可以直接用b0bts指令来进行判断。

一是当条件是是否满足一个预定的值,我们可以用CMPRS指令来进行判断,当然也可以将其转化为标志位来判断。也可以用减法的方法将其转化成为标志位进行判断,就看需要来安排了

更深入。。。。。。

A、串行多分支结构流程: 事实上大多数时候,单个的条件无法分析判断复杂的问题,有时候一个结果的出现需要很多个条件同时成立,而每一个条件成立,又都有不同的结果产生,这样就会有一系列的有层次的判断!

23

TDxxxV1.0

N P1为真? Y N P2为真? N Pn为真? Y Y C1 C2 C3 C4

图4-3 串行多分支结构流程图

C用if,else if嵌套来实现:

if(key_bibi_f) { buzzer_time = d_buzzer_time1; buzzer_not = d_buzzer_not3; }

else if(end_bibi_f) //buzzer10 { buzzer_time = d_buzzer_time2; //响声长短,500ms buzzer_not = d_buzzer_not1; //要响几次,#10 }

else if(segment_bibi_f) {

//buzzer20

//响声长短,500ms

//响声长短,200ms //要响几次,#1

buzzer_time = d_buzzer_time2; buzzer_not = d_buzzer_not1;

//要响几次,#4

} 说明: key_bibi_f end_bibi_f

;按键响声请求标志

;烹调结束响声请求标志

;段切换响声请求标志

sgment_bibi_f 按键响声:200ms/1次 结束响声:500ms/5次 段切换响声:500ms/2次

问:SN8 ASM怎么实现?

24

TDxxxV1.0

SN8 ASM的实现:

Buzzer00: b0bts1 key_bibi_f jmp buzzer10

mov a,@buzzer_time1 ;200ms mov buzzer_time,a ;响声长短 mov a,@buzzer_not3 mov buzzer_not,a

;#1 ;要响几次

jmp buzzer40 buzzer10:

b0bts1 end_bibi_f jmp buzzer20

mov a,@buzzer_time2 ;500ms mov buzzer_time,a ;响声长短 mov a,@buzzer_not1 mov buzzer_not,a jmp buzzer40

;#10

;要取反的次数

buzzer20: b0bts1 segment_bibi_f

jmp buzzer30

mov a,@buzzer_time2 ;500ms mov buzzer_time,a ;响声长短 mov a,@buzzer_not2 mov buzzer_not,a

;#4

;要取反的次数

jmp buzzer40

B、并行多分支结构流程:

P=? P = 1 P = 2 P = 3 P = n A1 A2 A3 An

25

TDxxxV1.0

从上面的程序中我们可以看出,汇编是通过用户规定的一个或几个寄存器来将参数传递进去!又用同样的方法,将结果传递出来。可以想到,所需的结果在函数返回后可以在A中得到,其应用方式如下:

mov cmprs

clock_min,a a,#60h

5.2 函数参数传递

在汇编程序中,用户必须自己定义寄存器用来存放往函数内传递的参数数据和返回的结果。在C程序中函数参数的传递都是通过形参和实参的对应来传递的,我们通过函数调用来将参数传递给函数,在完成函数功能得到结果后,又通过返回值将结果传回给调用的地方。如图:

clock_min = bcd(clock_min); unsigned int bcd(unsigned int input) {

图5-1 函数参数传递与返回

但是,我们都知道C是可以通过转成汇编来实现的,那么SN8 C是如何来实现C的函数传递的呢?虽然这是一个编译器内部的问题,但是面对单片机硬件的编程,了解其原理会有助于用户写出更为精简合理的程序。

我们来看看C函数调用时,编译器生成的中间汇编代码。假设于caller函数内调用callee函式. callee 的参数名称为

_callee_arg ? ; ? 为参数个数。

例如:

int foo(int a, int b, long c) 会产生 _foo_data SEGMENT DATA INBANK ;OVERLAYABLE _foo_arg0 DS 1 ; int a _foo_arg1 DS 1 ; int b _foo_arg2 DS 2 ; long c

前面所示的bcd函数调用的转换如下:

31

… … … return(result_buf); }

TDxxxV1.0

MOV

A, (_clock_min)

__SelectBANK _bcd_arg0 MOV _bcd_arg0, A ;End push arg.... call _bcd

__SelectBANK (_clock_min) MOV _clock_min, A

通过C与汇编程序的对应,我们很容易知道参数的传递是这样的:实际参数clock_min被直接赋值给函数的形参_bcd_arg0,然后在调用函数中参加运算。再来看看返回值,从程序中我们发现最后返回值是在A中得到的,那么在这里是由系统生成的,是不是有一定的规律呢?还是随意的?当然不可能是随意的!那么系统是如何规定的呢?

若返回值是基本的数据类型,一般都是放到固定的寄存器或虚拟寄存器里面,如下表所示:

返回值类型 unsigned/signed char unsigned/signed short unsigned/signed int unsigned/signed long float 寄存器 A A A A,R A,R,Y,Z 表5-1 不同返回值的存放列表

从上边的列表可以知道,对于不同返回值类型,编译器都会有固定的返回值存放点,当程序从函数中返回的时候,这些对应的寄存器里面就存放着返回值,这就从规则上印证了上述bcd函数的转换结果。

而对于那些返回值是数据结构的,系统则会在调用函数的时候增加一个隐含参数(地址)传递给被调函数,被调函数在完成功能运算后将结果放到指定的位置上,程序从函数里返回后就可以从该地址读取返回值。

尽管从原理上看起来,函数的参数传递与返回值的C与汇编的转换的形式是一样的,但是编译器产生代码时依然可能产生一些冗余代码。所以,在面对单片机这样的资源的硬件编程,过多的参数传递及过于复杂的返回值往往造成代码转换得低效率,这是用户需要注意的。

5.3 函数参数与全局变量

在通用计算机上编写C程序的时候,为了尽量使程序模块化,尽量减小模块之间的耦合度,往往会将函数的所有与外界的联系都通过参数传递来进行,这样就使得函数完全被功能化,同时具有很强的复用性。这些正是C所具有的优势所在,正是由于C具有这样的特征才使得C 具有很好的封装性,让这些函数很容易被封装成函数库,方便地提供任意的程序中调用。也正是有这么多的优势,才让我们选择了C。

但是,SN8 C是面对运算能力并不是很强的8-bit单片机处理核心,我们在关注程序的模

32

TDxxxV1.0

块化,可维护性,封装性的优势的同时又不得不考虑程序的转换效率和运行效率。

由于C 的封装特性,使得C程序非常简洁易懂,一个简单的加法赋值运算在汇编中会有很多条语句,而在C中则用一条非常简洁易懂的表达式就可以准确表达。从而使得C也显得更加简单易用。但是,忽略单片机的特征的C 程序就不会是一个好的程序。

面对C 的结构特征,用户必须定义适当数量的函数用于完成相应功能,这是符合C 的编程思想,也符合整体程序编程思想的。而如何来兼顾单片机的硬件特性呢?正因为如此,就要求SN8 C的用户对硬件和对SN8 ASM应该有一定的了解。

我们在前面已经看到过汇编是如何来传递参数的,事实上它是借助了汇编当中的变量的全局特性来完成的。因为全局变量的值不会因为程序的跳转而发生改变,所以在跳转到子程序内部执行的时候,依然可以得到所需变量的值。同样,当在子程序中对确定的全局变量进行改变,当返回到主调程序时依然可以从相应地址得到变量的值。

SN8 C 的参数传递也采取类似的方法,编译器会为相应的函数参数定义相应个数的全局变量(可覆盖的)用于传递参数,尽管这样做已经是一种很优化的处理方案,但相比于汇编的全局变量来看,始终存在数据传递的环节(将调用函数时的实际参数传递给定义的全局变量),不如汇编高效。而当有多个参数需要传递时更增加了转换的代码量!

再从返回值方面来考虑,我们先来看看得到返回值的转换代码。

如:

keyPress = keyConvert(keyPress);

这条keyFinishCHK中的语句调用keyConvert()函数,并将返回值存放到keyPress中。转换得代码如下:

;push arg....

__SelectBANK _keyFinishCHK_arg0 MOV A, (_keyFinishCHK_arg0) __SelectBANK _keyConvert_arg0 MOV _keyConvert_arg0, A ;End push arg.... call _keyConvert

__SelectBANK (_keyFinishCHK_arg0) MOV _keyFinishCHK_arg0, A ;end of function call

从中我们也可以看到返回的时候,都会有一个中间寄存器的转换(上例中的A) ,当返回值只有一个Byte时,这个中间寄存器是A,我们看不出差距。当用到其它寄存器时,就会有明显的代码消耗。

C有了全局变量和局部变量的区分,从而可以将有限的RAM进行复用,从而达到节省资源的目的。同时由于与底层地址的剥离,使变量的命名更趋向人性化,这些都为用户带来了很大的方便。但在考虑使用局部变量的时候我们又不得不考虑代码空间的消耗。这样,通用计算机编程时的“尽量少用全局变量”的准则在单片机编程中就受到了挑战。在函数定义上,我们就需要权衡,在全局变量和参数之间进行取舍。对于面对硬件的单片机编程,我们还是建议多用全局变量来传递数值,而不是用参数。这样在产生代码时就会越贴近汇编的形式,产生代码的效率会越高。

当然,必要时例外!

33

TDxxxV1.0

六、结构体、联合在SN8 C程序中的应用

结构体、联合都是属于C的数据封装形式,它们在通用C程序中起着举足轻重的作用。同样,在编写C程序时,用好这些数据封装形式可以非常方便用户实现面对硬件的编程。

6.1 结构体

结构体可以将几种数据类型组合在一起进行操作。

首先我们来看看结构体的定义,结构体用关键字struct进行定义,如:

Struct 结构体类型名{

成员1数据类型 成员2数据类型 ??

};

其中可以嵌套struct。

结构体在存储形式上是按定义形式的先后,将成员逐个存储摆放形成一个数据块。这样就为程序的数据块操作提供了一个方便的途径。例如在显示屏上要依次序显示[0,5],[1,10],[2,25],[3,50]这样格式的数,定义一个数据结构如下:

Struct disvalue{ Unsigned int number; Unsigned int Weight1;

Unsigned int weight2; };

成员1名称;

成员2名称;

Disvalue disarray[] = {{0,0,5},{1,1,0},{2,2,5},{3,5,0}};

在程序中我们就可以根据从程序操作过程中获得的num来进行显示。

Void diplayFun(unsigned int num) { }

Disvalue display;

Unsigned int disbuff[5]; Display = disarray[num]; disbuff[0] = display.number; disbuff[4] = display.weight1; disbuff[5] = display.weight2;

结构体在硬件编程中还有一个常用的功能就是定义位域,其定义的方法如下:

Struct 结构体名称{

Unsigned bit0:1; Unsigned bit1:2;

34

TDxxxV1.0

Unsigned bit2:1; Unsigned bit3:1; Unsigned num:4;

};

这个结构体中的成员数据类型都是Unsigned,这是必须的。成员名称同样要符合变量命名规则,它与其他结构体不同的是成员名称后面有一个冒号,后面跟一个数据。冒号后面的数据是指占用位的数量,如成员bit1会占用2个bit,而num会占用3个bit,其他分别占用1个bit。这样这个结构体就占用1个Byte的空间。在后面的应用我们会更详细地分析这一数据类型的应用。

同样,与通用C语言的编程相比,在SN8 C中有一些限制,对于struct,我们只能对其实例安排存放的空间,加__RAM和__ROM限制字加于限制。而对于struct的成员我们就无法对其进行限制,这个原因是非常明显的,若是都允许对存放空间进行限制的话,就会造成冲突。如下定义是系统不允许的:

struct StuType { int __ROM data; // 错误 };

对结构体内部的成员变量,其存储的位置由整个结构体来决定。

6.2 联合体

“联合”与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间, 一个结构变量的总长度是各成员长度之和。而在“联合”中,各成员共享一段内存空间, 一个联合变量的长度等于各成员中最长的长度。联合体的特点在于其成员的存储空间相同,即联合体内部成员指向同一个空间,事实上是同一个空间通过不同的名称调用。如同时定义几种数据类型char、int、long组成一个联合体。那么,这就意味着修改三个成员中的任意一个都会影响到另外两个的值。

一、联合的定义

定义一个联合类型的一般形式为:

union 联合名 {

成员表

};

成员表中含有若干成员,成员的一般形式为: 类型说明符 成员名 成员名的命名应符合标识符的规定。

例如:

union perdata { int class;

35

TDxxxV1.0

char office[10];

};

定义了一个名为perdata的联合类型,它含有两个成员,一个为整型,成员名为class;另一个为字符数组,数组名为office。联合定义之后,即可进行联合变量说明,被说明为perdata类型的变量,可以存放整型量class或存放字符数组office。

联合在硬件编程中的应用最大的好处在于对存储空间的控制,我们来看看下面的这段汇编代码:

.DATA

flag7 ds 1 …

micro_on_off_f equ flag7.0 grill_on_off_f equ flag7.1 motor_on_off_f equ flag7.2 uv_on_off_f equ flag7.3

… .CODE

b0bset motor_on_off_f

motor_on_off_f motor_port motor_on_off_f motor_port

;关炉灯

b0bts1 b0bclr b0bts0 b0bset b0bts0

grill_on_off_f

;关烧烤 ;关闭所有负载

;没有回到原来位置电机继续转

jmp $+3

b0bclr grill_port … clr flag7

这段代码中定义了一个变量flag7,占一个Byte。同时又对Flag7 的前面4个位进行定义。因为汇编对硬件操作的灵活性,所以能很容易的对一个Byte进行整体操作,也很容易对这个Byte 的每个位进行操作。我们用struct定义位域的方法,在C 中也能对位进行单独的操作而不用通过与或等运算。我曾想用下面的方法来定义:

Unsigned int Flag7;

Struct bitFlag{ Unsigned bit0:1; Unsigned bit1:1;

Unsigned bit2:1; Unsigned bit3:1; Unsigned null:4;

}pFlag7;

我想用一个结构体指针指向Flag7,但是我发现并不能达到我的需求目的,那么该怎么来

36

TDxxxV1.0

实现这上面的汇编所需实现的功能呢?当然,如果细想的话会有很多种方法,其中定义联合体就是一个很好的方法。我们在C中为了能对位进行直接的操作,一般都会定义一个以下形式的结构体:

Struct bitDefine{ Unsigned bit0:1; Unsigned bit1:1;

Unsigned bit2:1; Unsigned bit3:1; Unsigned bit4:1; Unsigned bit5:1; Unsigned bit6:1; Unsigned bit7:1;

};

那我们就可以利用这个定义来声明一个结构体放到一个联合体里,让它与一个int型变量共存。如:

union flagWord

{

unsigned int flagByte; bitdefine flagBit;

};

我们就可以通过union来对相同地址的Byte和Bit进行操作。 对于上面的这些功能我们得到下面的C程序: FlagWord flag7;

Flag7.flagBit.bit2 = 1;

;没有回到原来位置电机继续转

If(flag7.flagBit.bit2) motor_port =1; Else motor_port = 0; ;关炉灯 …

If(!flag7.flagBit.bit1) grill_port = 0;

;关烧烤

Else …

Flag7.flagByte = 0; ;关闭所有负载 这段C程序可以实现上面汇编一模一样的功能

同样的,将一个Long型数据与两个int型数据的结构体组成Union,如:

union longtype

{ };

37

unsigned long longV; struct inttype {

unsigned int int_l,int_h; }intV;

TDxxxV1.0

通过定义longType 的实例,我们就可以既对一个long型数据进行操作,又可以对数据的高低字节进行操作,而不用通过与或运算或移位就可以实现。这在我们的8-bit单片机的编程中,会用的比较频繁。这种的实现既兼顾了C的方便特性,又能有汇编的灵活性。这大概是union的最大优势了。

七、 中断

在实时性程序中,中断功能是一个非常重要的功能,中断的实现状况会直接影响程序中的计时计数,更可能影响程序功能的正确实现。因此如何来实现中断是一个很有价值的问题。那么在SN8 C中又如何来实现中断?

7.1 中断函数的定义

在芯片资源当中,大多是通过向量列表跳转的方法来实现程序的中断。在标准C中,所有的功能都是通过函数来实现,但是都有固定的中断函数,如int86()等,它们也都指向一个固定的中断向量,如0x80等系统固定的资源。

在8-bit Sonix 芯片当中,都有一个中断向量0x08,我们来看看汇编的中断程序实现:

.CODE org 00h

jmp Main_ST

org 08H jmp int_ser

int_ser: int_ser0:

b0xch a,accbuf mov a,pflag b0mov pflagbuf,a nop

fp00ien

int_ser10: b0bts1

jmp int_ser11 b0bts0 fp00irq jmp int_ser20 ;p00中断 b0bts1

ft0ien

38

int_ser11:

TDxxxV1.0

jmp int_ser19 b0bts0 ft0irq jmp int_ser40

;t0中断

int_ser19: jmp int_ser9 int_ser20:

b0bclr

fp00irq

;activation

;p00中断

jmp int_ser9 int_ser40:

b0bclr b0bset

ft0irq t0int t0c,#64h

;T0中断

;activation

mov_ int_ser9:

b0mov a,pflagbuf mov pflag,a b0xch

a,accbuf

reti …

上面的这段程序完成了从0x08的地址跳转并且根据设定的优先顺序判断中断类别(int_ser10—int_ser19),然后进行处理(int_ser20—int_ser40)的任务。其中中断开始和结束分别进行了寄存器的数据的Push和pop实现。

在SN8 C中,通过一个特殊函数来完成相同功能,这是一个SN8 C专有的函数,用关键字__interrupt 来声明。__interrupt关键字(以两个底线开头)指示函数是要作中断向量的处理函数。中断向量函数是一个无参数, 也无返回值的特殊函数。

其声明方式如下:

__interrupt MyHandler () { .... }

我们来看一个SN8 C的中断函数: __interrupt ints (void) //中断程序入口 ;1ms {

if(INTRQ&0x10) t0ints();

else if(INTRQ&0x01) p00ints(); //过零点中断

}

void t0ints(void) {

_bCLR(&INTRQ,4); t_loop_f = 1;

if(int_f) ++cnt11;

T0C+=t0int_val+1; _bCLR(&INTRQ,4); }

void p00ints(void) {

//有外部中断触发开始计时 继电器开关的时间 //T0中断数值重装

//20ms

39

TDxxxV1.0

_bCLR(&INTRQ,0); int_f = 1;

if(cnt11 >= 18) {

cnt11 = 0;

flag2.flagByte = 0xffU; ++t_ms2; if(t_ms2 == 50) { }

t_ms2= 0;

flag10.flagByte = 0xffU;

} }

我们看到__interrupt 函数非常简单,只是按顺序判断中断的类别,调用相应的处理函数。这里与汇编的区别就是没有0x08 的跳转和对寄存器的push和pop。原来中断向量进入点会备份所有寄存器,中断向量结束前, 前述备份的项目均会被还原。这些都是由编译器内部完成的。

事实上,这正是C语言的优势所在,只有跨过了硬件关联才使得C 有了跨平台特性!

用户若有其它额外的备份需求, 需要自行撰写inline assemble ( __asm(“asmcode”)或__asm { … } ),来完成或修改 sn8cc_macro.asm 中的 __pushInterruptSavedRegs 与 __popInterruptSavedRegs 。这里不对此进行详述!

7.2 中断过程的分析

中断产生后,系统都完成了哪些动作,对于单片机程序编写者来讲还是需要进了解才能使编写的程序达到最佳的效率。

② ① ④

图7-1 中断的过程

上图所描绘的是一个中断的过程。程序在主循环中运行到①的时候,中断条件成立,系统产生中断,此时,系统将会去执行中断程序。而为了能从中断中正确返回,在进入中断程序之前,系统会对当前的状态(ACC,PC等等)进行保存。然后,在②处程序运行进入中断程序,

40

TDxxxV1.0

③处中断程序结束,系统又需要将原来的运行状态还原,然后在④处继续运行主循环程序。一次中断完成。

7.3 中断函数的结构

在了解中断的过程后我们再来看看中断函数应该用一个什么样的结构。由于我们的中断往往涉及我们的计时,同时它还是占用主循环的时间来运行的。那么,要保证主循环的实时性,就要求中断占用的时间越短越好。那么,就要求我们在中断里不能做太多事情!

同时我们可能有几个中断源,我们在系统产生中断的时候就必须去判断是哪个中断源产生的中断。

鉴于以上要求,我们可以对中断程序作这样的安排:

N 是中断1? N Y Y Y 是中断2? N 是中断3? 是中断n? 中断1处理程序 中断2处理程序 中断3处理程序 中断n处理程序 RETI

图7-2 中断处理程序结构图

为了使中断资源不被长时间占用,我们的中断程序内不能运行任何长时间占用系统时间的程序!那么,我们怎么安排中断中的程序呢?其实占用中断资源的任务,我们完全可以安排到中断外去完成!我们只需要告诉主控程序发生了中断以及是哪个中断就可以了!

如:

41

TDxxxV1.0

__interrupt intserv(void) { } if(INTRQ&0x10) { _bCLR(&INTRQ,4); } else if(INTRQ&0x20) { _bCLR(&INTRQ,5); } TC0INT(); T0INT(); void INTround(void) { unsigned int bitValue = 0; if(intStatusWord1.ftc0int) { intStatusWord1.ftc0int = 0; //Add bit which on 10ms interrupt will affect; intStatusWord1.fkeyTimer = 1; intStatusWord1.fflashTimer = 1; } ... Void main(void) { }

…. } … intStatusWord1.ThandDelay = 1; … void TC0INT(void) { } TC0C=0x64; intStatusWord1.ftc0int = 1; INTround(); 图7-3 中断程序与主循环的关联

在上面图中的程序里面,我们看到中断程序只是判断是由哪个中断源引发的中断,然后调用中断处理程序,而中断处理程序只是将中断的状态恢复,然后将相应中断的自定义标志位置1。而主循环设置中断任务安排程序INTround(),由它通过检测相应的中断标志ftc0int去检测中断的发生,并且安排任务,然后在主循环中调用执行它。这样既能及时发现中断产生的请求,并及时处理,又不会过多地占用中断资源。

八、位操作

位是计算机的最基本存储单位,与其相对应的数值表示是二进制。事实上,硬件的所有数据的处理都是通过二进制来处理的,并且SN8系列的单片机的RAM的全部空间都是可位操作的。如此,位操作在我们的单片机程序中的重要性便不言而喻了。

8.1 位的定义

42

TDxxxV1.0

由于对位操作在单片机程序有着举足轻重的地位,所以,有很多类型的单片机C语言提供了bit型变量的定义,如C51。而SN8 C则和ANSI C一样,并没有bit型变量的定义方法。为了更好地标识我们所需要的每一个标志位,方便我们区分并进行操作,增强程序的可读性。我们都会寻求一种办法来对位进行定义,并给我们需要的位赋予一个我们容易理解的名称如keyPress,int10ms等等。

我们在前面已经提到过位域的定义,我们通过struct来定义一个Byte当中的每一个位,如:

Struct bitDefine{

Unsigned bit0:1; Unsigned bit1:1; Unsigned bit2:1; Unsigned bit3:1; Unsigned bit4:1; Unsigned bit5:1; Unsigned bit6:1; Unsigned bit7:1;

};

我们就可以定义一些具体的结构体实例:

Struct bitDefine flag1,flag2,flag3;

然后我们用宏定义的方法将我们需要的位名称赋予相对应的位。如:

#define fkeypress (flag1.bit1) #define fchatfinish (flag1.bit2)

//确认有一个键按下的标志 //按键Debounce完成标志

//有键正在处理的标志

#define fkeyProcessing (flag1.bit3) #define FhandDelay (flag1.bit4) #define FhandDelayRQ (flag1.bit5) #define FreleaseKey (flag1.bit6) #define FfirstAck (flag1.bit7)

//按键释放标志位

//按键第一次确认标志

通过这样的转换,我们就可以在后续的程序中直接用我们自己定义的名称来对每一个定义过的位进行处理了!

上面所说明的处理方法都是对用户自己定义的位进行的操作,其实还有一个重要的存储器部分是系统定义的。这就是芯片的系统寄存器(system register),这也是我们使用最频繁的寄存器。系统已经对需要用到的资源中的寄存器进行了定义,并被赋予了固定的名称,为其编写了固定的操作函数。用户只管拿来用就好了。

8.2 位的运算

对位的操作主要包含以下几种:置位,清除,位与(&),位或(|),位非(~),位异或(^),左移(<<),右移(>>)。

对于自定义的位,对于上面的这些操作举例如下:

Fkeypress = 1;

//置位

43

TDxxxV1.0

Fkeypress = 0;

//清除

而位与(&),位或(|),位异或(^),左移(<<),右移(>>)基本上是基于基本数据类型上的位操作,如:

keyinbuf <<=2;

tempbuf = P0&0x03; keyinbuf |= tempbuf;

keyinbuf = ~keyinbuf;

这些运算目的都是改变或者获得基本数据类型里面的位的值。这就是我们常用的逻辑尺等等东西。这些操作在生成.asm是转换为逻辑运算指令,上面的语句会被转换为:

L209: ;_keyinbuf = _keyinbuf << 2 ;__SelectBANK _keyinbuf

RLCM (_keyinbuf) RLCM (_keyinbuf) MOV A, #0xfc

AND (_keyinbuf), A B0MOV A, 0xd0 AND A, #(0xFF & 3)

__SelectBANK _ee_scale_c_keyscan_LOCAL MOV _ee_scale_c_keyscan_LOCAL+1, A

L210:

L211: ; R3 = _keyinbuf | _ee_scale_c_keyscan_LOCAL+1

__SelectBANK _keyinbuf MOV A, (_keyinbuf+0)

__SelectBANK _ee_scale_c_keyscan_LOCAL OR A, (_ee_scale_c_keyscan_LOCAL+1) B0MOV R3, A

B0MOV A, R3 __SelectBANK _keyinbuf MOV _keyinbuf, A L212:

__SelectBANK (_keyinbuf) MOV A, (_keyinbuf) B0MOV R3, A B0MOV A, R3 XOR A, #0xff B0MOV R3, A B0MOV A, R3

__SelectBANK _keyinbuf MOV _keyinbuf, A

而如果我们操作的对象是系统寄存器中的位,系统给我们提供了专门的位操作函数其原型如下:

44

TDxxxV1.0

void _bSET(unsigned long address, unsigned int bitOffset); void _bCLR(unsigned long address, unsigned int bitOffset); int _bTest0(unsigned long address, unsigned int bitOffset); int _bTest1(unsigned long address, unsigned int bitOffset);

其中传入参数 address 与 bitOffset 必须为常数,不可为变量。 BitOffset 有效值为 0 ~7

address 的高位为 bank number

前面两个是置位和清除函数,专门用于对系统寄存器进行位的置位和清除。 如我们要启动TC0,那么我们就将其TC0ENB(TC0M.bit7)位标志置1:

_bSET(&TC0M,7);

而如果要停止TC0,那么我们就清除其TC0ENB(TC0M.bit7)位标志:

_bCLR(&TC0M,7);

这两个函数也具有很高的转化效率,其转化代码分别为:

PreB0SET 218 7 0 PreB0CLR 218 7 0

后两个是位判断函数,其对给定的位判断为1或者为零,并返回一个int型数值。

8.3 位比较在程序流程控制中的应用

在程序中往往都通过判断一个或几个条件是否成立来控制程序的执行。而条件的成立就会用标志位来标示,这样位的判断比较在程序中就非常重要了!例如下面例子中的判断条件:

if(globalSW.tareRQ) {

//进行零位处理的话,不进行下面的处理

captrueZero(stabledata);

}

上面的判断是对自定义位globalSW.tareRQ进行的判断,若为1则执行函数调用,若为0则跳过函数调用,转而执行下面的语句。产生的汇编码如下:

__SelectBANK _globalSW BTS1 _globalSW.3

JMP L430 L450:

;push arg....

__SelectBANK _stabledata MOV A, _stabledata+1

;Select BANK

__SelectBANK _captrueZero_arg0 MOV (_captrueZero_arg0+1), A __SelectBANK _stabledata MOV A, _stabledata

45

TDxxxV1.0

图4-4 并行多分支结构流程

C可以用switch…Case来实现:

switch(step) { case 0:

//侍机状态下第一次按时钟键

//按第二次设置时钟的分钟位 //按第三次时钟设置完毕

ks81();break;

case ONE_PRESS_CLOCK_KEY_C: ks82();break;

case TWO_PRESS_CLOCK_KEY_C : ks83();break;

case BESPOKE_ING_C : ks84();break;

case SELECT_TIME_C :

查询预约的时间

运行的时间

//设置好预约后再按下时钟键可以

//按时钟键之前已经设置过烹调程序表

示想预约,置预约标志bespoke_f=1

if(job_mode1== DEFROST_MODE_C) break; else ks85();break; case START_ING_C: //在开始过程中按时钟键可以查询当前正在

if(job_mode1 == DEFROST_MODE_C) ks86();break;

}

汇编的实现如下:

预约

mov a,step b0bts1 fz

;=0侍机状态按时钟键设置小时

jmp ks82

cmprs a,ONE_PRESS_CLOCK_KEY_C

jmp ks83

cmprs a,TWO_PRESS_CLOCK_KEY_C jmp ks84 ;完成时钟设置 cmprs a,BESPOKE_ING_C ;预约状态下查询时间 jmp ks85

cmprs a,SELECT_TIME_C

;选择过功能后再按时钟键表示要

jmp ks86 key81: key82: key83: key84:

jmp jmp jmp jmp

key89 key89 key89 key89

26

TDxxxV1.0

key85: key86: key89:

jmp jmp ret

key89 key89

4.3循环结构

N P为真? Y A

图4-5 While型循环

While型循环的C语言实现:

tempbuf = 0;

while(tempbuf==15){++tempbuf;}

While型循环的汇编实现:

Loop:

Clr y B0mov a,y Cmprs a,#15 Jmp $+2 Jmp decms

loop90 y loop

;delay //delay

jmp

loop90:

RET

27

TDxxxV1.0

A Y P为真? N

图4-6 Do…while 循环

Do…while 循环的C实现:

unsigned int * pyz = (unsigned int *)0x7f; do{ *pyz = 0x00;

--i; }while(i);

Do…while 循环的汇编实现:

ClrRAM:

clr Y b0mov Z,#0x7f

;Select bank 0 ;Set @YZ address from 7fh

ClrRAM10:

clr @YZ

;Clear @YZ content ;z = z - 1 , skip next if z=0 ;Clear address 0x00

decms Z jmp ClrRAM10 clr @YZ mov a,#00H

28

TDxxxV1.0

五、函数

函数在C当中占有相当重要的地位,它是组成程序的元素。函数的定义、函数的调用构成C程序的主要结构。而以单片机为对象的编程又与通用C 的编程略有不同,主要表现在全局变量与函数参数的选择上以及参数类型的选择上。

5.1 函数定义

函数是完成一定的功能的模块,函数的出现使代码可以得以复用,可以大大减少产生的代码量,这是使用函数最为充分的理由。但是,从另一方面来看,函数基本都涉及参数的传递,若是处理不好就很容易产生参数传递的冗余代码,还有就是调用函数就一定涉及到入栈和出栈的处理,这些都占用CPU的资源,影响程序的实时性,若函数调用过于频繁,势必影响程序的执行效率。所以函数定义之前要考虑函数的划分,函数功能既不能覆盖过大,也不能覆盖过小,使函数的划分太细。

SN8 C 函数声明方式:

返回值类型 函数名(形参1数据类型,形参2数据类型,??);

函数定义方式:

返回值类型 函数名(参数列表) {

函数体; }

无返回值,无参数函数定义:

Void 函数名(void){}

Note: 对于无返回值的函数,都要声明是void,以免系统为其预留空间!

在C当中,函数都应该先声明,后调用,标准C的写法一般是先在程序的开头声明要用到

的函数,然后在程序中编写完整的函数,我们先来看看C的函数。

如:

unsigned int bcd(unsigned int); . .

clock_min = bcd(clock_min);

29

TDxxxV1.0

. . .

unsigned int bcd(unsigned int input) { unsigned int result_buf; }

result_buf = input & 0x0f; if(result_buf > 0x0a) { result_buf = input + 6; } else {

result_buf = input;

}

return(result_buf);

上面的函数完成一个固定的功能,能提供给程序中任意一个地方来调用并完成所需的功能,在C中都是通过给予不同的参数来得到需要的计算结果,这样就可以节省代码量,使程序得到复用。

其实汇编一样可以写成函数形式,这样就可以起到和C一样的效果,我们来看看汇编是怎么来完成的。

如:

调用点的汇编代码:

mov

a,clock_min

b0mov y,a call bcd mov

clock_min,a

功能函数代码:

bcd: b0mov a,y

30

;调整前的数放在Y

and cmprs nop b0bts0

a,#0fh a,#0ah fc

jmp $+3 b0mov a,y ret

b0mov a,y add a,#6h ret

;调整后的数放在a

本文来源:https://www.bwwdw.com/article/difh.html

Top