代码整理_1

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

054.7.1 代码整理(1)

/******************** main.c ********************/
#include "reg52.h"
#include <intrins.h>
#include "delay.h"
#include "main.h"
#include "uart.h"

uchar recv;

void main()
{
    UART_Init();

    while (1)
    {
        UART_Send_Str("I am wfeng!\r\n");
        Delay_xms(1000);
    }
}

/*****************
串口中断处理函数,接收数据
*****************/
void UART_Routine(void) interrupt 4
{
    if (RI == 1) // 判断接收标志位是否为1
    {
        RI = 0;        // RI接收标志位清0
        recv = SBUF;   // 接收数据

        switch (recv)
        {
            case 0x01:
                LED1 = 0;
                break;
            case 0x02:
                LED2 = 0;
                break;
            case 0x03:
                LED3 = 0;
                break;
            case 0x04:
                LED4 = 0;
                break;
            case 0x05:
                P1 |= 0x0F;
                break;
            case 0x06:
                BEEP = 0;
                break;
            case 0x07:
                BEEP = 1;
                break;
            case 0x08:
                JDQ1 = 0;
                break;
            case 0x09:
                JDQ1 = 1;
                break;
            default:
                break;
        }
    }
}

/******************** main.h ********************/
#ifndef _MAIN_H_
#define _MAIN_H_

#include "reg52.h"

typedef unsigned char uchar;
typedef unsigned int uint;

extern uchar recv;

sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
sbit BEEP = P1^6;
sbit JDQ1 = P2^0;

#endif

/******************** Delay/delay.h ********************/
#ifndef _DELAY_H_
#define _DELAY_H_

#include "main.h"

void Delay_xms(uint xms);

#endif

/******************** Delay/delay.c ********************/
#include "delay.h"
#include <intrins.h>

void Delay_xms(uint xms) //@11.0592MHz
{
    uchar data i, j;

    while (xms)
    {
        _nop_();
        i = 2;
        j = 199;
        do
        {
            while (--j);
        } while (--i);
        xms--;
    }
}

/******************** Uart/uart.h ********************/
#ifndef _UART_H_
#define _UART_H_

#include "main.h"

void UART_Send_Byte(uchar send_byte);
void UART_Send_Str(uchar *send_str);
void UART_Init(void);

#endif

/******************** Uart/uart.c ********************/
#include "uart.h"
#include "main.h"

// 串口发送一个字节
void UART_Send_Byte(uchar send_byte)
{
    SBUF = send_byte;
    while (!TI);
    TI = 0;
}

// 串口发送字符串
void UART_Send_Str(uchar *send_str)
{
    while (*send_str != '\0') // 检测字符串结束符
    {
        UART_Send_Byte(*send_str++); // 发送一字节后,指针移向下一字节
    }
}

void UART_Init(void)
{
    SCON = 0x50;   // 串口工作方式1,并允许接收
    PCON &= 0x7F;  // 波特率不加倍
    TMOD &= 0x0F;
    TMOD |= 0x20;  // 定时器1方式2,8位自动重装
    TH1 = 0xFD;
    TL1 = 0xFD;    // 11.0592MHz、12T、SMOD=0时为9600bit/s
    TR1 = 1;       // 启动定时器1
    ES = 1;        // 打开串口中断
    EA = 1;        // 打开总中断
    recv = 0;
}

一、代码整理

1.1 任务目标

51单片机前面学习的基本I/O、外部中断、定时器和UART,是嵌入式开发中最基础、最常见的组成部分。以后学习STM32等功能更强的单片机,仍然要面对GPIO、EXTI、Timer和UART,只是寄存器数量、时钟系统、外设功能及软件层次会更复杂。

本讲不增加硬件电路,也不改变开发板接线和程序功能,目标是把集中在一个 main.c 中的代码按功能拆成多个模块,使工程更容易阅读、复用、排错和扩展。

模块化整理前,所有内容可能都堆在一个文件中:

