2006-03-10 08:17:00 roruby 阅读数 39
  • 51单片机综合小项目-第2季第4部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第2季第4个课程,也是51单片机学完之后的一个综合小项目,该项目运用了开发板上大多数外设设备,并将之结合起来实现了一个时间、温度显示以及报警功能、时间调整功能等单片机控制常见的功能,有一定代码量,需要一定调试技巧和编程能力来完成,对大家是个很好的总结和锻炼,并且能拓展项目经验。

    3414 人正在学习 去看看 朱有鹏
1单片机C51编程规范- 前言
  为了提高源程序的质量和可维护性,从而最终提高软件产品生产力,特编写此规范。

2 单片机C51编程规范-范围
  本标准规定了程序设计人员进行程序设计时必须遵循的规范。本规范主要针对C51编程语言和keil编译器而言,包括排版、注释、命名、变量使用、代码可测性、程序效率、质量保证等内容。

3 单片机C51编程规范-总则
l 格式清晰
l 注释简明扼要
l 命名规范易懂
l 函数模块化
l 程序易读易维护
l 功能准确实现
l 代码空间效率和时间效率高
l 适度的可扩展性

4 单片机C51编程规范-数据类型定义
编程时统一采用下述新类型名的方式定义数据类型。
建立一个datatype.h文件,在该文件中进行如下定义:
typedef bit BOOL; // 位变量 //
typedef unsigned char INT8U; // 无符号8位整型变量 //
typedef signed char INT8S; // 有符号8位整型变量 //
typedef unsigned int INT16U; // 无符号16位整型变量 //
typedef signed int INT16S; // 有符号16位整型变量 //
typedef unsigned long INT32U; // 无符号32位整型变量 //
typedef signed long INT32S; // 有符号32位整型变量 //
typedef float FP32; // 单精度浮点数(32位长度) //
typedef double FP64; // 双精度浮点数(64位长度) //

5 单片机C51编程规范-标识符命名

5.1 命名基本原则
l 命名要清晰明了,有明确含义,使用完整单词或约定俗成的缩写。通常,较短的单词可通过去掉元音字母形成缩写;较长的单词可取单词的头几个字母形成缩写。即"见名知意"。
l 命名风格要自始至终保持一致。
l 命名中若使用特殊约定或缩写,要有注释说明。
l 除了编译开关/头文件等特殊应用,应避免使用以下划线开始和/或结尾的定义。
l 同一软件产品内模块之间接口部分的标识符名称之前加上模块标识。

5.2 宏和常量命名
宏和常量用全部大写字母来命名,词与词之间用下划线分隔。对程序中用到的数字均应用有意义的枚举或宏来代替。

5.3 变量命名
变量名用小写字母命名,每个词的第一个字母大写。类型前缀(u8\s8 etc.)全局变量另加前缀g_。
局部变量应简明扼要。局部循环体控制变量优先使用i、j、k等;局部长度变量优先使用len、num等;临时中间变量优先使用temp、tmp等。

5.4 函数命名
函数名用小写字母命名,每个词的第一个字母大写,并将模块标识加在最前面。

5.5 文件命名
一个文件包含一类功能或一个模块的所有函数,文件名称应清楚表明其功能或性质。
每个.c文件应该有一个同名的.h文件作为头文件。

6 单片机C51编程规范-注释

6.1 注释基本原则
l 有助于对程序的阅读理解,说明程序在"做什么",解释代码的目的、功能和采用的方法。
l 一般情况源程序有效注释量在30%左右。
l 注释语言必须准确、易懂、简洁。
l 边写代码边注释,修改代码同时修改相应的注释,不再有用的注释要删除。

6.2 文件注释
文件注释必须说明文件名、函数功能、创建人、创建日期、版本信息等相关信息。
修改文件代码时,应在文件注释中记录修改日期、修改人员,并简要说明此次修改的目的。所有修改记录必须保持完整。
文件注释放在文件顶端,用"/*……*/"格式包含。
注释文本每行缩进4个空格;每个注释文本分项名称应对齐。
/***********************************************************
文件名称:
作 者:
版 本:
说 明:
修改记录:
***********************************************************/

6.3 函数注释
6.3.1 函数头部注释
函数头部注释应包括函数名称、函数功能、入口参数、出口参数等内容。如有必要还可增加作者、创建日期、修改记录(备注)等相关项目。
函数头部注释放在每个函数的顶端,用"/*……*/"的格式包含。其中函数名称应简写为FunctionName(),不加入、出口参数等信息。
/***********************************************************
函数名称:
函数功能:
入口参数:
出口参数:
备 注:
***********************************************************/

6.3.2 代码注释
代码注释应与被注释的代码紧邻,放在其上方或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开。一般少量注释应该添加在被注释语句的行尾,一个函数内的多个注释左对齐;较多注释则应加在上方且注释行与被注释的语句左对齐。
函数代码注释用"//…//"的格式。
通常,分支语句(条件分支、循环语句等)必须编写注释。其程序块结束行"}"的右方应加表明该程序块结束的标记"end of ……", 尤其在多重嵌套时。

6.4 变量、常量、宏的注释
同一类型的标识符应集中定义,并在定义之前一行对其共性加以统一注释。对单个标识符的注释加在定义语句的行尾。
全局变量一定要有详细的注释,包括其功能、取值范围、哪些函数或过程存取它以及存取时的注意事项等。
注释用"//…//"的格式。

7 单片机C51编程规范-函数

7.1 设计原则
函数的基本要求:
l 正确性:程序要实现设计要求的功能。
l 稳定性和安全性:程序运行稳定、可靠、安全。
l 可测试性:程序便于测试和评价。
l 规范/可读性:程序书写风格、命名规则等符合规范。
l 扩展性:代码为下一次升级扩展留有空间和接口。
l 全局效率:软件系统的整体效率高。
l 局部效率:某个模块/子模块/函数的本身效率高。

编制函数的基本原则:
l 单个函数的规模尽量限制在200行以内(不包括注释和空行)。一个函数只完成一个功能。
l 函数局部变量的数目一般不超过5~10个。
l 函数内部局部变量定义区和功能实现区(包含变量初始化)之间空一行。
l 函数名应准确描述函数的功能。通常使用动宾词组为执行某操作的函数命名。
l 函数的返回值要清楚明了,尤其是出错返回值的意义要准确无误。
l 不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。
l 减少函数本身或函数间的递归调用。
l 尽量不要将函数的参数作为工作变量。

7.2 函数定义
l 函数若没有入口参数或者出口参数,应用void明确申明。
l 函数名称与出口参数类型定义间应该空一格且只空一格。
l 函数名称与括号()之间无空格。
l 函数形参必须给出明确的类型定义。
l 多个形参的函数,后一个形参与前一个形参的逗号分割符之间添加一个空格。
l 函数体的前后花括号"{}" 各独占一行。

7.3 局部变量定义
l 同一行内不要定义过多变量。
l 同一类的变量在同一行内定义,或者在相邻行定义。
l 先定义data型变量,再定义idtata型变量,再定义xdata型变量.
l 数组、指针等复杂类型的定义放在定义区的最后。
l 变量定义区不做较复杂的变量赋值。

7.4 功能实现区规范
l 一行只写一条语句。
l 注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级。
l 各程序段之间使用一个空行分隔,加以必要的注释。程序段指能完一个较具体的功能的一行或多行代码。程序段内的各行代码之间相互依赖性较强。
l 不要使用难懂的技巧性很高的语句。
l 源程序中关系较为紧密的代码应尽可能相邻。
l 完成简单功能、关系非常密切的一条或几条语句可编写为函数或定义为宏。

8 单片机C51编程规范-排版

8.1 缩进
代码的每一级均往右缩进4个空格的位置。

8.2 分行
过长的语句(超过80个字符)要分成多行书写;长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进适当的缩进,使排版整齐,语句可读。避免把注释插入分行中。

8.3 空行
l 文件注释区、头文件引用区、函数间应该有且只有一行空行。
l 相邻函数之间应该有且只有一行空行。
l 函数体内相对独立的程序块之间可以用一行空行或注释来分隔。
l 函数注释和对应的函数体之间不应该有空行。
l 文件末尾有且只有一行空行。

8.4 空格
l 函数语句尾部或者注释之后不能有空格。
l 括号内侧(即左括号后面和右括号前面)不加空格,多重括号间不加空格。
l 函数形参之间应该有且只有一个空格(形参逗号后面加空格)。
l 同一行中定义的多个变量间应该有且只有一个空格(变量逗号后面加空格)。
l 表达式中,若有多个操作符连写的情况,应使用空格对它们分隔:
在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符前后均加一个空格;在两个以上的关键字、变量、常量进行非对等操作时,其前后均不应加空格;
逗号只在后面加空格;
双目操作符,如比较操作符, 赋值操作符"="、"+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位操作符"<<"、"^"等,前后均加一个空格;
单目操作符,如"!"、"~"、"++"、"-"、"&"(地址运算符)等,前后不加空格;
"->"、"."前后不加空格;
if、for、while、switch等关键字与后面的括号间加一个空格;

8.5 花括号
l if、else if、else、for、while语句无论其执行体是一条语句还是多条语句都必须加花括号,且左右花括号各独占一行。
l do{}while()结构中,"do"和"{"均各占一行,"}"和"while();"共同占用一行。
if ( ) do
{ {

} }while( );
else
{

}

