代码整理_2

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

055.7.2 代码整理(2)

/* ==================== main.c:业务流程与串口接收中断 ====================
 * 整体目的:初始化 UART 后,每隔约 1 秒发送一行测试字符串;串口收到指定命令字节时,
 *           在中断中控制 LED、蜂鸣器和继电器。
 * 模块关系:本文件只调用 delay.h 与 uart.h 暴露的接口;延时、发送和初始化的具体实现
 *           分别保留在 delay.c、uart.c 中,正是本节“代码整理”的结果。
 * 硬件前提:LED、蜂鸣器和继电器均为低电平有效;因此写入 0 是使相应引脚灌电流、设备动作。
 */
#include "reg52.h"    /* 引入 8051/52 特殊功能寄存器声明;本文件中的 RI、SBUF 等名称由此提供。 */
#include <intrins.h>   /* 保留配套源码的 Keil C51 内在函数头文件;本文件当前没有直接调用其中的函数。 */
#include "delay.h"    /* 引入 Delay_xms() 的函数声明,使 main.c 能按正确参数调用延时模块。 */
#include "main.h"     /* 引入 uchar、公共引脚别名,以及 recv 的外部声明。 */
#include "uart.h"     /* 引入 UART_Init()、UART_Send_Str() 等串口模块对外接口。 */

uchar recv;            /* 真正定义并分配 1 字节存储空间;UART_Routine() 把最新接收的数据保存于此。 */

void main()             /* C51 的用户入口;启动代码完成复位后的准备工作后会进入此函数。 */
{                       /* 主函数体开始。 */
    UART_Init();        /* 配置串口、定时器 1 和串口中断,使发送与异步接收都具备工作条件。 */

    while (1)           /* 条件恒为真;单片机程序需要持续运行,不能执行一次后退出。 */
    {                   /* 无限主循环的一轮开始。 */
        UART_Send_Str("I am wfeng!\r\n"); /* 逐字节发送字符串;\r\n 使常见串口助手换到下一行行首。 */
        Delay_xms(1000);                  /* 忙等约 1000 ms,避免字符串以人眼难以阅读的速度连续刷出。 */
    }                   /* 本轮结束后立即回到 while (1) 顶部,重复发送。 */
}                       /* 语法边界;正常运行时不会离开无限循环。 */

/*
 * 函数目的:响应 UART 串口中断,并把收到的单字节命令转换成对应的板载设备动作。
 * 调用时机:串口硬件完整接收一个字节后置位 RI;因 UART_Init() 已打开 ES、EA,CPU 会跳到这里。
 * 重要限制:本例只检查 RI,未处理发送完成标志 TI;因为主循环采用查询 TI 的方式完成发送。
 */
void UART_Routine(void) interrupt 4 /* Keil C51 的 interrupt 4 指定 8051 串口中断向量(地址 0023H)。 */
{                                   /* 串口中断服务程序体开始;应尽量短小,以免阻塞其他实时工作。 */
    if (RI == 1)                    /* 仅当硬件确认“已收到一个完整字节”时才读取接收缓冲寄存器。 */
    {                               /* 接收事件的处理分支开始。 */
        RI = 0;                     /* 软件清除接收完成标志,为接收下一个字节做好准备。 */
        recv = SBUF;                /* 从串口数据缓冲寄存器读出刚收到的 8 位数据,保存为命令码。 */

        switch (recv)               /* 按命令码选择一个确定的外设控制动作。 */
        {                           /* switch 的各命令分支开始。 */
            case 0x01:              /* 命令 0x01:请求点亮 LED1。 */
                LED1 = 0;           /* P1.0 输出低电平;LED1 低电平有效,因此点亮。 */
                break;              /* 结束本分支,防止继续执行后面的 LED2 分支。 */
            case 0x02:              /* 命令 0x02:请求点亮 LED2。 */
                LED2 = 0;           /* P1.1 输出低电平,点亮 LED2;其他 LED 状态保持原样。 */
                break;              /* 结束 0x02 的处理。 */
            case 0x03:              /* 命令 0x03:请求点亮 LED3。 */
                LED3 = 0;           /* P1.2 输出低电平,点亮 LED3。 */
                break;              /* 结束 0x03 的处理。 */
            case 0x04:              /* 命令 0x04:请求点亮 LED4。 */
                LED4 = 0;           /* P1.3 输出低电平,点亮 LED4。 */
                break;              /* 结束 0x04 的处理。 */
            case 0x05:              /* 命令 0x05:请求关闭四路 LED。 */
                P1 |= 0x0F;         /* 将 P1.0~P1.3 置 1 而不改 P1.4~P1.7;低四位 LED 因高电平而熄灭。 */
                break;              /* 结束 0x05 的处理。 */
            case 0x06:              /* 命令 0x06:请求打开蜂鸣器。 */
                BEEP = 0;           /* P1.6 输出低电平;蜂鸣器低电平有效,因此开始鸣叫。 */
                break;              /* 结束 0x06 的处理。 */
            case 0x07:              /* 命令 0x07:请求关闭蜂鸣器。 */
                BEEP = 1;           /* P1.6 输出高电平,撤销蜂鸣器的有效控制电平。 */
                break;              /* 结束 0x07 的处理。 */
            case 0x08:              /* 命令 0x08:请求吸合继电器 1。 */
                JDQ1 = 0;           /* P2.0 输出低电平;经驱动电路后使继电器线圈得电吸合。 */
                break;              /* 结束 0x08 的处理。 */
            case 0x09:              /* 命令 0x09:请求释放继电器 1。 */
                JDQ1 = 1;           /* P2.0 输出高电平;经驱动电路后使继电器线圈断电释放。 */
                break;              /* 结束 0x09 的处理。 */
            default:                /* 其他命令码没有在本实验定义功能。 */
                break;              /* 不改变任何外设状态,直接结束本次中断处理。 */
        }                           /* switch 结束。 */
    }                               /* RI 接收处理结束;若此次仅由 TI 触发,则不会进入此块。 */
}                                   /* 中断服务程序结束;CPU 返回被中断的主循环位置。 */