main.c
├── 类型别名
├── 引脚定义
├── 延时函数
├── 串口初始化
├── 串口发送函数
├── main主循环
└── 串口中断函数

整理后的配套工程结构为:

7.0代码整理/
├── main.c              主程序和串口接收中断
├── main.h              公共类型、引脚名和全局变量声明
├── Delay/
│   ├── delay.c         延时函数定义
│   └── delay.h         延时函数声明
├── Uart/
│   ├── uart.c          串口初始化及发送函数定义
│   └── uart.h          串口函数声明
├── STARTUP.A51         51单片机启动代码
└── mcu51.uvproj        Keil µVision工程配置

模块化的核心不是“文件越多越好”,而是让每个模块职责清楚:

  • main.c负责组织业务流程,不再保存所有底层实现。
  • delay.c/.h只负责延时。
  • uart.c/.h只负责串口初始化与发送。
  • main.h保存多个模块都需要的公共定义。
  • 调用者只需要阅读头文件中的接口,不必先理解模块内部的全部代码。

课程后续还会学习Wi-Fi、I²C、单总线、直流电机、舵机和步进电机等应用。若所有驱动都写进 main.c,文件会迅速膨胀;按外设拆分后,新功能可以按模块加入,主程序仍能保持清晰。

1.2 工程分析

C工程不是把所有 .c 文件简单拼接后一次处理,而是经历预处理、编译和链接

flowchart LR
    A[main.c及其头文件] --> A1[main.obj]
    B[delay.c及其头文件] --> B1[delay.obj]
    C[uart.c及其头文件] --> C1[uart.obj]
    D[STARTUP.A51] --> D1[STARTUP.obj]
    A1 --> L[链接器]
    B1 --> L
    C1 --> L
    D1 --> L
    L --> H[mcu51可执行映像]
    H --> X[mcu51.hex烧录文件]

每个 .c 文件都是一个独立的翻译单元

  1. 预处理器先处理 #include#define#ifndef等指令。
  2. C51编译器分别把 main.cdelay.cuart.c编译成 .obj目标文件。
  3. 链接器把各目标文件、启动代码和库函数连接在一起。
  4. 连接成功后生成供单片机下载的HEX文件。

这解释了 .c.h 的根本区别:

文件类型典型内容是否独立编译
.c实现文件函数定义、变量定义、具体执行代码是,必须加入工程构建
.h头文件函数声明、类型声明、宏、公共接口否,由 #include展开到 .c

#include "delay.h"可以近似理解为:预处理器在编译前把 delay.h的文本放到当前位置。它不会自动把 delay.c一起带入工程。因此:

  • 只包含 delay.h但没有编译 delay.c,编译阶段可能通过,链接阶段会出现找不到 Delay_xms实现的错误。
  • 只把 delay.c加入工程却没有在 main.c声明函数,编译器在处理函数调用时不知道它的参数和返回类型。
  • .h通常不必加入Source Group才能参与编译,只要 #include能够找到它即可;把头文件显示在工程树中主要是为了浏览方便。

配套工程的调用与依赖关系如下:

flowchart TD
    MC[main.c] --> MH[main.h]
    MC --> DH[delay.h]
    MC --> UH[uart.h]
    DC[delay.c] --> DH
    DH --> MH
    DC --> IH[intrins.h]
    UC[uart.c] --> UH
    UH --> MH
    MH --> RH[reg52.h]

这里存在一条值得关注的依赖:delay.h只因为使用了 uint,就必须包含定义 uintmain.h。学习阶段这样组织便于理解;更大的工程通常会把公共整数类型单独放入 types.h,或直接使用明确的标准类型,减少延时模块对主模块的依赖。

1.3 代码整理思路

模块拆分应遵循下面的顺序:

  1. 确定模块职责,例如延时、串口、按键、LED。
  2. .h 中设计其他模块可以调用的接口。
  3. .c 中保存接口的具体实现和模块内部细节。
  4. 把实现文件加入Keil工程,把头文件所在目录加入Include Paths。
  5. 在调用模块中包含头文件,编译并逐个解决错误。