8.6 switch语句
l 每个case和其判据条件独占一行。
l 每个case程序块需用break结束。特殊情况下需要从一个case块顺序执行到下一个case块的时候除外,但需要在交界处明确注释如此操作的原因,以防止出错。
l case程序块之间空一行,且只空一行。
l 每个case程序块的执行语句保持4个空格的缩进。
l 一般情况下都应该包含default分支。
Switch ( )
{
case x:

break;

case x:

break;

default:

break;
}

9 单片机C51编程规范-程序结构

9.1 基本要求
l 有main()函数的.c文件应将main()放在最前面,并明确用void声明参数和返回值。
l 对由多个.c文件组成的模块程序或完整监控程序,建立公共引用头文件,将需要引用的库头文件、标准寄存器定义头文件、自定义的头文件、全局变量等均包含在内,供每个文件引用。通常,标准函数库头文件采用尖角号< >标志文件名,自定义头文件采用双撇号″″标志文件名。
l 每个.c文件有一个对应的.h文件,.c文件的注释之后首先定义一个唯一的文件标志宏,并在对应的.h文件中解析该标志。
在.c文件中:
#define FILE_FLAG
在.h文件中:
#ifdef FILE_FLAG
#define XXX
#else
#define XXX extern
#endif
l 对于确定只被某个.c文件调用的定义可以单独列在一个头文件中、单独调用。

9.2 可重入函数
可重入函数中若使用了全局变量,应通过关中断、信号量等操作手段对其加以保护。

9.3 函数的形参
l 由函数调用者负责检查形参的合法性。
l 尽量避免将形参作为工作变量使用。

9.4 循环
l 尽量减少循环嵌套层数
l 在多重循环中,应将最忙的循环放在最内层
l 循环体内工作量最小
l 尽量避免循环体内含有判断语句
2013-08-01 14:47:17 lishan1998 阅读数 747
  • 51单片机综合小项目-第2季第4部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第2季第4个课程,也是51单片机学完之后的一个综合小项目,该项目运用了开发板上大多数外设设备,并将之结合起来实现了一个时间、温度显示以及报警功能、时间调整功能等单片机控制常见的功能,有一定代码量,需要一定调试技巧和编程能力来完成,对大家是个很好的总结和锻炼,并且能拓展项目经验。

    3414 人正在学习 去看看 朱有鹏

单片机C51编程几个有用的模块

作者:佚名     更新时间:2005-04-12

Keil C51常用功能模块使用说明

2001/11/19

说明

本文档包括单片机系统中常用到的时钟中断、通讯及键盘扫描等模块(见所附源程序)的说明。这些模块使用前后台系统模型。为达到最大的灵活性, 需要在用户工程中定义config.h文件, 在其中定义各模块可选参数的设置而不是直接更改源代码。

这些可选内容大部分为宏定义,如果不定义宏相应的功能在编译时被屏蔽,不会增加代码长度。 具体可选内容见各模块中的说明。

在Config.h文件中还要包含一个单片机硬件的资源头文件。

各模块使用了定义在Common.h中的一些数据类型。如:BIT(bit) BYTE(unsigned char)等,具体请参见源程序。

时钟模块

  在单片机软件设计中, 时钟是重要资源, 为了充分利用时钟资源, 故设计本时钟模块。 本模块使用定时器0,在完成用户指定功能的同时, 还能够自动处理一些其它模块中与时钟相关的信息。

  时钟模块由声明文件Timer.h以及实现文件Timer.c组成。

  用户应该在Config.h中定义宏TIMER_RELOAD来设定定时器0的重装载初值。推荐的定时器0的中断时间大于1毫秒。

  在程序的初始化阶段调用时钟模块的初始化函数InitTimerModule()之后,就可以使用时钟模块所以支持的各种功能。具体描述如下:

延时:当用户需要进行一定时间的延时时,可以通过调用Delay()来进行,参数为时钟中断的次数。如时钟中断周期为1ms, 想进行100ms的延时, 则可以调用Delay(100)。

注意:

如果延时的绝对时间小于时钟中断的周期,则不能够用本方法做到延时。

定时:当程序中需要使用定时功能时,如等待某外部事件,如果在一定时间内发生则继续执行,如果在这段时间内发生,则认为出现错误,转向错误处理机制。

在此推荐一种编程模式,但用户可以用自己认为更合理的方式处理此类问题。

这里简单说明一下关于阻塞式函数及非阻塞式函数。简单说,阻塞式函数就是当检测完成条件,如果不能够完成则等待,如:

void CheckSomething()

{

  // gbitSuccessFlag is a global variable

  while(gbitSuccessFlag == FALSE)

  {

    // do nothing but waiting

  }

}

可以看到,当bitSuccessFlag没有被设置为TRUE时,函数保持等待状态不返回,这样就是阻塞式的函数。

另外一种情况:

BIT CheckSomething()

{

  if(gbitSuccessFlag == TRUE)

  {

    // …

    return TRUE;

  }

  return FALSE;

}

在这里,如果所检测的事件有没有完成,函数进行检测之后,立刻返回,通过返回值报告完成情况,如果没有完成,则等待调用者分配再次执行的机会。这样的函数就是非阻塞函数。

在应用定时功能时,首先要将检测函数定义成非阻塞函数。如上面的第二个版本的CheckSomething。

然后下面模式:

BIT bitDone = FALSE;

ResetClock(); // clear timer interrupt times counter

while(GetClock() < MAX_WAITINGTIME)

{

  if(CheckSomething() == TRUE)

  {

    bitDone = TRUE;

    break;

  }

}

if(bitDone == FALSE)

{

  // process time out

}

或者简单写成:

BIT bitDone = FALSE;

ResetClock();

while(GetClock() < MAX_WAITINGTIME && (bitDone = CheckSomething));

if(bitDone == FLASE)

{

  // …

}

软件看门狗:实现具有局限性的看门狗功能。在程序中合适的地方加入对软件看门狗的复位函数ResetWatchDog(),在Config.h中加入宏TIMER_WATCHDOGTIMEOUT。当程序运行时,如果在发生TIMER_WATCHDOGTIMEOUT次时钟中断之内没有复位软件看门狗, 则系统复位。

注意:

如果没有加入TIMER_WATCHDOGTIMEOUT宏,程序中的ResetWatchDog没有任何用处,不用删除。

如果系统不能实现时钟中断,则软件看门狗也同时失去功能。

目前版本的的时钟模块的复位功能并不是完全复位,主要表现在当复位之后,系统将不再响应任何中断。所以软件看门狗只是一个程序的调试功能,不应该将它用于正式工作的程序,此时应该使用硬件看门狗。

用户自定义任务:如果想在时钟中断内执行一些耗时较短的任务,可以定义回调函数OnTimerInterrupt。函数原形为:void OnTimerInterrupt();

如果想在发生时钟中断时执行一些功能,而这些功能又耗时相对较长,不合适放在中断响应函数内部,则可以在程序中的主循环中的任意地方添加: ImpTimerService(),同时提供原形为void OnTimerEvent()的回调函数。具体的程序如下所示:

void main()

{

  Initialize();

  while(TRUE)

  {

    // … working

    ImpTimerService();

    // … working

  }

}

void OnTimerEvent()

{

  // do some task

}

对通讯模块提供支持:如通讯中的各种超时等,见通讯模块中的详细说明。

对键盘扫描模块提供支持:可以自动调用键盘扫描模块,见键盘扫描模块中的详细说明。

对程序调试提供支持:在程序开发过程中,有时为了判断程序是不是在工作,常用利用单片机系统的某一空闲引脚通过一个限流电阻接一个发光二极管,在程序中间隔固定时间交替控制发光管的明暗。实现这个功能只要在Config.h文件中定义TIMER_FLASHLED宏,如:

#define TIMER_FLASHLED        P1_0

则当时钟中断发生256次之后,改变发光管的状态。

通讯模块

  串口资源做为单片机与外界通信的常用手段,通讯模块提供了完全缓冲的串口通讯底层机制,适用于长度不大的数据包的发送及接收。如果处理关键数据,需要用户自己提供纠错协议。

  通讯模块由声明文件SComm.h及实现文件SComm.c组成。

  初始化:调用函数InitSCommModule()来初始化通讯模块:

  void InitSCommModule(BYTE byTimerReload, BIT bitTurbo)

  参数说明:

byTimerReload: 定时器1的重装载初始值。

