【0基础嵌入式学习日志】Day03:多文件拆分、函数声明与 Makefile 多源文件编译
一、前言
今天继续进行嵌入式 C 语言基础学习。前两天主要完成了 C 工程结构、头文件、结构体、故障码、函数封装、结构体指针以及 Makefile 的基础使用。
Day02 中虽然已经把功能拆成了多个函数,但是这些函数仍然全部写在 main.c 里面。随着功能越来越多,main.c 会越来越臃肿,不利于后期维护。
因此,Day03 的重点是学习 C 工程中的多文件拆分,也就是把不同功能拆成不同的 .h 和 .c 文件:
.h 文件:负责函数声明
.c 文件:负责函数实现
main.c:负责主流程调用
Makefile:负责编译多个源文件
本次练习的核心目标是从“写一个 C 文件”过渡到“写一个简单 C 工程”。
二、Day03 学习目标
本次 Day03 主要学习以下内容:
三、工程结构
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();
这种写法在代码较少时可以使用,但随着项目变大,会出现几个问题:
真实嵌入式项目中,通常会按照功能划分模块,例如:
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 的学习,主要掌握了以下内容:
Day03 是从“写 C 程序”到“写 C 工程”的关键一步。后续学习 UART、CAN、I2C、传感器驱动、状态机和 RTOS 时,都会大量使用这种 .h + .c 的模块化组织方式。
二十一、项目源码
本次 Day03 学习代码已上传至 GitHub:
https://github.com/jdai10590-afk/Embedded-C-Learning-Projects/tree/main/day03