功能拆分后,调用者看到的是接口:

void Delay_xms(uint xms);

实现细节保存在 delay.c

void Delay_xms(uint xms)
{
    /* 具体延时代码 */
}

这种“接口与实现分离”的方式,使以后替换延时算法时不必改动所有调用位置。只要函数名称、参数和返回类型保持一致,main.c仍然可以照常调用。

1.3.1 示例函数整理

本讲以 Delay_xms()为例,把它从 main.c移入独立的Delay模块。需要同时处理四件事:

函数实现  ──> Delay/delay.c
函数声明  ──> Delay/delay.h
公共类型  ──> main.h
函数调用  ──> main.c包含delay.h后调用
1.3.1.1 文件夹保存

可在工程目录中建立 Delay文件夹,集中保存 delay.cdelay.h。文件夹名称不是C语言语法的一部分,可以自行命名,但建议做到:

  • 名称能够直接反映模块功能;
  • 同一工程统一大小写规则;
  • 路径尽量避免过长以及难以识别的特殊字符;
  • 一个模块的接口和实现放在相邻位置。

物理文件夹和Keil左侧的Source Group不是同一个概念:

对象保存位置作用
Windows文件夹磁盘决定文件真实路径
Keil Source Group.uvproj工程配置决定IDE如何分组显示、哪些源文件参与构建

把文件复制到工程目录后,它不会自动进入Keil构建列表;反过来,删除工程树中的条目通常也不等于删除磁盘文件。操作时要分清“从工程移除”和“从磁盘删除”。

1.3.1.2 新建c文件和h文件

推荐直接在Keil中选择新建文本文件,然后分别另存为 delay.cdelay.h;也可用记事本、VS Code等纯文本编辑器创建。Windows默认可能隐藏扩展名,可在文件资源管理器中选择:

查看 → 显示 → 文件扩展名

显示扩展名后,应检查文件真实名称:

正确:delay.c
正确:delay.h
错误:delay.c.txt
错误:delay .h
错误:delay.h.txt

扩展名决定工具怎样识别文件,但只改扩展名不会转换文件内部格式。空白的 .docx也是ZIP结构的Word二进制文档,把它直接重命名为 .c后,内容仍不是纯文本C源码,不能作为可靠的新建方法。C源码和头文件应由纯文本编辑器创建,并保存为Keil能够识别的文本编码。

.c文件保存函数定义,.h文件保存对外声明:

/* delay.h:告诉编译器“这个函数存在,调用方式如下” */
void Delay_xms(uint xms);

/* delay.c:真正提供函数代码 */
void Delay_xms(uint xms)
{
    /* ... */
}

声明末尾有分号,定义后面是函数体。声明可以被多个文件看到,但一个普通函数在整个程序中只能有一个对应定义。

1.3.1.3 添加文件到工程

把实现文件加入Keil µVision工程的方法为:

右键 Source Group 1
  → Add Existing Files to Group 'Source Group 1'
  → 选择 delay.c
  → Add
  → Close

配套 mcu51.uvproj实际参与构建的项目文件是:

STARTUP.A51
main.c
Delay/delay.c
Uart/uart.c

头文件不会单独生成目标文件,所以 delay.huart.hmain.h无需作为C源文件加入构建。可以把它们加入工程树方便双击查看,但真正决定编译器能否找到头文件的是 #include的搜索规则与Include Paths。

这里使用的是Keil µVision工程,不是Qt工程。C51代表8051的C编译器选项页,A51代表汇编器选项页;设置C头文件搜索目录时应进入 Options for Target → C51

1.3.1.4 添加文件路径

由于 delay.huart.h不在工程根目录,需要配置头文件搜索路径:

点击 Options for Target(魔术棒)
  → C51
  → Include Paths 右侧的“...”
  → 添加 .\Delay 和 .\Uart
  → 确认

配套工程中的实际配置为:

.\Delay;.\Uart

添加的是文件夹路径,不是逐个选择 .h文件。编译器遇到:

#include "delay.h"

会在相应位置寻找名为 delay.h的文件。双引号形式优先用于项目自己的头文件,尖括号形式通常用于编译器或库提供的头文件:

#include "delay.h"   // 项目头文件
#include <intrins.h>  // Keil C51工具链头文件

本例中 delay.c还必须包含 <intrins.h>,因为延时函数使用了Keil提供的 _nop_()内在函数。若缺少这个头文件,编译器无法正确识别 _nop_()

头文件保护的作用

delay.h采用条件编译避免同一个翻译单元内重复展开:

#ifndef _DELAY_H_
#define _DELAY_H_

#include "main.h"
void Delay_xms(uint xms);

#endif

预处理过程如下:

第一次包含delay.h
  ├─ _DELAY_H_尚未定义
  ├─ 执行#define _DELAY_H_
  └─ 保留头文件正文

同一翻译单元再次包含delay.h
  ├─ _DELAY_H_已经定义
  └─ 跳过#ifndef到#endif之间的正文

保护宏在整个工程中必须唯一。配套代码使用 _DELAY_H__MAIN_H__UART_H_;编写新工程时更推荐使用不以下划线加大写字母开头的形式,例如:

#ifndef VIDEO_LEARN_DELAY_H
#define VIDEO_LEARN_DELAY_H
/* ... */
#endif

这是因为以下划线开头并紧跟大写字母的标识符通常保留给编译器和系统实现使用。

为什么 main.h也需要保护

main.c的一次预处理过程中,main.h可能沿多条路径出现:

main.c ───────────────> main.h
main.c → delay.h ─────> main.h
main.c → uart.h ──────> main.h

如果 main.h没有保护,同一个翻译单元可能反复看到类型和引脚声明。保护宏确保其有效内容只展开一次。

公共变量的声明与定义

配套工程用下面两行共享 recv

/* main.h:声明,不分配新的存储空间 */
extern uchar recv;

/* main.c:定义,真正分配存储空间 */
uchar recv;

extern告诉其他模块:“这个变量定义在别处”。如果直接在头文件中定义普通全局变量,头文件又被多个 .c包含,链接时可能出现多重定义。通常应在一个 .c中定义,在需要使用它的头文件中写 extern声明。

1.3.1.5 编译错误处理

一次失误可能引发一长串连锁错误,因此应从编译输出中的第一条有效错误开始处理。双击Keil错误信息,可以跳转到对应文件和行号。

拆分模块时常见问题如下:

现象常见原因检查方法
delay.h无法打开Include Paths未包含Delay目录,或文件名错误检查真实后缀、路径及大小写
Delay_xms未声明main.c没有包含 delay.h添加 #include "delay.h"
uint附近出现语法错误当前翻译单元尚未见到 uint类型定义delay.h包含提供类型的头文件
类型或符号重复定义公共内容重复书写,或头文件没有保护集中定义并检查保护宏
_nop_无法识别delay.c缺少 <intrins.h>包含Keil内在函数头文件
UNRESOLVED EXTERNAL有函数声明,但相应 .c未参与构建,或名称不一致检查Source Group和函数签名
多个错误同时出现前面的语法或包含错误引发级联诊断先修复第一条,再重新编译

main.c调用 Delay_xms()时,编译器只需要先看到声明;到链接阶段,链接器才去 delay.obj中寻找函数实现:

main.c看到delay.h声明
        │
        ▼
生成“需要Delay_xms”的main.obj
        │
        ├──────────────┐
        ▼              ▼
delay.c编译成delay.obj  链接器把调用与实现连接
                       │
                       ▼
                    生成HEX

成功编译和链接后,还应下载到开发板进行功能验证。本讲整理前后仍然周期性发送 I am wfeng!\r\n,并保留串口接收控制LED、蜂鸣器和继电器的功能;模块拆分只改变代码组织,不应改变运行结果。下载工具若无法打开串口,应先关闭仍占用同一COM口的串口助手,再重新下载。