/* ==================== main.h:跨模块共享的声明 ==================== */
#ifndef _MAIN_H_        /* 若本翻译单元尚未包含 main.h,则保留下方正文。 */
#define _MAIN_H_        /* 定义保护宏;同一翻译单元再次包含本文件时会跳过正文。 */

#include "reg52.h"      /* 为 P1、P2 及 sbit 位定义提供寄存器声明。 */

typedef unsigned char uchar; /* 给 8 位无符号类型起短名;本工程用它表示一个字节。 */
typedef unsigned int uint;   /* 给 16 位无符号类型起短名;可表示本例的毫秒延时参数。 */

extern uchar recv;      /* 声明 recv 的实体在其他 .c 文件中;本工程的定义位于 main.c。 */

sbit LED1 = P1^0;       /* 将 LED1 映射为可位寻址的 P1.0;^ 是 C51 位定位语法。 */
sbit LED2 = P1^1;       /* 将 LED2 映射为 P1.1。 */
sbit LED3 = P1^2;       /* 将 LED3 映射为 P1.2。 */
sbit LED4 = P1^3;       /* 将 LED4 映射为 P1.3。 */
sbit BEEP = P1^6;       /* 将蜂鸣器控制信号映射为 P1.6。 */
sbit JDQ1 = P2^0;       /* 将继电器 1 的控制信号映射为 P2.0。 */

#endif                  /* 与 #ifndef _MAIN_H_ 配对,结束 main.h 的防重复包含区域。 */

/* ==================== Delay/delay.h:延时模块接口 ==================== */
#ifndef _DELAY_H_       /* 检查 delay.h 是否已经在当前翻译单元展开过。 */
#define _DELAY_H_       /* 定义延时模块的保护宏。 */

#include "main.h"       /* 取得 uint 类型定义,确保下面的函数声明能被正确解析。 */

void Delay_xms(uint xms); /* 仅声明延时接口;具体函数体在 Delay/delay.c,单位是约毫秒。 */

#endif                  /* 结束 delay.h 的防重复包含区域。 */

/* ==================== Delay/delay.c:软件延时实现 ====================
 * 函数目的:按 xms 指定的次数执行约 1 ms 忙等,为简单演示提供时间间隔。
 * 时间前提:内部循环参数针对 11.0592 MHz、12T 内核及 Keil C51 生成代码设计;时钟或优化改变时需重新校准。
 * 使用限制:忙等期间 CPU 不执行其他前台任务,精确计时或并发任务应使用定时器。
 */
#include "delay.h"      /* 引入本模块自己的函数声明和所需的 uint、uchar 类型定义。 */
#include <intrins.h>     /* 引入 _nop_() 的 Keil C51 内在函数声明。 */