bitTurob: 当此参数为TRUE时,串行通讯在定时器1的溢出速率基础上加倍。为FALSE时,串行通讯速率为定时器1的溢出速率。

  缓冲区:模块使用了由宏SCOMM_SENDBUFSIZE、SCOMM_RECEBUFSIZE及SCOMM_PKGBUFSIZE所指定长度的三个缓冲区,分别为发送、接收及数据包(用于处理接收到的数据)缓冲区(如果没有使用异步接收功能,则不需要使用数据包缓冲区)。

  在缺省时,这三个宏都被定义为10,但用户可以自已按照系统的RAM资源占用情况在Config.h中重定义缓冲区的大小。需要注意的是,如果缓冲的长度不够,当发送或接收长数据包的时候可能会发生问题,关于数据缓冲区的最小值的设置可以参考下面的说明。

  注意:需要尽快取出接收缓冲区中的数据,否则当缓冲区满之后,新的数据将被简单的丢掉。

  字节级服务函数: 在Config.h文件中定义了宏SCOMM_DriverInterface(如:#define SCOMM_DriverInterface),则可以使用字节级服务函数,即通讯模块的底层函数。

  共有两个函数可以使用:

  void SendByte(BYTE byData);

  发送一个字节,如果当前缓冲区满,则等待。参数byData为要发送的数据。

BYTE ReceByte();

接收一个字节,如果当前缓冲区中没有数据,则此函数阻塞,直到接收到数据为止。接收到数据通过返回值返回。

可以通过调用IsSendBufEmpty() IsSendBufFull() IsReceBufEmpty() IsReceBufFull()  宏来判断缓冲区的空或满,以防系统阻塞。

不推荐直接使用这一级的服务函数,应该使用高层次上的服务函数或者在这一级服务函数的基础上构造自己的通讯函数。

  

  数据包级服务函数:在Config.h文件中定义宏SCOMM_PackageInterface(如: #define SCOMM_PackageInterface)则可以使用数据包级服务函数。

  共有两个函数可以使用:

void SendPackage(BYTE* pbyData, BYTE byLen);

发送数据包,参数pbyData为将要发送的数据包缓冲区(数组)的指针,byLen为将要发送的数据包的长度。

当没有定义SCOMM_DriverInterface时,数据被完全缓冲。即不能够发送长度超过发送缓冲区长度的数据包。当定义了SCOMM_DriverInterface时,采用单字节发送,这时不限制需要发送的数据的长度。

BYTE RecePackage(BYTE* pbyData, BYTE byLen);

接收数据包,参数pbyData为存放将要接收的数据的缓冲区,byLen为缓冲区长度。返回值为接收到的字节数,当模块的接收缓冲区为空时,函数非阻塞,立即返回,返回值为零。

同步发送接收服务函数:

比如在一个串行总线多机通讯系统中,主机需要定时循检各从机的状态,往往是发一个包含从机地址及指令的数据包给从机,之后等待一定的时间,从机需要在这段时间之内给主机一个应答,如果没有这个应答,则认为从机工作状态出错,转去进行相应的处理。在这个模型里,主机不能够不进行等待而给另一台从机发送指令,也不能够不管从机在很久没有应答的情况下继续等待。还有一种情况,比如当使用485总线进行通信时,如果是两条通讯线则系统只能工作在半双工模式下,总线在同一时间内只能工作在发送或接收, 为了防止发送和接收相互干扰,这时的通讯常常需要使用同步发送和接收。

当在Config.h文件中定义宏SCOMM_SyncInterface后,则可以使用通讯模块提供同步发送接收函数:

void SendPackage(BYTE* pbyData, BYTE byLen);

发送数据包,参数pbyData为将要改善的数据包的缓冲区指针,byLen为将要发送的数据包的长度。

这个函数可以保证等待一个完整的数据包完全发送出去之后,它才返回,在这段时间内,它会阻塞运行。

BYTE SyncRecePackage(BYTE* pbyBuf, BYTE byBufLen, WORD wTimeout, BYTE byParam);

接收数据包。返回值为接收到的数据包长度。参数pbyBuf为将要接收数据包的缓冲区的指针,byBufLen为提供的缓冲区的长度,wTimeout为通信超时值,如果在发生了由wTimeout所指定次数的时钟中断而还没有接收到或没有接收到完整的数据包时,函数返回零,最后一个参数byParam的含义见后面的解释。

异步发送接收服务函数:

在一个简单的系统或多机通讯系统中的从机上,一般情况下不需要复杂的停等的工作模式,而且往往单片机需要对硬件进行控制和检测,不允许长时间的停下来检测通讯,但又要求当需要通讯时需要尽快的反应速度,这时就需要使用异步发送和接收服务函数。

使用异步发送和接收服务函数需要在Config.h文件中定义SCOMM_AsyncInterface宏。

同样提供两个服务函数:

void SendPackage(BYTE* pbyData, BYTE byLen);

发送数据包,参数pbyData为将要改善的数据包的缓冲区指针,byLen为将要发送的数据包的长度。

这里的函数的接口与同步发送和接收的服务函数相同。关于这里的细节,见后面对同步和异步服务函数的说明。

void AsyncRecePackage(BYTE byParam);

接收数据包,参数byParam的意义见后面的描述。

使用异步通讯需要用户定义一个回调函数,原型如下:

void OnRecePackage(BYTE* pbyData, BYTE byBufLen);

当异步接收服务函数接收到数据包之后,调用OnRecePackage回调函数,在pbyData指定的缓冲区中存放数据包,byBufLen为数据包的长度。

在Config.h文件中定义宏SCOMM_TIMEOUT可以设定异步接收的超时值,当开始接收数据包,但没有收完数据而发生了SCOMM_TIMEOUT次时钟中断后,认为接收超时, 将已接收到的数据删除。

同步和异步通讯服务函数:

有些情况下,比如一个通讯系统中,由一台计算机通过串口控制主机,主机通过串口连接很多从机,主机的串口采用分时复用,在这样的模型中,主机和控制计算机之间的通讯可以使用,异步通讯方式,而主机与从机可以使用同步通讯方式。而同步和异步的发送函数接口是相同的,在这样的情况下,发送都是同步的。在这样的模型中,当使用不同的接收函数之前,需要注意清除接收缓冲区中的内容,通讯模块提供函数:ClearReceBuffer来做到这一点,此函数原型如下:

void ClearReceBuffer();

通讯过程中,数据包往往是有固定的格式的,这种格式需要根据用户所使用的协议的不同而不同。同步和异步接收服务函数支持从接收到的数据中识别出一定格式的数据包。

举例说明:目前使用的协议决定数据包的格式为固定的包头0xff,固定的长度4个字节。其它的细节在这里不重要,所以忽略掉。

为了能够使用用SyncRecePackage或AsyncRecePackage函数从接收到的数据中识别出如上格式的数据包,有两种方法:

第一种办法是在Config.h文件中定义宏SCOMM_SimplePackageFormat,说明数据包为一种简单格式,比如上面的协议。

之后还要定义两个宏分别用来识别数据包头和数据包尾,两个宏分别是:

IsPackageHeader(x)和IsPackageTailer(x, y, z)

接收函数(SyncRecePackage和AsyncRecePackage)在没有开始接收数据包(准确的说是还没有从接收到的数据包中找到包头的时候),会对接收到的每一个字节的数据调用IsPackageHeader宏,将相应的数据作为参数,如果IsPackageHeader宏的结果为TRUE,则认为找到了数据包头,否则继续对下一个字节进行判断。

上面的协议对应的IsPackageHeader宏可以写为:

#define IsPackageHeader(x)    ((x) == 0xff)

当接收到包头之后,接收函数会对接下来的每一个字节数据调用IsPackagTailer宏来判断是不是已经接收完数据包,三个参数分别为:

x: 当前判断的数据。

y: 从包头开始到当前被判断的数据止的计数值,即当前已经接收到的字节数。

z:用户在调用SyncRecePackage或AsyncRecePackage时指定的byParam参数。

与IsPackageHeader相似,如果宏IsPackageTailer的运算结果为TRUE,则认为接收到完整的数据包,则调用相应的回调函数(对于异步接收函数)或返回(对于同步接收函数)。如果运算结果为FALSE则继续判断下一个字节的数据。

上面的协议对应的IsPackageTailer宏可以写为:

#define IsPackageTailer(x, y, z)  ((y) >= (z))

当然,用户也可以将IsPackageHeader和IsPackageTailer定义成为函数,通过BIT类型的返回值来向调用者提供与相应宏相同的信息。

另一种办法需要在Config.h文件中定义宏SCOMM_ComplexPackageFormat。(需要注意的是,不能够同时定义SCOMM_SimplePackageFormat和SCOMM_ComplexPackageFormat宏,否则会造成严重的不可预见性错误。

这时需要提供回调函数QueryPackageFormat,原形如下:

BYTE QueryPackageFormat(BYTE byData, BYTE byCount, BYTE byParam);

函数中三个参数的含义与使用简单数据包格式时判断数据包尾的宏的参数相同。

函数通过返回值来通知作为调用者的接收函数对接收到的数据如何处理,但目前这种方法仅为需要处理复杂数据包格式时的一种可选方法,但不推荐。用户如果想使用这种方法可以自己更改接收函数中相应的

#ifdef SCOM_ComplexPackageFormat

#endif // SCOMM_ComplexPackageFormat

预编译指令之间的内容。

例如指定QueryPackageFormat的返回值的含义:

0:继续找数据包头或继续找数据包尾。

1:找到数据包头。

2:找到数据包尾。

3:数据包出错,需要抛弃。

然后更改源代码来实现上面的协议。

注意:当用户需要使用字符串的时候,可以利用简单的包装函数将字符串转换为字节数组。所以没有必要提供专用的字符串处理函数。

键盘扫描模块

  键盘扫描模块有两种工作方式, 一种为自动的由时钟模块调用, 另一种是由程序员自行调用。

1) 由时钟模块自动调用的方式

将时钟模块实现文件(Timer.h)及键盘扫描模块的实现文件(KBScan。c)包含进工程, 在Config.h 文件中添加TIMER_KBSCANDELAY宏。 时钟模块自动对时钟中断进行计数, 当达到TIMER_KBSCANDELAY宏所定义的值后, 自动调用键盘扫描模块中的函数KBScanProcess()进行键盘扫描,也就是说,这个宏的值可以决定按键消抖动的时间。

用户应该提供两个回调函数OnKBScan()及onKeysPressed()。 在函数OnKBScan中进行键盘扫描, 并返回扫描码。 扫描码的类型缺省为BYTE, 当键盘规模较大时, BYTE不能够完全包含键盘信息时, 可在Config.h文件中重定义宏KBVALUE, 如下:

#define KBVALUE      WORD

这样, 就可以使用16位的键盘扫描码, 如果此时还达不到要求, 可以将键盘扫描码定义成一个结构, 但这样做将会增加代码量及消耗更多的RAM资源, 故不推荐。

  扫描模块调用OnKBScan取得扫描码, 并调用用户可以重定义的宏IsNoKeyPressed来判断是否有键按下, 缺省的IsNoKeyPressed实现如下:

#define IsNoKeyPressed(x)      ((x) == 0x00)  

即认为OnKBScan返回0扫描码时为没有键按下, 如果扫描函数返回其它非零扫描码做为无键按下的扫描码时, 可以在Config.h文件中重定义IsNoKeyPressed宏的实现。

  8位键盘扫描码(缺省值)时, 相应的扫描函数为:

BYTE OnKBScan()

  当扫描模块经过软件消抖动之后, 发现有键按下, 就会调用另一个回调函数onKeysPressed。 函数的声明应该如下:

void onKeyPressed(BYTE byKBValue, BYTE byState)

其中中的参数byKBValue的类型为BYTE, 此为缺省值, 如果使用其它类型的扫描码, 就将此参数变为相应类型。 这个值由OnKBScan返回。 另一个参数byState在通常情况下为零。 但当用户在Config.h中定义宏KBSCAN_BRUSTCOUNT, 同时键盘上的某键被按住不放时, 扫描模块对它自己的调用(注意这里和TIMER_KBSCANDELAY宏不同, TIMER_KBSCANDELAY是时钟中断足够的次数后调用扫描模块, 而KBSCAN_BRUSHCOUNT为扫描模块自身的被调用次数)进行计数,当达到KBSCAN_BRUSTCOUNT时,扫描模块调用onKeysPressed,此时第一个参数的含义不变, 而byState变成1, 同时计数器复位,又经过一段时间后,用值为3的byState 调用onKeysPressed。 这样就可以很方便的实现多功能键或者检测某键的长时间被按下。

2)由用户自行调用

由用户自行在程序中调用扫描模块,而不是由时钟中断自行调用。其它与方式1相同。

注意:

1) 函数KBScanProcess为非阻塞函数,它将在很快的时间内返回,等待再次分配给它执行的机会。

