欢迎光临
我们一直在努力

【0基础嵌入式学习日志】Day03:多文件拆分、函数声明与 Makefile 多源文件编译

【0基础嵌入式学习日志】Day03:多文件拆分、函数声明与 Makefile 多源文件编译

一、前言

今天继续进行嵌入式 C 语言基础学习。前两天主要完成了 C 工程结构、头文件、结构体、故障码、函数封装、结构体指针以及 Makefile 的基础使用。

Day02 中虽然已经把功能拆成了多个函数,但是这些函数仍然全部写在 main.c 里面。随着功能越来越多,main.c 会越来越臃肿,不利于后期维护。

因此,Day03 的重点是学习 C 工程中的多文件拆分,也就是把不同功能拆成不同的 .h 和 .c 文件:

.h 文件:负责函数声明
.c 文件:负责函数实现
main.c:负责主流程调用
Makefile:负责编译多个源文件

本次练习的核心目标是从“写一个 C 文件”过渡到“写一个简单 C 工程”。


二、Day03 学习目标

本次 Day03 主要学习以下内容:

  • 理解为什么要进行多文件拆分;
  • 理解 .h 文件和 .c 文件的区别;
  • 学会使用头文件声明函数;
  • 学会在源文件中实现函数;
  • 学会让 main.c 只保留主流程;
  • 学会使用 Makefile 编译多个 .c 文件;
  • 继续巩固结构体指针和故障码判断逻辑。

  • 三、工程结构

    Day03 的工程目录如下:

    day03
    ├── Makefile
    ├── include
    │ ├── fault_code.h
    │ ├── fault.h
    │ ├── sensor.h
    │ ├── system.h
    │ └── system_type.h
    ├── src
    │ ├── fault.c
    │ ├── main.c
    │ ├── sensor.c
    │ └── system.c
    └── build
    └── day03_test

    其中:

    • include:存放头文件;
    • src:存放源文件;
    • build:存放编译生成的可执行文件;
    • Makefile:管理编译命令;
    • README.md:记录项目说明。

    相比 Day02,Day03 最大的变化是:不再把所有函数都写在 main.c 中,而是按照功能拆分为不同模块。


    四、为什么要多文件拆分?

    Day02 中,所有函数都写在 main.c 中,例如:

    system_init();
    sensor_update();
    fault_check();
    system_print();
    main();

    这种写法在代码较少时可以使用,但随着项目变大,会出现几个问题:

  • main.c 文件越来越长;
  • 不同功能混在一起,不容易维护;
  • 修改某个功能时容易影响其他代码;
  • 不符合真实嵌入式工程的组织方式。
  • 真实嵌入式项目中,通常会按照功能划分模块,例如:

    uart.c / uart.h
    can.c / can.h
    sensor.c / sensor.h
    motor.c / motor.h
    fault.c / fault.h
    system.c / system.h

    因此,Day03 将系统功能拆分为三个模块:

    system 模块:负责系统初始化和状态打印
    sensor 模块:负责传感器数据更新
    fault 模块:负责故障检测


    五、基础数据类型:system_type.h

    system_type.h 用于定义系统状态结构体。

    #ifndef SYSTEM_TYPE_H
    #define SYSTEM_TYPE_H

    typedef struct
    {
    int led_state;
    float voltage;
    float current;
    float temperature;
    unsigned int fault_code;
    } SystemStatus;

    #endif

    这里定义了一个结构体 SystemStatus,用于统一管理系统状态。

    结构体中包含:

    • led_state:LED 状态;
    • voltage:电压;
    • current:电流;
    • temperature:温度;
    • fault_code:故障码。

    使用结构体后,可以将系统相关数据统一封装到一个变量中:

    SystemStatus sys;

    这样 sys 就可以表示当前系统的整体状态。


    六、故障码定义:fault_code.h

    fault_code.h 用于定义故障码。

    #ifndef FAULT_CODE_H
    #define FAULT_CODE_H

    #define FAULT_NONE 0x0000
    #define FAULT_LOW_VOLTAGE 0x0001
    #define FAULT_OVER_CURRENT 0x0002
    #define FAULT_OVER_TEMP 0x0004
    #define FAULT_SENSOR_ERROR 0x0008

    #endif

    这里使用宏定义表示不同故障。

    其中:

    • FAULT_NONE:无故障;
    • FAULT_LOW_VOLTAGE:低电压故障;
    • FAULT_OVER_CURRENT:过电流故障;
    • FAULT_OVER_TEMP:过温故障;
    • FAULT_SENSOR_ERROR:传感器异常故障。

    采用 0x0001、0x0002、0x0004 这样的数值,是为了让每一种故障占用不同的 bit 位。

    这样一个 fault_code 变量就可以同时表示多个故障。例如:

    FAULT_LOW_VOLTAGE | FAULT_OVER_CURRENT | FAULT_OVER_TEMP

    结果为:

    0x0001 | 0x0002 | 0x0004 = 0x0007

    这表示同时发生了低电压、过电流和过温故障。


    七、system 模块

    system 模块由两个文件组成:

    system.h
    system.c

    其中:

    system.h:声明系统相关函数
    system.c:实现系统相关函数


    八、system.h:函数声明

    system.h 内容如下:

    #ifndef SYSTEM_H
    #define SYSTEM_H

    #include "system_type.h"

    void system_init(SystemStatus *sys);
    void system_print(const SystemStatus *sys);

    #endif

    这里声明了两个函数:

    void system_init(SystemStatus *sys);
    void system_print(const SystemStatus *sys);

    其中:

    • system_init() 用于初始化系统状态;
    • system_print() 用于打印系统状态。

    需要注意的是,system.h 中只写函数声明,不写函数具体实现。


    九、system.c:函数实现

    system.c 用于实现 system.h 中声明的函数。

    #include <stdio.h>
    #include "system.h"
    #include "fault_code.h"

    void system_init(SystemStatus *sys)
    {
    sys->led_state = 0;
    sys->voltage = 12.5f;
    sys->current = 1.2f;
    sys->temperature = 35.6f;
    sys->fault_code = FAULT_NONE;
    }

    void system_print(const SystemStatus *sys)
    {
    printf("LED state: %d\\n", sys->led_state);
    printf("Voltage: %.2f V\\n", sys->voltage);
    printf("Current: %.2f A\\n", sys->current);
    printf("temperature: %.2f C\\n", sys->temperature);
    printf("Fault code: 0x%04X\\n", sys->fault_code);

    if (sys->fault_code & FAULT_LOW_VOLTAGE)
    {
    printf("Fault: Low voltage\\n");
    }

    if (sys->fault_code & FAULT_OVER_CURRENT)
    {
    printf("Fault: Over current\\n");
    }

    if (sys->fault_code & FAULT_OVER_TEMP)
    {
    printf("Fault: Over temperature\\n");
    }

    if (sys->fault_code == FAULT_NONE)
    {
    printf("System normal\\n");
    }
    }

    这里需要注意头文件包含方式:

    #include <stdio.h>
    #include "system.h"
    #include "fault_code.h"

    其中:

    • <stdio.h> 是系统标准库头文件;
    • "system.h" 和 "fault_code.h" 是自己写的头文件。

    通常情况下:

    系统库头文件使用 <>
    自己写的头文件使用 ""


    十、sensor 模块

    sensor 模块由两个文件组成:

    sensor.h
    sensor.c

    该模块负责模拟传感器数据更新。


    十一、sensor.h:传感器函数声明

    sensor.h 内容如下:

    #ifndef SENSOR_H
    #define SENSOR_H

    #include "system_type.h"

    void sensor_update(SystemStatus *sys);

    #endif

    这里声明了一个函数:

    void sensor_update(SystemStatus *sys);

    该函数用于更新系统状态中的传感器数据。

    因为函数参数中使用了 SystemStatus 类型,所以需要包含:

    #include "system_type.h"


    十二、sensor.c:传感器数据更新

    sensor.c 内容如下:

    #include "sensor.h"

    void sensor_update(SystemStatus *sys)
    {
    sys->voltage = 9.5f;
    sys->current = 2.5f;
    sys->temperature = 72.0f;
    }

    这里暂时没有读取真实硬件,而是使用固定数值模拟传感器数据。

    这三个值是故意设置成异常值:

    voltage = 9.5f 低于 10.0V
    current = 2.5f 高于 2.0A
    temperature = 72.0f 高于 60.0℃

    这样后续故障检测函数可以触发低电压、过电流和过温故障。


    十三、fault 模块

    fault 模块由两个文件组成:

    fault.h
    fault.c

    该模块负责根据传感器数据判断系统是否存在故障。


    十四、fault.h:故障检测函数声明

    fault.h 内容如下:

    #ifndef FAULT_H
    #define FAULT_H

    #include "system_type.h"

    void fault_check(SystemStatus *sys);

    #endif

    这里声明了:

    void fault_check(SystemStatus *sys);

    该函数用于检查系统状态,并更新 fault_code。


    十五、fault.c:故障检测函数实现

    fault.c 内容如下:

    #include "fault.h"
    #include "fault_code.h"

    void fault_check(SystemStatus *sys)
    {
    sys->fault_code = FAULT_NONE;

    if (sys->voltage < 10.0f)
    {
    sys->fault_code |= FAULT_LOW_VOLTAGE;
    }

    if (sys->current > 2.0f)
    {
    sys->fault_code |= FAULT_OVER_CURRENT;
    }

    if (sys->temperature > 60.0f)
    {
    sys->fault_code |= FAULT_OVER_TEMP;
    }
    }

    该函数主要完成三个判断:

    电压 < 10.0V → 低电压故障
    电流 > 2.0A → 过电流故障
    温度 > 60.0℃ → 过温故障

    其中:

    sys->fault_code |= FAULT_LOW_VOLTAGE;

    表示在当前故障码的基础上添加低电压故障位。

    这里的 |= 是按位或赋值,用来设置故障位。


    十六、main.c:主流程调用

    拆分模块后,Day03 的 main.c 变得非常简洁:

    #include "system_type.h"
    #include "system.h"
    #include "sensor.h"
    #include "fault.h"

    int main(void)
    {
    SystemStatus sys;

    system_init(&sys);
    sensor_update(&sys);
    fault_check(&sys);
    system_print(&sys);

    return 0;
    }

    可以看到,main.c 不再负责具体功能实现,只负责组织程序流程。

    整体流程为:

    1. 创建系统状态变量 sys
    2. 初始化系统状态
    3. 更新传感器数据
    4. 检查故障
    5. 打印系统状态和故障信息

    这里的 &sys 表示取 sys 的地址。

    因为函数参数都是:

    SystemStatus *sys

    所以调用时需要传入:

    &sys


    十七、Makefile 多源文件编译

    Day02 中只需要编译一个源文件:

    src/main.c

    而 Day03 中有多个源文件:

    src/main.c
    src/system.c
    src/sensor.c
    src/fault.c

    因此 Makefile 中的 SRC 需要写入多个 .c 文件。

    Day03 的 Makefile 内容如下:

    CC = gcc
    CFLAGS = -Wall -Wextra -Iinclude
    TARGET = build/day03_test
    SRC = src/main.c src/system.c src/sensor.c src/fault.c

    all:
    $(CC) $(SRC) $(CFLAGS) -o $(TARGET)

    run:
    ./$(TARGET)

    clean:
    rm -f $(TARGET)

    其中:

    • CC = gcc:指定使用 GCC 编译器;
    • CFLAGS:指定编译参数;
    • TARGET:指定生成的可执行文件;
    • SRC:指定所有参与编译的源文件;
    • all:默认编译目标;
    • run:运行目标;
    • clean:清理目标。

    执行:

    make

    等价于:

    gcc src/main.c src/system.c src/sensor.c src/fault.c -Wall -Wextra -Iinclude -o build/day03_test

    执行:

    make run

    等价于:

    ./build/day03_test


    十八、编译与运行结果

    进入 Day03 目录:

    cd /root/Embedded_14Days/day03

    执行编译:

    make

    执行运行:

    make run

    运行结果如下:

    LED state: 0
    Voltage: 9.50 V
    Current: 2.50 A
    temperature: 72.00 C
    Fault code: 0x0007
    Fault: Low voltage
    Fault: Over current
    Fault: Over temperature

    可以看到,程序成功触发了三个故障:

    低电压故障
    过电流故障
    过温故障

    最终故障码为:

    0x0007

    其含义为:

    0x0001 | 0x0002 | 0x0004 = 0x0007


    十九、遇到的问题与解决方法

    1. 头文件放错位置

    一开始新建 system.h 时,文件放到了 day03 根目录,而不是 day03/include 目录。

    错误位置:

    day03/system.h

    正确位置:

    day03/include/system.h

    解决方法是使用 mv 命令移动文件:

    mv day03/system.h day03/include/system.h

    这说明在 C 工程中,文件位置非常重要。一般情况下:

    .h 文件放 include
    .c 文件放 src
    Makefile 放当前项目根目录


    2. 函数名拼写不一致

    在写 sensor.h 时,曾经把:

    sensor_update

    写成了:

    sensor_updata

    这类拼写错误会导致声明和实现不一致。

    在 C 语言中,函数名、变量名、结构体成员名必须完全一致,一个字母写错都会导致编译或链接错误。


    3. Makefile 没保存导致 make 失败

    一开始执行:

    make

    出现:

    make: *** No targets. Stop.

    原因是 Makefile 内容还没有保存到磁盘中,终端读取到的文件不是最新内容。

    解决方法是先保存 Makefile:

    Ctrl + S

    再执行:

    make
    make run


    4. Makefile 中需要写入所有源文件

    Day03 是多文件工程,如果 Makefile 只写:

    SRC = src/main.c

    那么 main.c 调用的函数实现可能找不到,容易出现链接错误。

    所以需要写成:

    SRC = src/main.c src/system.c src/sensor.c src/fault.c

    这样 GCC 才能把多个源文件一起编译并链接成最终程序。


    5. GitHub push 网络失败

    在上传 GitHub 时,出现过:

    Failed to connect to github.com port 443

    这是 WSL 连接 GitHub 的网络问题,不是代码问题。

    因为本地已经成功执行了:

    git commit -m "Add Day03 multi-file C project"

    所以代码已经保存在本地 Git 版本库中,只是没有同步到 GitHub。

    后面通过重新连接网络并再次执行:

    git push

    最终成功上传。


    二十、今日总结

    通过 Day03 的学习,主要掌握了以下内容:

  • 理解了为什么要把一个 main.c 拆分为多个文件;
  • 理解了 .h 文件负责函数声明,.c 文件负责函数实现;
  • 学会了使用 system.h/system.c 管理系统模块;
  • 学会了使用 sensor.h/sensor.c 管理传感器模块;
  • 学会了使用 fault.h/fault.c 管理故障检测模块;
  • 学会了让 main.c 只保留主流程;
  • 学会了使用 Makefile 编译多个 .c 文件;
  • 进一步理解了结构体指针 SystemStatus *sys 和 &sys 的用法;
  • 认识到文件路径、函数名拼写和 Makefile 保存都会影响编译结果。
  • Day03 是从“写 C 程序”到“写 C 工程”的关键一步。后续学习 UART、CAN、I2C、传感器驱动、状态机和 RTOS 时,都会大量使用这种 .h + .c 的模块化组织方式。


    二十一、项目源码

    本次 Day03 学习代码已上传至 GitHub:

    https://github.com/jdai10590-afk/Embedded-C-Learning-Projects/tree/main/day03
    duhong2026

    赞(0)
    未经允许不得转载:171主机测评 » 【0基础嵌入式学习日志】Day03:多文件拆分、函数声明与 Makefile 多源文件编译
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址