void Delay_xms(uint xms) /* 定义延时函数;xms 是要重复执行“约 1 ms”循环的次数。 */
{                        /* 延时函数体开始。 */
    uchar data i, j;     /* 定义两个 8 位计数器;data 是 C51 对片内直接寻址 RAM 的存储限定。 */

    while (xms)          /* 只要还有未完成的毫秒数,就继续执行一轮校准后的空转。 */
    {                    /* 一轮约 1 ms 延时开始。 */
        _nop_();         /* 执行一条空操作指令,用于补齐本延时循环的指令周期。 */
        i = 2;           /* 初始化外层计数器;该常数是延时校准参数的一部分。 */
        j = 199;         /* 初始化内层计数器;它决定每次外层循环的空转次数。 */
        do               /* 使用后判断循环,使内层计数至少执行一次。 */
        {                /* 外层 do 循环体开始。 */
            while (--j); /* 每次先递减 j;非零继续空循环,到零才退出内层循环。 */
        } while (--i);   /* j 用尽后递减 i;i 非零时重入循环,8 位 j 在下一次 --j 时由 0 回绕为 255,形成下一轮空转。 */
        xms--;           /* 已完成约 1 ms,减少剩余延时计数。 */
    }                    /* xms 为 0 时跳出,延时结束。 */
}                        /* 返回调用位置。 */

/* ==================== Uart/uart.h:串口模块接口 ==================== */
#ifndef _UART_H_        /* 检查 uart.h 是否已经在当前翻译单元展开过。 */
#define _UART_H_        /* 定义串口模块的保护宏。 */

#include "main.h"       /* 取得 uchar 类型定义,供下方串口函数的参数使用。 */

void UART_Send_Byte(uchar send_byte); /* 声明“发送一个字节”的接口。 */
void UART_Send_Str(uchar *send_str);  /* 声明“发送以 \0 结尾字符串”的接口;参数是首字符地址。 */
void UART_Init(void);                 /* 声明 UART、波特率定时器和相关中断的初始化接口。 */

#endif                  /* 结束 uart.h 的防重复包含区域。 */

/* ==================== Uart/uart.c:串口初始化与发送实现 ==================== */
#include "uart.h"       /* 引入本模块的对外声明及其依赖的公共类型。 */
#include "main.h"       /* 引入 SFR 相关的公共定义;按配套源码保留此直接包含。 */

/* 函数目的:将一个 8 位数据写入 UART 发送缓冲区,并等待硬件把它从 TXD 引脚完整发送出去。 */
void UART_Send_Byte(uchar send_byte) /* send_byte 是本次要发送的一个字节。 */
{                                    /* 单字节发送函数体开始。 */
    SBUF = send_byte;                /* 写入串口数据缓冲寄存器,硬件随后按 UART 格式从 TXD 移出该字节。 */
    while (!TI);                     /* 查询发送完成标志 TI;分号表示等待期间故意不执行循环体。 */
    TI = 0;                          /* 软件清除 TI,避免下一次发送误把旧的完成状态当作新完成。 */
}                                    /* 单字节发送结束。 */

/* 函数目的:从字符串首地址开始,逐个发送字符,直到遇到 C 字符串结束符 \0。 */
void UART_Send_Str(uchar *send_str)  /* 指针参数指向待发送字符串的第一个字符。 */
{                                    /* 字符串发送函数体开始。 */
    while (*send_str != '\0')       /* 解引用读取当前字符;不是结束符时才继续发送。 */
    {                                /* 当前字符的处理体开始。 */
        UART_Send_Byte(*send_str++); /* 取当前字符发送,再把指针后移到下一个字符;后缀 ++ 在取值后生效。 */
    }                                /* 遇到 \0 后退出;结束符本身不发送。 */
}                                    /* 字符串发送结束。 */

/*
 * 函数目的:配置 8051 UART 的方式 1,并以定时器 1 的自动重装溢出率生成 9600 bit/s 波特率。
 * 参数前提:11.0592 MHz 晶振、传统 12T 8051 时序、SMOD=0;此时 TH1=TL1=0xFD 对应 9600 bit/s。
 * 中断关系:ES 只允许串口中断源,EA 是总开关;二者同时为 1 后 RI 可触发 UART_Routine()。
 */