2) 函数KBScanProcess是在时钟中断外部运行的,它的过程可以被任何中断打断,但不影响系统运行。

3) byState的最大值为250,之后被复位为零。

应用举例

  现在来举例说明上述几个模块的使用方法。

  硬件环境描述:

  为了控制一盏灯,需要单片机提供一个做控制功能的开关量,这里不描述外部接口电路,只说明当单片机的P10脚为高电平时,灯灭,当P10脚为低电平时,灯亮。

可以通过计算机由串口发送命令来控制,或通过一个按键(push button不是自锁式的按键)来手动控制(按键接在P11脚上,当键没有按下时,P11电平为高,键按下时,引脚电平被接低),当使用按键手动控制的时候,需要给计算机发送通知。

设定串口通讯指令如下:

数据包由0xff做包头,4个字节长,第二个字节为命令代码,第三个字节为数据,最后一个字节为校验位。

命令和数据代码有如下组合:

(计算机发给单片机)

0x10 0x01: 计算机控制灯亮。(数据位是非零值即可)

0x10 0x00: 计算机控制灯灭。

(单片机发给计算机)

0x11 0x01:单片机正常执行控制指令,返回。(数据位是非零值即可)

0x11 0x00: 单片机不能够正常执行控制指令,或控制指令错(不明含义的数据包或校验错等)。

0x12 0x01:手动控制灯亮。(数据位是非零值即可)

0x12 0x00: 手动控制灯灭。

  建立工程:

  在硬盘上建立文件夹Projects,在Projects下建立Common文件夹及Example文件夹。将各模块的头文件及实现文件拷贝到Common文件夹下(推荐使用这样的文件组织结构,其它工程也可以建立在Projects下,各工程共享Common文件夹中的代码)。

  启动KeilC的IDE,在Example下建立新工程,将各模块的实现文件包含进工程。

  在Example文件夹下建立Output文件夹,更改工程设置,将Output作为输出文件和List文件的输出文件夹(推荐使用这样的结构,当保存工程文件时,可以简单的删除Output文件夹中的内容而不会误删有用的工程文件)。

  建立工程配置头文件Config.h及工程主文件Example.c,并将Exmaple.c文件加入工程。

  输入代码:

  代码的具体编写过程略。下面是最后的Config.h文件及Example.c文件。

//

// file: Config.h

//

#ifndef _CONFIG_H_

#define _CONFIG_H_

#include          // 使用AT89C52做控制

#include “../Common/Common.h”        // 使用自定义的数据类型

#define TIMER_RELOAD      922    // 11.0592MHz晶振,1ms中断周期

#define TIMER_KBSCANDELAY  40    // 40ms重检测按键状态,即40ms消抖

#define SCOMM_AsyncInterface        // 使用异步通讯服务

#define IsPackageHeader(x)    ((x) == 0xff)  // 判断包头是不是0xff

#define IsPackageTailer(x, y, z)  ((y) <= (z))  // 判断包的长度是不是足够

#endif // _CONFIG_H_

//

// file: Example.c

//

#include

#include “../Common/Common.h”

#include “../Common/Timer.h”

#include “../Common/Scomm.h”

#include “../Common/KBScan.h”

BIT gbitLampState = 1;            // 灯的状态,缺省为off

static void Initialize()

{

  InitTimerModule();            // 初始化时钟模块

  InitSCommModule(0xfd, TRUE);      // 初始化通讯模块,11.0592MHz晶振,

                    // 波特率为19200

  EA = 1;                // 开中断

}

void main()

{

  Initialize();              // 初始化

  while(TRUE)              // 主循环

  {

    ImpTimerService();          // 实现时钟中断服务,如键盘扫描

    AsyncRecePackage(4);        // 接收4个字节长的数据包

}

}

// 在中断外部响应时钟中断事件

void OnTimerEvent()            

{

  // do nothing

}

// 控制外部灯

static void TriggerLamp(BIT bEnable)  

{

  P10 = ~bEnable;            // 需要反相控制

}

// 键扫描回调函数

BYTE KBScan()  

{

  BIT b;

  P11 = 1;                // 读之前拉高引脚电平

  b = P11;                // 读入引脚状态

  return ~b;                // 数据反相做扫描码

}

// 计算校验和

static BYTE CalcCheckSum(BYTE* pbyBuf, BYTE byLen)

{

  BYTE by, bySum = 0;

  for(by = 0; by < byLen; by++)

    bySum += pbyBuf[by];

  return 0 – bySum;

}

// 接收到键盘消息回调函数

void onKeyPressed(BYTE byValue, BYTE byState)

