×
嵌入式 > 技术百科 > 详情

单片机模块化编程方法

发布时间:2020-06-20 发布时间:
|

  目前我们在学习和开发单片机时广泛采用 c 语言进行编程,当我们开发的单片机项目较小时,或者我们所写的练习程序很小时,我们总是习惯于将所有代码编写在同一个 c 文件下,由于程序代码量较少,通常为几十行或者上百行, 此时这种操作是可行方便的, 也没有什么问题。但如果要开发的项目较大,代码量上千行或者上万行甚至更大,如果你还继续将所有代码全部编写在仅有的一个 c 文件下, 这种方式的弊病会凸显出来, 它会给代码调试、更改及后期维护都会带来极大的不便。试想一下,当你尝试着从几千几万行代码中定位到某一位置或者去寻找某一错误点,上下拉动巨长的滚动条慢慢地、一点点地浏览整个 c 文件, 是件多么令人眼花缭乱,头昏脑胀的事。 模块化编程可解决这个问题,我们只要根据实际需要使用模块化编程的思维将具有不同功能的程序封装在不同模块中,将各个不同模块存放在不同的 c 文件中。 模块化编程后的程序不但使整体的程序功能结构清晰明了,同时也提高程序代码的利用率,有些模块代码我们可以直接进行移植或者经简单修改就可另作他用,好比封装好的函数。
  那么什么是模块化呢?首先我们来简单来聊聊模块概念,我们可能听说过电源模块,通信模块,这些是硬件模块,它们都提供一些接口,譬如电源模块会有输出额定电压电流的接口,通信模块可能提供了 RS232、 USB 等接口。那么对软件来说模块是怎样的呢?软件里的模块跟硬件模块类似,抽象地说就像一个黑盒子,盒子内部细节我们可以不予理会,我们只关心盒子给我们提供什么东西,即提供了什么接口,利用这些接口我们能实现什么功能。
我们把相对独立,具有独立功能用代码编写在一个 c 文件下,把需要对外的函数或变量进行声明供外部使用, 把不需要的细节尽可能对外部屏蔽起来,这就是软件模块化编程的思维。
这样不同的模块占用不同 c 文件,一个个 c 文件将整个项目串接起来实现所有的功能。

  1. 模块化编程的原则:
模块化编程通常要遵循以下几个原则:
   每一个 c 文件对应一个同名的 h 头文件
  一个 h 文件伴随相应 c 文件存在, 头文件是为了声明对外公开的接口。如果一个 c 文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如 main 函数所在的文件,同时 main 函数所在文件可以没有对应的头文件。 如有一个 led.c,那应该同时制作一个 led.h 头文件。
   头文件中适合放置接口的声明,不适合放置实现

  头文件是模块的对外的接口,供外部程序调用。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、变量类型声明等。 函数的实现、变量的赋值、语句的操作等决不能放在头文件中。因为头文件的功能是向外提供接口,譬如函数,变量,具体如何实现是在 c 文件中进行,头文件仅是进行了描述声明。
   任意一个 c 文件只要使用了其他 c 文件提供的接口, 都要同时包含其对应的头文件,每个 c 文件应该头文件自包含

  任意一个 c 文件只要使用了其他 c 文件提供的接口, 都要将其对应的头文件包含到该 c文件中,没有使用到其他 c 文件的接口就不应该将其匹配的头文件包含,并且每个 c 文件都应该包含自己的头文件。
   防止头文件被重复包含
  避免一个头文件被重复包含,通常使用条件编译命令#ifndef--#endif,如下示例:
示例 1:

#ifndef TIME_H#define TIME_H……#endif

 


示例 2:

#ifndef LED_H#define LED_H……#endif

 

  其中#define FILENAME_H 为基本格式, FILENAME_H 为头文件名称,但要全部使用大写形式,使用单下划线后紧跟一个 H 表明是头文件。 不要在宏名最前面加上“ _"或“ __” ,即使用 FILENAME_H 代替_FILENAME_H_, 因为一般以"_"和” __"开头的标识符为系统保留或者标准库使用。

2. 模块化编程实例
  我们使用 AT89C52 单片机,在编程软件 keil 环境下实施一个工程,来说明模块化编程具体操作的方法和步骤。例子要实现的功能:和 P1 相连的 8 个 LED 灯每 500ms 亮灭交替闪烁,通过串口将数字 0-9 发送给单片机并显示在一个数码管上。 LED 闪烁的时间使用定时器 0 中断方式来控制, T0 每 50ms 溢出产生中断,定义一个计数器,每次 T0 中断就计数一次,累计计数 10 次,那么时长为 500ms,作为 LED 闪烁时间间隔。 单片机的时钟为11.0592MHz。那么使用模块化编程的方法, 整个项目将会有如下表中的文件。
表 1 工程文件清单

C 文件H 头文件 描述
main.cMain 文件可以没有对应的头文件
timer0.ctimer0.h定时器 0 定时 50ms 中断
led.c led.hLed 闪烁实现
uart.cuart.h串口通信配置实现
digitron.cdigitron.h数码管显示

 

  2.1 创建工程步骤
  2.1.1 新建工程文件目录
  新建工程文件目录(如 test),在工程目录下创建 Project、 Source、 Output、 Listing 和Readme 这 5 个文件夹,并在文件夹 Readme 下创建 Readme.txt 文件。 这样做的目的是为
了增强工程文件的可读性及结构化,便于维护和管理。
 Project 存放工程文件
 Source 存放用户编写的 c 文件及 h 头文件
 Output 存放各种输出文件,如 hex 文件
 Listing 存放编译过程中产生的各种中间文件
 Readme 存放工程项目的说明文件
  2.1.2 创建 keil 工程