void UART_Init(void)                 /* 初始化不接收参数,也不向调用者返回数值。 */
{                                    /* UART 初始化函数体开始。 */
    SCON = 0x50;                     /* 设置串口方式 1(8 位 UART、可变波特率),并置 REN=1 允许接收。 */
    PCON &= 0x7F;                    /* 清除 SMOD 位,使波特率不倍增;保留 PCON 的其余位状态。 */
    TMOD &= 0x0F;                    /* 清除定时器 1 的高四位配置,保留定时器 0 的低四位配置。 */
    TMOD |= 0x20;                    /* 将定时器 1 设为方式 2:8 位自动重装,作为 UART 波特率发生器。 */
    TH1 = 0xFD;                      /* 写入定时器 1 重装值;溢出后硬件从此值重新开始计数。 */
    TL1 = 0xFD;                      /* 初始化当前计数值,避免首次溢出的时间与后续周期不同。 */
    TR1 = 1;                         /* 置位定时器 1 运行控制位,使其开始产生 UART 所需的溢出节拍。 */
    ES = 1;                          /* 打开串口中断源;接收完成时允许提出中断请求。 */
    EA = 1;                          /* 打开 CPU 总中断开关;未置位时任何已使能外设中断都不会响应。 */
    recv = 0;                        /* 清空共享接收变量,使上电后的初始命令状态明确。 */
}                                    /* 初始化完成,返回 main(),随后主循环可安全调用发送函数。 */

一、代码整理

1.1 精简代码

延时函数已经放入 Delay/delay.c,公共类型、引脚名称和跨文件变量声明集中放入 main.h。这里所谓“精简”,不是删除功能,而是把不同职责交给不同模块,使 main.c只保留程序入口、主循环和本讲仍放在主模块中的串口接收中断。

整理后的 main.c主体非常直接:

void main()
{
    UART_Init();

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

从主函数可以立即看出程序行为:先初始化UART,再每隔约1秒发送一次字符串。延时算法和串口寄存器配置都被隐藏在对应模块内部。

视频中把 sbit引脚定义移入 main.h后,编译器提示 P1未定义。原因不是LED引脚接线发生变化,而是C编译器处理 main.h时,必须先知道符号 P1代表什么。

reg52.h、P1与LED别名的依赖关系如下:

reg52.h
   │
   ├── 声明P0、P1、P2、P3等51单片机特殊功能寄存器
   │
   ▼
main.h
   ├── sbit LED1 = P1^0;
   ├── sbit LED2 = P1^1;
   ├── sbit LED3 = P1^2;
   └── sbit LED4 = P1^3;

因此 main.h需要先包含器件寄存器头文件:

#include "reg52.h"

在Keil C51语法中:

sbit LED1 = P1^0;

表示把位变量 LED1映射到P1口的第0位。此处的 P1^0位于Keil的 sbit声明语法中,表示SFR的位地址,并不是普通C表达式中的按位异或计算。

P1特殊功能寄存器

位序号:   7    6    5    4    3    2    1    0
         ┌────┬────┬────┬────┬────┬────┬────┬────┐
P1:     │P1.7│P1.6│P1.5│P1.4│P1.3│P1.2│P1.1│P1.0│
         └────┴────┴────┴────┴────┴────┴────┴────┘
                                      │    │    │    │
                                     LED4 LED3 LED2 LED1

一个头文件应尽量做到“自包含”:它公开的声明用到了什么类型或寄存器,就应包含能够提供这些名称的头文件,不能依赖调用者碰巧先包含了某个文件。否则改变 #include顺序就可能出现难以理解的未定义错误。

main.h中使用的名称来源
P1、P2reg52.h
uchar、uintmain.h自己的 typedef
recv在 main.c中定义,在 main.h中用 extern声明
LED1~LED4、BEEP、JDQ1main.h中的Keil sbit声明

1.2 将代码放到不同的文件

串口相关代码可封装为独立的Uart模块:

Uart/
├── uart.h   对外公开“可以调用什么”
└── uart.c   保存“具体怎样实现”

uart.h公开三个接口:

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

uart.c保存三个函数的完整实现。其他文件只要包含 uart.h,就能按照统一的函数签名调用这些接口。

flowchart LR
    M[main.c] -->|包含并调用| H[uart.h 公共接口]
    C[uart.c] -->|包含并实现| H
    M -->|调用UART_Init| O1[uart.obj中的UART_Init]
    M -->|调用UART_Send_Str| O2[uart.obj中的UART_Send_Str]
    O2 --> O3[UART_Send_Byte]
    O3 --> S[SBUF与TXD硬件]

完整操作顺序如下:

  1. 在工程目录建立 Uart文件夹。
  2. 使用Keil或纯文本编辑器创建 uart.c和 uart.h,确认不存在空格或隐藏的 .txt后缀。
  3. 把 uart.c加入Keil的Source Group,使其参与编译。
  4. 在 Options for Target → C51 → Include Paths中加入 ./Uart。
  5. 把串口发送函数放入 uart.c,把相应声明放入 uart.h。
  6. uart.c和调用串口接口的 main.c都包含 uart.h。
  7. 把串口寄存器配置封装为 UART_Init(),并在 main()进入循环前调用。

.c文件必须加入工程构建;.h不单独编译,只需能被 #include找到。把头文件显示在工程树中可以方便浏览,但不是链接函数实现的必要条件。

声明、定义和调用的写法

/* uart.h:函数声明,分号结尾 */
void UART_Init(void);

/* uart.c:函数定义,带函数体 */
void UART_Init(void)
{
    /* 串口初始化代码 */
}

/* main.c:函数调用,只写实参列表和分号 */
UART_Init();

声明与定义中的 void表示该函数不接收参数;调用时括号里没有实参,不能写成 UART_Init(void);。后者在函数体内会被当作声明形式,而不是正常的函数调用语句。

UART_Init()封装了什么

flowchart TD
    I[调用UART_Init] --> A[SCON=0x50<br/>方式1并允许接收]
    A --> B[PCON.SMOD=0<br/>波特率不加倍]
    B --> C[定时器1方式2<br/>TH1=TL1=0xFD]
    C --> D[TR1=1<br/>启动定时器1]
    D --> E[ES=1、EA=1<br/>允许串口中断]
    E --> F[UART可收发数据]

在11.0592 MHz、传统12T、SMOD=0的条件下,0xFD产生9600 bit/s。模块化只把这些语句移入函数,并未改变寄存器配置。

配套代码最后才执行 recv = 0;,但此前已经执行 EA = 1。更稳妥的初始化顺序是先给共享状态设置初值,最后再开放中断,避免在极短的时间窗口内发生接收中断后,刚收到的数据又被初始化语句覆盖:

recv = 0;
ES = 1;
EA = 1;

Keil中的 Go to Definition可以根据工程符号信息跳到函数定义。若无法跳转,应先确认:

  • 函数声明与定义的名称、大小写和参数完全一致;
  • 对应 .c已经加入工程;
  • 工程已经成功编译并建立浏览信息;
  • 头文件搜索路径正确。

1.3 外部声明

每个 .c独立编译。main.c中的全局变量 recv不会因为另一个文件也属于同一工程,就自动在 uart.c中变成已知名称。跨文件共享普通全局变量需要“一处定义,多处 extern声明”。

/* main.c:定义,分配唯一的存储空间 */
uchar recv;

/* main.h:外部声明,不再分配一份存储空间 */
extern uchar recv;

/* uart.c:包含声明后即可引用同一变量 */
#include "main.h"

void UART_Init(void)
{
    recv = 0;
}

编译和链接过程可以这样理解:

flowchart LR
    A[main.c<br/>uchar recv] --> A1[main.obj<br/>提供符号recv及存储]
    B[main.h<br/>extern uchar recv] --> C[uart.c使用recv]
    C --> C1[uart.obj<br/>产生对recv的外部引用]
    A1 --> L[链接器]
    C1 --> L
    L --> R[把uart.obj中的recv引用<br/>连接到main.obj的唯一存储]

定义与声明的区别

写法性质是否分配存储应放在哪里
uchar recv;定义是只放在一个 .c中
uchar recv = 0;带初值的定义是只放在一个 .c中
extern uchar recv;外部声明否可放在公共 .h中
static uchar recv;文件内部定义是仅当前 .c可见,不能作为同名外部变量共享

extern不是“把变量变成全局变量”的魔法。它告诉编译器:该名称和类型已经声明,真正定义由链接器在其他目标文件中寻找。要成功连接,必须满足:

  • 某一个 .c中确实存在且只存在一个外部定义;
  • 声明与定义的类型完全一致;
  • 定义不能带 static内部链接;
  • 提供定义的 .c必须参与工程构建。

为什么 extern声明不应写初始化值

推荐写法是:

/* main.h */
extern uchar recv;

/* main.c */
uchar recv = 0;

不要在公共头文件中写:

extern uchar recv = 0;

在标准C的文件作用域中,带初始化器的 extern会成为定义,而不再是单纯的外部声明;某些C51编译器还可能直接拒绝这种形式。若头文件被多个 .c包含,就可能生成多份定义并导致编译或链接错误。因此,初始化值应写在唯一的 .c定义处。

类型必须先被声明

编译器从上到下处理声明。下面的顺序正确:

typedef unsigned char uchar;
extern uchar recv;

如果 extern uchar recv;出现在 uchar类型别名之前,编译器尚不知道 uchar是什么,会在类型附近报错。

与中断共享时的 volatile

recv由串口中断函数写入。如果主循环或其他普通函数也要持续读取它,建议声明成:

/* main.c */
volatile uchar recv;

/* main.h */
extern volatile uchar recv;

volatile告诉编译器,该变量可能被中断等当前执行流之外的因素改变,每次使用时都应按规则重新访问存储位置。声明和定义必须同时保留相同的 volatile限定。

volatile只解决编译器优化可见性,不自动保证复杂操作的原子性。若主程序与中断共同读写多字节变量或执行“读—改—写”操作,还要考虑临界区或短暂屏蔽相关中断。

1.4 任务信息资源导图组件库总结

头文件保护的工作过程

以 main.h为例:

#ifndef _MAIN_H_
#define _MAIN_H_

/* main.h的有效内容 */

#endif

#ifndef的意思是“如果这个宏尚未定义”。第一次包含时,保护宏还不存在,正文被保留,并立即执行 #define;同一翻译单元再次包含时,条件为假,正文被跳过。

main.c的一次预处理

第1次遇到main.h
  _MAIN_H_未定义 → 定义宏 → 展开正文

第2次遇到main.h
  _MAIN_H_已定义 → 跳过正文

第3次遇到main.h
  _MAIN_H_已定义 → 跳过正文

配套代码存在多条间接包含路径:

flowchart TD
    M[main.c] --> D[delay.h]
    M --> U[uart.h]
    M --> H[main.h]
    D --> H
    U --> H

没有头文件保护时,main.h可能在编译 main.c时展开三次;有保护后,只有第一次保留正文。

头文件保护有两个重要边界:

  1. 它防止的是同一翻译单元中的重复展开。main.c和 uart.c分别编译时,各自都会正常处理一次 main.h。
  2. 它不能修复头文件中不应出现的普通全局变量定义。共享变量仍应采用 .c中唯一一次定义、.h中 extern声明的方式。

保护宏必须唯一。配套代码使用 _MAIN_H_、_DELAY_H_、_UART_H_;编写自己的头文件时,建议避免以下划线加大写字母开头,采用更不易冲突的名称:

#ifndef VIDEO_LEARN_UART_H
#define VIDEO_LEARN_UART_H

/* 声明 */

#endif

双引号与尖括号

#include "uart.h"
#include <intrins.h>

两者的核心区别是编译器采用的搜索路径和顺序,不是文件内容本身发生变化。通常约定:

  • "文件名"用于工程自己的头文件,优先从当前工程或源文件附近寻找,再查配置的包含目录。
  • <文件名>用于编译器、标准库或芯片工具链提供的头文件,主要从系统和配置的包含目录寻找。

具体搜索顺序由编译器实现决定。Keil C51中,双引号形式会先搜索当前目录和源文件目录,找不到后再按尖括号形式继续搜索;Include Paths决定额外的头文件目录。

reg52.h通常也是Keil提供的器件头文件,因此写成 <reg52.h>更能表达它来自工具链;配套工程使用 "reg52.h"仍可由Keil的搜索规则找到对应文件。intrins.h提供 _nop_()等Keil内在函数声明。

整理后的关键规则

规则正确做法
功能拆分一个模块使用对应的 .c/.h组织实现与接口
函数接口.h声明,.c定义,调用者包含 .h
源文件构建每个实现函数所在的 .c必须加入工程
头文件路径把头文件所在目录加入C51的Include Paths
公共变量一个 .c定义,公共 .h中写 extern声明
中断共享变量根据访问方式考虑 volatile与原子性
重复包含每个头文件使用唯一保护宏
依赖顺序头文件先获得它所使用的类型、寄存器和宏定义
排查错误从第一条有效错误开始,重新编译后再看后续问题

经过整理,main.c只负责组织流程,Delay模块负责延时,Uart模块负责串口初始化和发送,公共头文件负责接口与共享声明。模块之间通过明确的声明连接,而不是靠复制代码或偶然的包含顺序工作。