{

  BYTE by[4];

  if(byState == 0)

  {

    switch(byValue)

    {

    case 0x01:

      gbitLampState = ~g  bitLampState;  // 灯状态取反

      TriggerLamp(gbitLampState);  // 执行控制

      by[0] = 0xff;          // 构造数据包

      by[1] = 0x12;

      by[2] = (BYTE)gbitLampState;

      by[3] = CalcCheckSum(by, 3);  // 求校验和

      SendPackage(by, 4);      // 发送数据包

      break;

    // 处理其它扫描码

    default:

      break;

  }

}

// 接收到数据包回调函数

void OnRecePackage(BYTE* pbyBuf, BYTE byBufLen)

{

  BYTE by[4];

  by[0] = 0xff;

  by[1] = 0x11;

  if(byBufLen != 4 || pbyBuf[3] != CalcCheckSum(pbyBuf, 3))

  {

    by[2] = 0;

    by[3] = CalcCheckSum(by, 3);

    SendPackage(by, 4);         // 处理长度或校验和不正确

  }

  switch(pbyBuf[1])

  {

  case 0x10:

    gbitLampState = (BIT)pbyBuf[2];

    TriggerLamp(gbitLampState);

    by[2] = 1;

    by[3] = CalcCheckSum(by, 3);

    SendPackage(by, 4);        // 发送成功执行通知

    break;

  default:                // 不知道的命令

    by[2] = 0;

    by[3] = CalcCheckSum(by, 3);

    SendPackage(by, 4);        // 发送没有成功执行通知

    break;

  }

}

2015-04-29 14:20:08 zd_2010 阅读数 1496
  • 51单片机综合小项目-第2季第4部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第2季第4个课程,也是51单片机学完之后的一个综合小项目,该项目运用了开发板上大多数外设设备,并将之结合起来实现了一个时间、温度显示以及报警功能、时间调整功能等单片机控制常见的功能,有一定代码量,需要一定调试技巧和编程能力来完成,对大家是个很好的总结和锻炼,并且能拓展项目经验。

    3414 人正在学习 去看看 朱有鹏
<%@LANGUAGE="JAVASCRIPT" CODEPAGE="936"%> 

单片机C51编程几个有用的模块(1)
 
Keil C51常用功能模块使用说明
说明
本文档包括单片机系统中常用到的时钟中断、通讯及键盘扫描等模块(见所附源程序)的说明。这些模块使用前后台系统模型。为达到最大的灵活性, 需要在用户工程中定义config.h文件, 在其中定义各模块可选参数的设置,而不是直接更改源代码。
这些可选内容大部分为宏定义,如果不定义宏相应的功能在编译时被屏蔽,不会增加代码长度。 具体可选内容见各模块中的说明。
在Config.h文件中还要包含一个单片机硬件的资源头文件。
各模块使用了定义在Common.h中的一些数据类型。如:BIT(bit) BYTE(unsigned char)等,具体请参见源程序。

时钟模块
在单片机软件设计中, 时钟是重要资源, 为了充分利用时钟资源, 故设计本时钟模块。 本模块使用定时器0,在完成用户指定功能的同时, 还能够自动处理一些其它模块中与时钟相关的信息。
时钟模块由声明文件Timer.h以及实现文件Timer.c组成。
用户应该在Config.h中定义宏TIMER_RELOAD来设定定时器0的重装载初值。推荐的定时器0的中断时间大于1毫秒。

在程序的初始化阶段调用时钟模块的初始化函数InitTimerModule()之后,就可以使用时钟模块所以支持的各种功能。具体描述如下:
延时:当用户需要进行一定时间的延时时,可以通过调用Delay()来进行,参数为时钟中断的次数。如时钟中断周期为1ms, 想进行100ms的延时, 则可以调用Delay(100)。
注意:
如果延时的绝对时间小于时钟中断的周期,则不能够用本方法做到延时。

定时:当程序中需要使用定时功能时,如等待某外部事件,如果在一定时间内发生则继续执行,如果在这段时间内发生,则认为出现错误,转向错误处理机制。
在此推荐一种编程模式,但用户可以用自己认为更合理的方式处理此类问题。
这里简单说明一下关于阻塞式函数及非阻塞式函数。简单说,阻塞式函数就是当检测完成条件,如果不能够完成则等待,如:
void CheckSomething()
{
// gbitSuccessFlag is a global variable
while(gbitSuccessFlag == FALSE)
{
// do nothing but waiting
}
}
可以看到,当bitSuccessFlag没有被设置为TRUE时,函数保持等待状态不返回,这样就是阻塞式的函数。
另外一种情况:
BIT CheckSomething()
{
if(gbitSuccessFlag == TRUE)
{
// …
return TRUE;
}
return FALSE;
}
在这里,如果所检测的事件有没有完成,函数进行检测之后,立刻返回,通过返回值报告完成情况,如果没有完成,则等待调用者分配再次执行的机会。这样的函数就是非阻塞函数。
在应用定时功能时,首先要将检测函数定义成非阻塞函数。如上面的第二个版本的CheckSomething。
然后下面模式:
BIT bitDone = FALSE;
ResetClock(); // clear timer interrupt times counter
while(GetClock() < MAX_WAITINGTIME)
{
if(CheckSomething() == TRUE)
{
bitDone = TRUE;
break;
}
}
if(bitDone == FALSE)
{
// process time out
}

或者简单写成:
BIT bitDone = FALSE;
ResetClock();
while(GetClock() < MAX_WAITINGTIME && (bitDone = CheckSomething));
if(bitDone == FLASE)

// …
}

软件看门狗:实现具有局限性的看门狗功能。在程序中合适的地方加入对软件看门狗的复位函数ResetWatchDog(),在Config.h中加入宏TIMER_WATCHDOGTIMEOUT。当程序运行时,如果在发生TIMER_WATCHDOGTIMEOUT次时钟中断之内没有复位软件看门狗, 则系统复位。
注意:
如果没有加入TIMER_WATCHDOGTIMEOUT宏,程序中的ResetWatchDog没有任何用处,不用删除。
如果系统不能实现时钟中断,则软件看门狗也同时失去功能。
目前版本的的时钟模块的复位功能并不是完全复位,主要表现在当复位之后,系统将不再响应任何中断。所以软件看门狗只是一个程序的调试功能,不应该将它用于正式工作的程序,此时应该使用硬件看门狗。

用户自定义任务:如果想在时钟中断内执行一些耗时较短的任务,可以定义回调函数OnTimerInterrupt。函数原形为:void OnTimerInterrupt();
如果想在发生时钟中断时执行一些功能,而这些功能又耗时相对较长,不合适放在中断响应函数内部,则可以在程序中的主循环中的任意地方添加: ImpTimerService(),同时提供原形为void OnTimerEvent()的回调函数。具体的程序如下所示:
void main()
{
Initialize();
while(TRUE)
{
// … working
ImpTimerService();
// … working
}
}

void OnTimerEvent()
{
// do some task
}

对通讯模块提供支持:如通讯中的各种超时等,见通讯模块中的详细说明。
对键盘扫描模块提供支持:可以自动调用键盘扫描模块,见键盘扫描模块中的详细说明。
对程序调试提供支持:在程序开发过程中,有时为了判断程序是不是在工作,常用利用单片机系统的某一空闲引脚通过一个限流电阻接一个发光二极管,在程序中间隔固定时间交替控制发光管的明暗。实现这个功能只要在Config.h文件中定义TIMER_FLASHLED宏,如:
#define TIMER_FLASHLED P1_0
则当时钟中断发生256次之后,改变发光管的状态。

通讯模块
串口资源做为单片机与外界通信的常用手段,通讯模块提供了完全缓冲的串口通讯底层机制,适用于长度不大的数据包的发送及接收。如果处理关键数据,需要用户自己提供纠错协议。
通讯模块由声明文件SComm.h及实现文件SComm.c组成。
初始化:调用函数InitSCommModule()来初始化通讯模块:
void InitSCommModule(BYTE byTimerReload, BIT bitTurbo)
参数说明:
byTimerReload: 定时器1的重装载初始值。
bitTurob: 当此参数为TRUE时,串行通讯在定时器1的溢出速率基础上加倍。为FALSE时,串行通讯速率为定时器1的溢出速率。

缓冲区:模块使用了由宏SCOMM_SENDBUFSIZE、SCOMM_RECEBUFSIZE及SCOMM_PKGBUFSIZE所指定长度的三个缓冲区,分别为发送、接收及数据包(用于处理接收到的数据)缓冲区(如果没有使用异步接收功能,则不需要使用数据包缓冲区)。
在缺省时,这三个宏都被定义为10,但用户可以自已按照系统的RAM资源占用情况在Config.h中重定义缓冲区的大小。需要注意的是,如果缓冲的长度不够,当发送或接收长数据包的时候可能会发生问题,关于数据缓冲区的最小值的设置可以参考下面的说明。
注意:需要尽快取出接收缓冲区中的数据,否则当缓冲区满之后,新的数据将被简单的丢掉。