( 1) 启动软件 Keil μVision, 点击工具栏上的 Project,选择 NewμVision Project,新建 test工程到目录 Project 下。

( 2) 选择目标器件,点击“ OK”确认。
( 3) 出现是否添加启动文件到工程中对话框,选择否。
( 4) 目标选项设置, 点击 target option 工具进入选项配置界面。
( 5) 选中 Target 项,根据实际情况设置晶振频率。本例子频率为 11.0592MHz。
( 6) 选中 output 选项,点击“ Select Folder for Objects…”选择工程目录下的 Output 文件夹, 将“ Create HEX File”勾选中。
( 7) 选中 Listing 选项,点击“ Select Folder for Listings…”选择工程目录下的 Listing 文件夹。
( 8) 最后点击“ OK”保存各选项的设置,至此完成 Target Option 的配置。
( 9)新建 5 个文件,以 c 为后缀名保存到工程目录的 Source 文件下,并分别命名为 main.c、led.c、 uart.c、 timer0.c 和 digitron.c。右击“ Source Group1”选择“ Add Existing Flies to Group‘Source Group1’”,将以上新建的 5 个文件添加到工程中。
(10) 至此,就完成整个 keil 工程的创建。
( 11) 工程 test 目录文件结构

  3. 程序代码编写
  3.1 LED 闪烁文件
  首先编写 led.c 文件代码,主要内容就是 LED 闪烁实现的函数,由于使用到 timer0.c 中TimeCounter 变量,故需要包含 timer0.h 头文件。其程序代码如下:

#include #include "led.h"#include "timer0.h"void Light_LED(void)
{    static unsigned char TickFlag = 0;    if (TimeCounter == 0) //判断 500ms 定时是否到    {
        TickFlag = 1;
        TimeCounter = 10;
    }    if (TickFlag == 1) // 每间隔 500ms P1 口取反    {
        P1 = ~P1;
        TickFlag = 0;
    }
}

 


这个文件中,仅有 Light_LED()函数提供给外部使用,故在 led.h 仅声明该函数即可,声明时要使用关键字 extern 表明该函数可供外部模块使用, led.h 头文件内容如下:

#ifndef LED_H#define LED_H    extern void Light_LED(void);#endif

 


3.2 定时器 0 中断文件
定时器 0 文件 timer0.c 代码如下:

#include #include "timer0.h"unsigned char TimeCounter = 10;void Timer0Config(void)
{
    TMOD |= 0x01; //定时器 0 工作在方式 2,即 16 位计数器
    TH0 = 0x3C;
    TL0 = 0xB0; //定时器 0 重赋初值
    ET0 = 1; //使能定时器 0 中断
    TR0 = 1; //开启定时器 0}void timer0(void) interrupt 1 //定时器 0 中断函数{
    TH0 = 0x3C;
    TL0 = 0xB0; //定时器 0 重赋初值
    if (TimeCounter != 0)
    {
        TimeCounter--;
    }
}

 


其对应的头文要向 led.c 文件提供 TimeCounter 变量,向 main.c 提 Timer0Config 函数,因此要在头文件中进行声明,具体内容如下:

#ifndef TIMR0_H#define TIMR0_H    extern unsigned char TimeCounter;    extern void Timer0Config(void);#endif

 


3.3 串口通信文件
串口配置及中断服务程序 uart.c 文件,该文件的代码如下:

#include #include "uart.h"unsigned char tem = 0;void UartConfig(void)
{
    SCON = 0x50; //串口工作在方式 1,即 10 位收发器模式
    PCON = 0x00; //波特率不增倍
    TMOD |= 0x20; //定时器 1 做波特率发生器,工作在方式 2 即 8 位自动重装初值模式
    TH1 = 0xFD; //定器 1 赋初值
    TL1 = TH1;
    ES = 1; //使能串口中断
    TR1 = 1; //开启定时器 1}void InterruptUart(void) interrupt 4 //串口中断服务函数{    if (RI == 1)
    {
        RI = 0;
        tem = SBUF;
    }
}

 


uart.h 头文件向 mian.c 提供变量 tem 和函数 UartConfig(),故要进行声明, 内容如下:

#ifndef UART_H#define UART_H    extern void UartConfig(void);    extern unsigned char tem;#endif

 


3.4 数码管显示文件
数码管显示模块文件 digitron.c 代码如下:

#include #include "digitron.h"unsigned char code table[16] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,
                   0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};void DigiShow(unsigned char num)
{
    P2 = 0xFE; //打开第一个数码管的位码
    P0 = table[num]; //将串口发送过来的数字送到数码上显示}

 


文件 main.c 调用该文件的 DigiShow()函数,因此 digitron.h 头文件要声明这个函数,
其头文件内容如下:

#ifndef DIGITRON_H#define DIGITRON_H    extern void DigiShow(unsigned char num);#endif

 


3.5 主函数所在文件
mian.c 文件是程序代码的入口文件,并且调用其他模块接口,实现所有功能的核心文件, 故需要包含器其他 4 个头文件,其代码如下:

#include #include "led.h"#include "uart.h"#include "timer0.h"#include "digitron.h"void main(void)
{
    Timer0Config(); //初始化定时器 0
    UartConfig(); //初始化串口,设置波特率为 9600
    EA = 1; //使能全局中断
    while (1)
    {
        Light_LED();
        DigiShow(tem);
    }
}

 


『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
Semtech的LoRa技术携手Chipsafer将牧场连接至云端