代码整理_1

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

054.7.1 代码整理(1)

/* ==================== 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 任务目标

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.c、delay.c、uart.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,就必须包含定义 uint的 main.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.c和 delay.h。文件夹名称不是C语言语法的一部分,可以自行命名,但建议做到:

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

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

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

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

1.3.1.2 新建c文件和h文件

推荐直接在Keil中选择新建文本文件,然后分别另存为 delay.c和 delay.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.h、uart.h、main.h无需作为C源文件加入构建。可以把它们加入工程树方便双击查看,但真正决定编译器能否找到头文件的是 #include的搜索规则与Include Paths。

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

1.3.1.4 添加文件路径

由于 delay.h和 uart.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口的串口助手,再重新下载。