字节级服务函数: 在Config.h文件中定义了宏SCOMM_DriverInterface(如:#define SCOMM_DriverInterface),则可以使用字节级服务函数,即通讯模块的底层函数。
共有两个函数可以使用:
void SendByte(BYTE byData);
发送一个字节,如果当前缓冲区满,则等待。参数byData为要发送的数据。
BYTE ReceByte();
接收一个字节,如果当前缓冲区中没有数据,则此函数阻塞,直到接收到数据为止。接收到数据通过返回值返回。
可以通过调用IsSendBufEmpty() IsSendBufFull() IsReceBufEmpty() IsReceBufFull() 宏来判断缓冲区的空或满,以防系统阻塞。
不推荐直接使用这一级的服务函数,应该使用高层次上的服务函数或者在这一级服务函数的基础上构造自己的通讯函数。

数据包级服务函数:在Config.h文件中定义宏SCOMM_PackageInterface(如: #define SCOMM_PackageInterface)则可以使用数据包级服务函数。
共有两个函数可以使用:
void SendPackage(BYTE* pbyData, BYTE byLen);
发送数据包,参数pbyData为将要发送的数据包缓冲区(数组)的指针,byLen为将要发送的数据包的长度。
当没有定义SCOMM_DriverInterface时,数据被完全缓冲。即不能够发送长度超过发送缓冲区长度的数据包。当定义了SCOMM_DriverInterface时,采用单字节发送,这时不限制需要发送的数据的长度。

BYTE RecePackage(BYTE* pbyData, BYTE byLen);
接收数据包,参数pbyData为存放将要接收的数据的缓冲区,byLen为缓冲区长度。返回值为接收到的字节数,当模块的接收缓冲区为空时,函数非阻塞,立即返回,返回值为零。

同步发送接收服务函数:
比如在一个串行总线多机通讯系统中,主机需要定时循检各从机的状态,往往是发一个包含从机地址及指令的数据包给从机,之后等待一定的时间,从机需要在这段时间之内给主机一个应答,如果没有这个应答,则认为从机工作状态出错,转去进行相应的处理。在这个模型里,主机不能够不进行等待而给另一台从机发送指令,也不能够不管从机在很久没有应答的情况下继续等待。还有一种情况,比如当使用485总线进行通信时,如果是两条通讯线则系统只能工作在半双工模式下,总线在同一时间内只能工作在发送或接收, 为了防止发送和接收相互干扰,这时的通讯常常需要使用同步发送和接收。
当在Config.h文件中定义宏SCOMM_SyncInterface后,则可以使用通讯模块提供同步发送接收函数:
void SendPackage(BYTE* pbyData, BYTE byLen);
发送数据包,参数pbyData为将要改善的数据包的缓冲区指针,byLen为将要发送的数据包的长度。
这个函数可以保证等待一个完整的数据包完全发送出去之后,它才返回,在这段时间内,它会阻塞运行。

BYTE SyncRecePackage(BYTE* pbyBuf, BYTE byBufLen, WORD wTimeout, BYTE byParam);
接收数据包。返回值为接收到的数据包长度。参数pbyBuf为将要接收数据包的缓冲区的指针,byBufLen为提供的缓冲区的长度,wTimeout为通信超时值,如果在发生了由wTimeout所指定次数的时钟中断而还没有接收到或没有接收到完整的数据包时,函数返回零,最后一个参数byParam的含义见后面的解释。

异步发送接收服务函数:
在一个简单的系统或多机通讯系统中的从机上,一般情况下不需要复杂的停等的工作模式,而且往往单片机需要对硬件进行控制和检测,不允许长时间的停下来检测通讯,但又要求当需要通讯时需要尽快的反应速度,这时就需要使用异步发送和接收服务函数。
使用异步发送和接收服务函数需要在Config.h文件中定义SCOMM_AsyncInterface宏。
同样提供两个服务函数:
void SendPackage(BYTE* pbyData, BYTE byLen);
发送数据包,参数pbyData为将要改善的数据包的缓冲区指针,byLen为将要发送的数据包的长度。
这里的函数的接口与同步发送和接收的服务函数相同。关于这里的细节,见后面对同步和异步服务函数的说明。

void AsyncRecePackage(BYTE byParam);
接收数据包,参数byParam的意义见后面的描述。
使用异步通讯需要用户定义一个回调函数,原型如下:
void OnRecePackage(BYTE* pbyData, BYTE byBufLen);
当异步接收服务函数接收到数据包之后,调用OnRecePackage回调函数,在pbyData指定的缓冲区中存放数据包,byBufLen为数据包的长度。
在Config.h文件中定义宏SCOMM_TIMEOUT可以设定异步接收的超时值,当开始接收数据包,但没有收完数据而发生了SCOMM_TIMEOUT次时钟中断后,认为接收超时, 将已接收到的数据删除。

同步和异步通讯服务函数:
有些情况下,比如一个通讯系统中,由一台计算机通过串口控制主机,主机通过串口连接很多从机,主机的串口采用分时复用,在这样的模型中,主机和控制计算机之间的通讯可以使用,异步通讯方式,而主机与从机可以使用同步通讯方式。而同步和异步的发送函数接口是相同的,在这样的情况下,发送都是同步的。在这样的模型中,当使用不同的接收函数之前,需要注意清除接收缓冲区中的内容,通讯模块提供函数:ClearReceBuffer来做到这一点,此函数原型如下:
void ClearReceBuffer();

通讯过程中,数据包往往是有固定的格式的,这种格式需要根据用户所使用的协议的不同而不同。同步和异步接收服务函数支持从接收到的数据中识别出一定格式的数据包。
举例说明:目前使用的协议决定数据包的格式为固定的包头0xff,固定的长度4个字节。其它的细节在这里不重要,所以忽略掉。
为了能够使用用SyncRecePackage或AsyncRecePackage函数从接收到的数据中识别出如上格式的数据包,有两种方法:
第一种办法是在Config.h文件中定义宏SCOMM_SimplePackageFormat,说明数据包为一种简单格式,比如上面的协议。
之后还要定义两个宏分别用来识别数据包头和数据包尾,两个宏分别是:
IsPackageHeader(x)和IsPackageTailer(x, y, z)
接收函数(SyncRecePackage和AsyncRecePackage)在没有开始接收数据包(准确的说是还没有从接收到的数据包中找到包头的时候),会对接收到的每一个字节的数据调用IsPackageHeader宏,将相应的数据作为参数,如果IsPackageHeader宏的结果为TRUE,则认为找到了数据包头,否则继续对下一个字节进行判断。
上面的协议对应的IsPackageHeader宏可以写为:
#define IsPackageHeader(x) ((x) == 0xff)
当接收到包头之后,接收函数会对接下来的每一个字节数据调用IsPackagTailer宏来判断是不是已经接收完数据包,三个参数分别为:
x: 当前判断的数据。
y: 从包头开始到当前被判断的数据止的计数值,即当前已经接收到的字节数。
z:用户在调用SyncRecePackage或AsyncRecePackage时指定的byParam参数。
与IsPackageHeader相似,如果宏IsPackageTailer的运算结果为TRUE,则认为接收到完整的数据包,则调用相应的回调函数(对于异步接收函数)或返回(对于同步接收函数)。如果运算结果为FALSE则继续判断下一个字节的数据。
上面的协议对应的IsPackageTailer宏可以写为:
#define IsPackageTailer(x, y, z) ((y) >= (z))
当然,用户也可以将IsPackageHeader和IsPackageTailer定义成为函数,通过BIT类型的返回值来向调用者提供与相应宏相同的信息。

另一种办法需要在Config.h文件中定义宏SCOMM_ComplexPackageFormat。(需要注意的是,不能够同时定义SCOMM_SimplePackageFormat和SCOMM_ComplexPackageFormat宏,否则会造成严重的不可预见性错误。
这时需要提供回调函数QueryPackageFormat,原形如下:
BYTE QueryPackageFormat(BYTE byData, BYTE byCount, BYTE byParam);
函数中三个参数的含义与使用简单数据包格式时判断数据包尾的宏的参数相同。
函数通过返回值来通知作为调用者的接收函数对接收到的数据如何处理,但目前这种方法仅为需要处理复杂数据包格式时的一种可选方法,但不推荐。用户如果想使用这种方法可以自己更改接收函数中相应的
#ifdef SCOM_ComplexPackageFormat
#endif // SCOMM_ComplexPackageFormat
预编译指令之间的内容。
例如指定QueryPackageFormat的返回值的含义:
0:继续找数据包头或继续找数据包尾。
1:找到数据包头。
2:找到数据包尾。
3:数据包出错,需要抛弃。
然后更改源代码来实现上面的协议。

注意:当用户需要使用字符串的时候,可以利用简单的包装函数将字符串转换为字节数组。所以没有必要提供专用的字符串处理函数。

键盘扫描模块
键盘扫描模块有两种工作方式, 一种为自动的由时钟模块调用, 另一种是由程序员自行调用。
1) 由时钟模块自动调用的方式
将时钟模块实现文件(Timer.h)及键盘扫描模块的实现文件(KBScan。c)包含进工程, 在Config.h 文件中添加TIMER_KBSCANDELAY宏。 时钟模块自动对时钟中断进行计数, 当达到TIMER_KBSCANDELAY宏所定义的值后, 自动调用键盘扫描模块中的函数KBScanProcess()进行键盘扫描,也就是说,这个宏的值可以决定按键消抖动的时间。 
用户应该提供两个回调函数OnKBScan()及OnKeysPressed()。 在函数OnKBScan中进行键盘扫描, 并返回扫描码。 扫描码的类型缺省为BYTE, 当键盘规模较大时, BYTE不能够完全包含键盘信息时, 可在Config.h文件中重定义宏KBVALUE, 如下:
#define KBVALUE WORD
这样, 就可以使用16位的键盘扫描码, 如果此时还达不到要求, 可以将键盘扫描码定义成一个结构, 但这样做将会增加代码量及消耗更多的RAM资源, 故不推荐。 
扫描模块调用OnKBScan取得扫描码, 并调用用户可以重定义的宏IsNoKeyPressed来判断是否有键按下, 缺省的IsNoKeyPressed实现如下:
#define IsNoKeyPressed(x) ((x) == 0x00) 
即认为OnKBScan返回0扫描码时为没有键按下, 如果扫描函数返回其它非零扫描码做为无键按下的扫描码时, 可以在Config.h文件中重定义IsNoKeyPressed宏的实现。
8位键盘扫描码(缺省值)时, 相应的扫描函数为:
BYTE OnKBScan()
当扫描模块经过软件消抖动之后, 发现有键按下, 就会调用另一个回调函数OnKeysPressed。 函数的声明应该如下:
void OnKeyPressed(BYTE byKBValue, BYTE byState)
其中中的参数byKBValue的类型为BYTE, 此为缺省值, 如果使用其它类型的扫描码, 就将此参数变为相应类型。 这个值由OnKBScan返回。 另一个参数byState在通常情况下为零。 但当用户在Config.h中定义宏KBSCAN_BRUSTCOUNT, 同时键盘上的某键被按住不放时, 扫描模块对它自己的调用(注意这里和TIMER_KBSCANDELAY宏不同, TIMER_KBSCANDELAY是时钟中断足够的次数后调用扫描模块, 而KBSCAN_BRUSHCOUNT为扫描模块自身的被调用次数)进行计数,当达到KBSCAN_BRUSTCOUNT时,扫描模块调用OnKeysPressed,此时第一个参数的含义不变, 而byState变成1, 同时计数器复位,又经过一段时间后,用值为3的byState 调用OnKeysPressed。 这样就可以很方便的实现多功能键或者检测某键的长时间被按下。
2)由用户自行调用
由用户自行在程序中调用扫描模块,而不是由时钟中断自行调用。其它与方式1相同。

注意:
1) 函数KBScanProcess为非阻塞函数,它将在很快的时间内返回,等待再次分配给它执行的机会。
2) 函数KBScanProcess是在时钟中断外部运行的,它的过程可以被任何中断打断,但不影响系统运行。
3) byState的最大值为250,之后被复位为零。

<%@LANGUAGE="JAVASCRIPT" CODEPAGE="936"%>
单片机C51编程几个有用的模块(2)
应用举例
现在来举例说明上述几个模块的使用方法。
硬件环境描述:
为了控制一盏灯,需要单片机提供一个做控制功能的开关量,这里不描述外部接口电路,只说明当单片机的P10脚为高电平时,灯灭,当P10脚为低电平时,灯亮。
可以通过计算机由串口发送命令来控制,或通过一个按键(push button不是自锁式的按键)来手动控制(按键接在P11脚上,当键没有按下时,P11电平为高,键按下时,引脚电平被接低),当使用按键手动控制的时候,需要给计算机发送通知。
设定串口通讯指令如下:
数据包由0xff做包头,4个字节长,第二个字节为命令代码,第三个字节为数据,最后一个字节为校验位。
命令和数据代码有如下组合:
(计算机发给单片机)
0x10 0x01: 计算机控制灯亮。(数据位是非零值即可)
0x10 0x00: 计算机控制灯灭。
(单片机发给计算机)
0x11 0x01:单片机正常执行控制指令,返回。(数据位是非零值即可)
0x11 0x00: 单片机不能够正常执行控制指令,或控制指令错(不明含义的数据包或校验错等)。
0x12 0x01:手动控制灯亮。(数据位是非零值即可)
0x12 0x00: 手动控制灯灭。

 

建立工程:
在硬盘上建立文件夹Projects,在Projects下建立Common文件夹及Example文件夹。将各模块的头文件及实现文件拷贝到Common文件夹下(推荐使用这样的文件组织结构,其它工程也可以建立在Projects下,各工程共享Common文件夹中的代码)。
启动KeilC的IDE,在Example下建立新工程,将各模块的实现文件包含进工程。
在Example文件夹下建立Output文件夹,更改工程设置,将Output作为输出文件和List文件的输出文件夹(推荐使用这样的结构,当保存工程文件时,可以简单的删除Output文件夹中的内容而不会误删有用的工程文件)。
建立工程配置头文件Config.h及工程主文件Example.c,并将Exmaple.c文件加入工程。

输入代码:
代码的具体编写过程略。下面是最后的Config.h文件及Example.c文件。
//
// File: Config.h
//
#ifndef _CONFIG_H_
#define _CONFIG_H_
#include <Atmel/At89x52.h> // 使用AT89C52做控制
#include “../Common/Common.h” // 使用自定义的数据类型
#define TIMER_RELOAD 922 // 11.0592MHz晶振,1ms中断周期
#define TIMER_KBSCANDELAY 40 // 40ms重检测按键状态,即40ms消抖
#define SCOMM_AsyncInterface // 使用异步通讯服务
#define IsPackageHeader(x) ((x) == 0xff) // 判断包头是不是0xff
#define IsPackageTailer(x, y, z) ((y) <= (z)) // 判断包的长度是不是足够
#endif // _CONFIG_H_

//
// File: Example.c
//
#include <Atmail/At89x52.h>
#include “../Common/Common.h”
#include “../Common/Timer.h”
#include “../Common/Scomm.h”
#include “../Common/KBScan.h”

BIT gbitLampState = 1; // 灯的状态,缺省为off

static void Initialize()
{
InitTimerModule(); // 初始化时钟模块
InitSCommModule(0xfd, TRUE); // 初始化通讯模块,11.0592MHz晶振,
// 波特率为19200
EA = 1; // 开中断
}

void main()
{
Initialize(); // 初始化
while(TRUE) // 主循环
{
ImpTimerService(); // 实现时钟中断服务,如键盘扫描
AsyncRecePackage(4); // 接收4个字节长的数据包
}
}

// 在中断外部响应时钟中断事件
void OnTimerEvent() 
{
// do nothing
}

// 控制外部灯
static void TriggerLamp(BIT bEnable) 
{
P10 = ~bEnable; // 需要反相控制
}

// 键扫描回调函数
BYTE KBScan() 
{
BIT b;
P11 = 1; // 读之前拉高引脚电平
b = P11; // 读入引脚状态
return ~b; // 数据反相做扫描码
}

// 计算校验和
static BYTE CalcCheckSum(BYTE* pbyBuf, BYTE byLen)
{
BYTE by, bySum = 0;
for(by = 0; by < byLen; by++)
bySum += pbyBuf[by];
return 0 – bySum;
}

// 接收到键盘消息回调函数
void OnKeyPressed(BYTE byValue, BYTE byState)
{
BYTE by[4];
if(byState == 0)
{
switch(byValue)
{
case 0x01:
gbitLampState = ~g bitLampState; // 灯状态取反
TriggerLamp(gbitLampState); // 执行控制
by[0] = 0xff; // 构造数据包
by[1] = 0x12;
by[2] = (BYTE)gbitLampState;
by[3] = CalcCheckSum(by, 3); // 求校验和
SendPackage(by, 4); // 发送数据包
break;
// 处理其它扫描码
default:
break;
}
}

// 接收到数据包回调函数
void OnRecePackage(BYTE* pbyBuf, BYTE byBufLen)
{
BYTE by[4];
by[0] = 0xff;
by[1] = 0x11;
if(byBufLen != 4 || pbyBuf[3] != CalcCheckSum(pbyBuf, 3))
{
by[2] = 0;
by[3] = CalcCheckSum(by, 3);
SendPackage(by, 4); // 处理长度或校验和不正确
}

switch(pbyBuf[1])
{
case 0x10:
gbitLampState = (BIT)pbyBuf[2];
TriggerLamp(gbitLampState);
by[2] = 1;
by[3] = CalcCheckSum(by, 3);
SendPackage(by, 4); // 发送成功执行通知
break;

default: // 不知道的命令
by[2] = 0;
by[3] = CalcCheckSum(by, 3);
SendPackage(by, 4); // 发送没有成功执行通知
break;
}

 

2013-08-01 14:47:21 lishan1998 阅读数 449
  • 51单片机综合小项目-第2季第4部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第2季第4个课程,也是51单片机学完之后的一个综合小项目,该项目运用了开发板上大多数外设设备,并将之结合起来实现了一个时间、温度显示以及报警功能、时间调整功能等单片机控制常见的功能,有一定代码量,需要一定调试技巧和编程能力来完成,对大家是个很好的总结和锻炼,并且能拓展项目经验。

    3414 人正在学习 去看看 朱有鹏

 1.前言

  为了提高源程序的质量和可维护性,从而最终提高软件产品生产力,特编写此规范。

2.范围

  本标准规定了程序设计人员进行程序设计时必须遵循的规范。本规范主要针对C51编程语言和keil编译器而言,包括排版、注释、命名、变量使用、代码可测性、程序效率、质量保证等内容。

3.总则

   a.格式清晰

   b.注释简明扼要

   c.命名规范易懂

   d.函数模块化

   e.程序易读易维护

   f.功能准确实现

   g.代码空间效率和时间效率高

   h.适度的可扩展性

4.数据类型定义

  编程时统一采用下述新类型名的方式定义数据类型。

  建立一个datatype.h文件,在该文件中进行如下定义:

     typedef bit BOOL; // 位变量 //

     typedef unsigned char INT8U; // 无符号8位整型变量 //

     typedef signed char INT8S; // 有符号8位整型变量 //

     typedef unsigned int INT16U; // 无符号16位整型变量 //

     typedef signed int INT16S; // 有符号16位整型变量 //

     typedef unsigned long INT32U; // 无符号32位整型变量 //

     typedef signed long INT32S; // 有符号32位整型变量 //

     typedef float FP32; // 单精度浮点数(32位长度) //

     typedef double FP64; // 双精度浮点数(64位长度) //

5.标识符命名

5.1.命名基本原则

     a.命名要清晰明了,有明确含义,使用完整单词或约定俗成的缩写。通常,较短的单词可通过去掉元音字母形成缩写;较长的单词可取单词的头几个字母形成缩写。即"见名知意"。

     b.命名风格要自始至终保持一致。

     c.命名中若使用特殊约定或缩写,要有注释说明。

     d.除了编译开关/头文件等特殊应用,应避免使用以下划线开始和/或结尾的定义。

     e.同一软件产品内模块之间接口部分的标识符名称之前加上模块标识。

5.2.宏和常量命名

宏和常量用全部大写字母来命名,词与词之间用下划线分隔。对程序中用到的数字均应用有意义的枚举或宏来代替。

5.3 变量命名

变量名用小写字母命名,每个词的第一个字母大写。类型前缀(u8\s8 etc.)全局变量另加前缀g_。

局部变量应简明扼要。局部循环体控制变量优先使用i、j、k等;局部长度变量优先使用len、num等;临时中间变量优先使用temp、tmp等。

5.4 函数命名

函数名用小写字母命名,每个词的第一个字母大写,并将模块标识加在最前面。

5.5 文件命名

一个文件包含一类功能或一个模块的所有函数,文件名称应清楚表明其功能或性质。

每个.c文件应该有一个同名的.h文件作为头文件。

6.注释

6.1.注释基本原则

a.有助于对程序的阅读理解,说明程序在"做什么",解释代码的目的、功能和采用的方法。

b.一般情况源程序有效注释量在30%左右。

c.注释语言必须准确、易懂、简洁。

d.边写代码边注释,修改代码同时修改相应的注释,不再有用的注释要删除。

6.2 文件注释

文件注释必须说明文件名、函数功能、创建人、创建日期、版本信息等相关信息。

修改文件代码时,应在文件注释中记录修改日期、修改人员,并简要说明此次修改的目的。所有修改记录必须保持完整。

文件注释放在文件顶端,用"/*……*/"格式包含。

注释文本每行缩进4个空格;每个注释文本分项名称应对齐。

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

文件名称:

作 者:

版 本:

说 明:

修改记录:

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

6.3 函数注释

6.3.1 函数头部注释

函数头部注释应包括函数名称、函数功能、入口参数、出口参数等内容。如有必要还可增加作者、创建日期、修改记录(备注)等相关项目。

函数头部注释放在每个函数的顶端,用"/*……*/"的格式包含。其中函数名称应简写为FunctionName(),不加入、出口参数等信息。

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

函数名称:

函数功能:

入口参数:

出口参数:

备 注:

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

6.3.2 代码注释

代码注释应与被注释的代码紧邻,放在其上方或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开。一般少量注释应该添加在被注释语句的行尾,一个函数内的多个注释左对齐;较多注释则应加在上方且注释行与被注释的语句左对齐。

函数代码注释用"//…//"的格式。

通常,分支语句(条件分支、循环语句等)必须编写注释。其程序块结束行"}"的右方应加表明该程序块结束的标记"end of ……", 尤其在多重嵌套时。

6.4 变量、常量、宏的注释

同一类型的标识符应集中定义,并在定义之前一行对其共性加以统一注释。对单个标识符的注释加在定义语句的行尾。

全局变量一定要有详细的注释,包括其功能、取值范围、哪些函数或过程存取它以及存取时的注意事项等。

注释用"//…//"的格式。

7.函数

7.1 设计原则

函数的基本要求:

a.正确性:程序要实现设计要求的功能。

b.稳定性和安全性:程序运行稳定、可靠、安全。

c.可测试性:程序便于测试和评价。

d.规范/可读性:程序书写风格、命名规则等符合规范。

e.扩展性:代码为下一次升级扩展留有空间和接口。

f.全局效率:软件系统的整体效率高。

g. 局部效率:某个模块/子模块/函数的本身效率高。

编制函数的基本原则:

a.单个函数的规模尽量限制在200行以内(不包括注释和空行)。一个函数只完成一个功能。

b.函数局部变量的数目一般不超过5~10个。

c.函数内部局部变量定义区和功能实现区(包含变量初始化)之间空一行。

d.函数名应准确描述函数的功能。通常使用动宾词组为执行某操作的函数命名。

e.函数的返回值要清楚明了,尤其是出错返回值的意义要准确无误。

f.不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。

g.减少函数本身或函数间的递归调用。

f.尽量不要将函数的参数作为工作变量。

7.2 函数定义

a.函数若没有入口参数或者出口参数,应用void明确申明。

b.函数名称与出口参数类型定义间应该空一格且只空一格。

c.函数名称与括号()之间无空格。

d.函数形参必须给出明确的类型定义。

e. 多个形参的函数,后一个形参与前一个形参的逗号分割符之间添加一个空格。

f. 函数体的前后花括号"{}" 各独占一行。

7.3 局部变量定义

a.同一行内不要定义过多变量。

b.同一类的变量在同一行内定义,或者在相邻行定义。

c.先定义data型变量,再定义idtata型变量,再定义xdata型变量.

d. 数组、指针等复杂类型的定义放在定义区的最后。

e.变量定义区不做较复杂的变量赋值。

7.4 功能实现区规范

a.一行只写一条语句。

b.注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级。

c.各程序段之间使用一个空行分隔,加以必要的注释。程序段指能完一个较具体的功能的一行或多行代码。程序段内的各行代码之间相互依赖性较强。

d.不要使用难懂的技巧性很高的语句。

e. 源程序中关系较为紧密的代码应尽可能相邻。

f.完成简单功能、关系非常密切的一条或几条语句可编写为函数或定义为宏。

8 排版

8.1 缩进

代码的每一级均往右缩进4个空格的位置。

8.2 分行

过长的语句(超过80个字符)要分成多行书写;长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进适当的缩进,使排版整齐,语句可读。避免把注释插入分行中。

8.3 空行

a. 文件注释区、头文件引用区、函数间应该有且只有一行空行。

b.相邻函数之间应该有且只有一行空行。

c.函数体内相对独立的程序块之间可以用一行空行或注释来分隔。

d.函数注释和对应的函数体之间不应该有空行。

e.文件末尾有且只有一行空行。

8.4 空格

a.函数语句尾部或者注释之后不能有空格。

b.括号内侧(即左括号后面和右括号前面)不加空格,多重括号间不加空格。

c.函数形参之间应该有且只有一个空格(形参逗号后面加空格)。

d.同一行中定义的多个变量间应该有且只有一个空格(变量逗号后面加空格)。

e.表达式中,若有多个操作符连写的情况,应使用空格对它们分隔:

在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符前后均加一个空格;在两个以上的关键字、变量、常量进行非对等操作时,其前后均不应加空格;

逗号只在后面加空格;

双目操作符,如比较操作符, 赋值操作符"="、"+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位操作符"<<"、"^"等,前后均加一个空格;

单目操作符,如"!"、"~"、"++"、"-"、"&"(地址运算符)等,前后不加空格;

"->"、"."前后不加空格;

if、for、while、switch等关键字与后面的括号间加一个空格;

8.5 花括号

a.if、else if、else、for、while语句无论其执行体是一条语句还是多条语句都必须加花括号,且左右花括号各独占一行。

b.do{}while()结构中,"do"和"{"均各占一行,"}"和"while();"共同占用一行。

if ( ) do

{ {

} }while( );

else

{

}

8.6 switch语句

a.每个case和其判据条件独占一行。

b.每个case程序块需用break结束。特殊情况下需要从一个case块顺序执行到下一个case块的时候除外,但需要在交界处明确注释如此操作的原因,以防止出错。

c.case程序块之间空一行,且只空一行。

d. 每个case程序块的执行语句保持4个空格的缩进。

e.一般情况下都应该包含default分支。

Switch ( )

{

case x:

break;

case x:

break;

default:

break;

}

9.程序结构

9.1 基本要求

a. 有main()函数的.c文件应将main()放在最前面,并明确用void声明参数和返回值。

b.对由多个.c文件组成的模块程序或完整监控程序,建立公共引用头文件,将需要引用的库头文件、标准寄存器定义头文件、自定义的头文件、全局变量等均包含在内,供每个文件引用。通常,标准函数库头文件采用尖角号< >标志文件名,自定义头文件采用双撇号″″标志文件名。

c.每个.c文件有一个对应的.h文件,.c文件的注释之后首先定义一个唯一的文件标志宏,并在对应的.h文件中解析该标志。

在.c文件中:

#define FILE_FLAG

在.h文件中:

#ifdef FILE_FLAG

#define XXX

#else

#define XXX extern

#endif

d.对于确定只被某个.c文件调用的定义可以单独列在一个头文件中、单独调用。

9.2 可重入函数

可重入函数中若使用了全局变量,应通过关中断、信号量等操作手段对其加以保护。

9.3 函数的形参

a.由函数调用者负责检查形参的合法性。

b.尽量避免将形参作为工作变量使用。

9.4 循环

a.尽量减少循环嵌套层数

b.在多重循环中,应将最忙的循环放在最内层

c.循环体内工作量最小

d.尽量避免循环体内含有判断语句

2008-08-07 17:25:00 cy757 阅读数 1145
  • 51单片机综合小项目-第2季第4部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第2季第4个课程,也是51单片机学完之后的一个综合小项目,该项目运用了开发板上大多数外设设备,并将之结合起来实现了一个时间、温度显示以及报警功能、时间调整功能等单片机控制常见的功能,有一定代码量,需要一定调试技巧和编程能力来完成,对大家是个很好的总结和锻炼,并且能拓展项目经验。

    3414 人正在学习 去看看 朱有鹏
 
  1. #include <reg52.h>
  2. #define uchar unsigned char
  3. #define uint unsigned int
  4. //延时子程序
  5. void mDelay(uchar Delay)
  6. {   uchar i;
  7.     for(;Delay>0;Delay--)
  8.     {   
  9.         for(i=0;i<123;i++);
  10.     }
  11. }
  12. void main()
  13. {
  14.     uchar i,d;
  15.     P0 = 0;  //所以LED点亮。
  16.     mDelay(250);//延时250mS(12M晶振)
  17.     mDelay(250);//延时250mS(12M晶振)
  18.     while(1)
  19.     {
  20.         d = 0x01;
  21.         for(i=0;i<8;i++)
  22.         {
  23.             P0 = ~d; //数据取反。
  24.             d<<=1; //左移一位
  25.             mDelay(200);//延时200mS(12M晶振)
  26.         }
  27.     }
  28. }

C51单片机编程规范

阅读数 411

没有更多推荐了,返回首页