嵌入式C语言模块化编程实战:从底层逻辑到架构演进
2026/9/20 8:19:02 网站建设 项目流程

1. 嵌入式C语言模块化编程的底层逻辑

1.1 为什么嵌入式开发绕不开模块化

做嵌入式这行十来年,接手过的烂摊子项目不算少。最常见的场景就是:一个main.c文件写了三千多行,里面塞满了GPIO初始化、串口收发、定时器中断、状态机、协议解析、EEPROM读写,甚至还有几个delay_ms的软件延时函数。这种代码能跑,但没人敢改——改一个LED闪烁的逻辑,可能把串口通信搞挂掉。

模块化编程要解决的核心问题就一个:让代码的修改影响范围可控。在嵌入式领域,这个问题比PC端更尖锐,因为资源受限、调试手段有限、硬件耦合度高。你在PC上写崩了顶多程序崩溃,在嵌入式上写崩了可能是设备死机、看门狗复位、甚至烧毁外设。

我见过太多初学者把“模块化”理解成“多建几个.c文件”,这是典型的知其然不知其所以然。模块化的本质是关注点分离接口契约。每个模块对外只暴露必要的接口,内部实现细节完全隐藏。就像你家的路由器,你只需要知道插上网线能上网,不需要知道里面怎么处理NAT转发。

嵌入式C语言的模块化还有一层特殊含义:硬件抽象。同一套应用逻辑,换一颗MCU就要重写一遍,这是嵌入式开发的经典痛点。模块化做得好,应用层代码可以做到与硬件无关,换平台时只需要替换底层驱动模块。

1.2 模块化在嵌入式场景下的特殊约束

PC端写模块化,你可以随便用动态库、依赖注入、面向对象。嵌入式不行,你得面对这些现实:

  • RAM和Flash极其有限:STM32F103C8T6只有20KB RAM、64KB Flash,你不可能像PC那样搞一堆虚函数表和动态内存分配。
  • 没有操作系统或只有RTOS:裸机环境下,模块之间的通信方式受限,全局变量满天飞是常态。
  • 中断上下文:模块之间的调用可能发生在中断里,可重入性、临界区保护必须考虑。
  • 编译链接模型:C语言的编译单元是.c文件,链接时符号可见性由static和extern控制,这是模块化的物理基础。

所以嵌入式C的模块化,不是照搬设计模式,而是在资源约束下找到工程上的最优解。我个人的经验是:能用static就用static,能编译期确定就不运行期判断,能直接调用就不搞回调

1.3 一个典型的模块化架构长什么样

以我做过的一个环境监控设备为例,硬件是STM32F407 + SHT30温湿度传感器 + ESP8266 WiFi模块 + OLED显示屏。如果不用模块化,代码大概是这样:

// main.c 伪代码 int main(void) { // 初始化时钟 RCC->AHB1ENR |= ...; // 初始化GPIO GPIOA->MODER |= ...; // 初始化I2C I2C1->CR1 |= ...; // 初始化UART USART2->BRR = ...; while(1) { // 读传感器 // 处理数据 // 显示 // 上传 } }

模块化之后,结构变成:

project/ ├── app/ │ ├── app_main.c // 应用主逻辑 │ └── app_main.h ├── bsp/ │ ├── bsp_gpio.c // GPIO底层封装 │ ├── bsp_i2c.c // I2C底层封装 │ └── bsp_uart.c // UART底层封装 ├── drivers/ │ ├── drv_sht30.c // 温湿度传感器驱动 │ ├── drv_oled.c // OLED驱动 │ └── drv_esp8266.c // WiFi模块驱动 ├── services/ │ ├── svc_sensor.c // 传感器数据服务 │ ├── svc_display.c // 显示服务 │ └── svc_network.c // 网络服务 └── main.c // 只负责初始化和调度

这个分层结构的关键在于:上层只依赖下层的头文件,不依赖实现。app_main.c里看不到任何寄存器操作,它只调用svc_sensor_read()这样的接口。换一颗MCU,只需要重写bsp层,drivers和services层几乎不用动。

2. 模块化编程的核心技术点拆解

2.1 头文件的设计原则:接口与实现的边界

头文件是模块化的门面,写得好不好直接决定模块能不能被复用。我见过太多头文件里塞满了不该出现的东西:全局变量定义、函数实现、甚至#include一堆无关的头文件。

一个合格的头文件应该只包含:

  • 函数声明:模块对外提供的接口
  • 类型定义:结构体、枚举、typedef
  • 宏定义:配置参数、错误码
  • 必要的#include:只包含头文件自身需要的类型

反面教材:

// bad_sensor.h #ifndef BAD_SENSOR_H #define BAD_SENSOR_H #include "stm32f4xx.h" #include "stm32f4xx_hal.h" #include "main.h" #include "oled.h" #include "esp8266.h" int sensor_value; // 全局变量定义,多个.c包含会重复定义 void sensor_init(void) { // 函数实现写在头文件里,每个包含的.c都会生成一份 } #endif

正确做法:

// sensor.h #ifndef SENSOR_H #define SENSOR_H #include <stdint.h> #include <stdbool.h> typedef enum { SENSOR_OK = 0, SENSOR_ERR_I2C, SENSOR_ERR_CRC, SENSOR_ERR_TIMEOUT } sensor_err_t; typedef struct { float temperature; float humidity; } sensor_data_t; sensor_err_t sensor_init(void); sensor_err_t sensor_read(sensor_data_t *data); #endif

头文件里不出现任何硬件相关的头文件,这样sensor模块就可以在PC上编译测试,不需要STM32的HAL库。这是模块化带来的额外好处:可测试性

注意:头文件里绝对不要定义变量。如果多个.c文件包含同一个头文件,链接时会报重复定义错误。如果确实需要模块间共享变量,用extern声明,在.c文件里定义。

2.2 static关键字的妙用:模块私有化

C语言没有类,没有private关键字,但static可以做到类似的效果。在文件作用域下,static修饰的函数和变量只在本编译单元可见,链接器看不到它们。

这意味着你可以在.c文件里随便定义辅助函数,不用担心命名冲突。比如drv_sht30.c里:

// drv_sht30.c #include "drv_sht30.h" static uint8_t crc8(const uint8_t *data, int len) { // CRC校验,只在本文件使用 } static sensor_err_t write_cmd(uint16_t cmd) { // 写命令,只在本文件使用 } sensor_err_t sht30_read(sensor_data_t *data) { // 对外接口,调用上面的static函数 }

crc8和write_cmd在别的文件里完全不可见,即使另一个模块也定义了同名函数,链接时也不会冲突。这是嵌入式C模块化最实用的技巧之一。

我个人的习惯是:所有不需要对外暴露的函数一律加static。这不仅是命名空间隔离,还能帮助编译器优化——static函数可以被内联,减少函数调用开销。

2.3 模块间通信的几种方式与选型

模块化之后,模块之间必然需要通信。嵌入式C里常见的方式有这几种:

通信方式适用场景优点缺点
直接函数调用同步、实时性要求高简单、开销小耦合度高
全局变量+extern简单状态共享实现简单难以追踪修改来源
回调函数注册事件通知解耦、灵活函数指针开销、调试困难
消息队列RTOS环境异步、解耦需要RTOS支持、内存开销
发布订阅多模块监听完全解耦实现复杂、RAM占用大

裸机环境下,我推荐直接函数调用为主,回调函数为辅。全局变量能少用就少用,因为全局变量是模块化最大的敌人——你永远不知道谁在什么时候改了它。

回调函数的典型用法:

// svc_sensor.h typedef void (*sensor_data_cb_t)(const sensor_data_t *data); void svc_sensor_register_cb(sensor_data_cb_t cb); void svc_sensor_poll(void); // 在main循环里调用 // svc_sensor.c static sensor_data_cb_t s_cb = NULL; void svc_sensor_register_cb(sensor_data_cb_t cb) { s_cb = cb; } void svc_sensor_poll(void) { sensor_data_t data; if (sht30_read(&data) == SENSOR_OK) { if (s_cb) s_cb(&data); } }

这样显示模块和网络模块都可以注册回调,传感器模块不需要知道谁在用它。

2.4 条件编译与模块裁剪

嵌入式项目经常需要根据硬件配置裁剪功能。比如同一套代码,低配版没有OLED,高配版有。这时候条件编译就派上用场了。

// config.h #define CONFIG_USE_OLED 1 #define CONFIG_USE_ESP8266 1 #define CONFIG_USE_SHT30 1 // app_main.c #if CONFIG_USE_OLED #include "svc_display.h" #endif void app_init(void) { #if CONFIG_USE_OLED svc_display_init(); #endif }

条件编译的好处是:不需要的功能完全不参与编译,不占Flash空间。但要注意,条件编译不能滥用,否则代码会变成一团乱麻。我的原则是:只在硬件配置层面用条件编译,业务逻辑层面不用。

3. 从零搭建一个模块化嵌入式项目

3.1 目录结构与构建系统

先建目录。我习惯按功能分层,而不是按文件类型分层。有些人喜欢把所有.c放一个目录,所有.h放另一个目录,这在嵌入式项目里是灾难——你根本分不清哪个文件属于哪个模块。

推荐的结构:

firmware/ ├── app/ # 应用层,业务逻辑 ├── services/ # 服务层,功能服务 ├── drivers/ # 驱动层,外设驱动 ├── bsp/ # 板级支持,寄存器封装 ├── common/ # 公共工具,环形缓冲、CRC、日志 ├── config/ # 配置文件 ├── main.c └── Makefile

构建系统用Makefile就够了,嵌入式项目不需要CMake那么重。关键是要支持自动扫描源文件,不然每加一个.c都要改Makefile,太麻烦。

# Makefile 核心部分 SRCS := $(shell find app services drivers bsp common -name '*.c') SRCS += main.c OBJS := $(SRCS:.c=.o) CFLAGS := -mcpu=cortex-m4 -mthumb -Os -Wall -Wextra CFLAGS += -Iapp -Iservices -Idrivers -Ibsp -Icommon -Iconfig all: $(OBJS) $(CC) $(OBJS) -T linker.ld -o firmware.elf

-Wall -Wextra一定要加,编译器警告能帮你发现很多模块化问题,比如隐式声明、未使用变量、类型不匹配。

3.2 底层驱动模块的封装实例

以SHT30温湿度传感器为例,展示一个完整的驱动模块怎么写。

drv_sht30.h

#ifndef DRV_SHT30_H #define DRV_SHT30_H #include <stdint.h> #include <stdbool.h> typedef enum { SHT30_OK = 0, SHT30_ERR_I2C, SHT30_ERR_CRC, SHT30_ERR_PARAM } sht30_err_t; typedef struct { float temperature; float humidity; } sht30_data_t; sht30_err_t sht30_init(void); sht30_err_t sht30_read(sht30_data_t *data); #endif

drv_sht30.c

#include "drv_sht30.h" #include "bsp_i2c.h" #define SHT30_ADDR 0x44 #define SHT30_CMD_MEASURE 0x2C06 #define SHT30_CMD_SOFT_RESET 0x30A2 static uint8_t sht30_crc8(const uint8_t *data, int len) { uint8_t crc = 0xFF; for (int i = 0; i < len; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 0x80) crc = (crc << 1) ^ 0x31; else crc <<= 1; } } return crc; } static sht30_err_t sht30_write_cmd(uint16_t cmd) { uint8_t buf[2] = { cmd >> 8, cmd & 0xFF }; if (bsp_i2c_write(SHT30_ADDR, buf, 2) != 0) return SHT30_ERR_I2C; return SHT30_OK; } sht30_err_t sht30_init(void) { if (sht30_write_cmd(SHT30_CMD_SOFT_RESET) != SHT30_OK) return SHT30_ERR_I2C; bsp_delay_ms(10); return SHT30_OK; } sht30_err_t sht30_read(sht30_data_t *data) { if (!data) return SHT30_ERR_PARAM; if (sht30_write_cmd(SHT30_CMD_MEASURE) != SHT30_OK) return SHT30_ERR_I2C; bsp_delay_ms(20); uint8_t buf[6]; if (bsp_i2c_read(SHT30_ADDR, buf, 6) != 0) return SHT30_ERR_I2C; if (sht30_crc8(buf, 2) != buf[2] || sht30_crc8(buf + 3, 2) != buf[5]) return SHT30_ERR_CRC; uint16_t raw_temp = (buf[0] << 8) | buf[1]; uint16_t raw_humi = (buf[3] << 8) | buf[4]; >#ifndef SVC_SENSOR_H #define SVC_SENSOR_H #include "drv_sht30.h" typedef struct { float temperature; float humidity; uint32_t timestamp; bool valid; } svc_sensor_data_t; void svc_sensor_init(void); void svc_sensor_poll(void); const svc_sensor_data_t* svc_sensor_get(void); #endif

svc_sensor.c

#include "svc_sensor.h" #include "bsp_tick.h" static svc_sensor_data_t s_data; static uint32_t s_last_poll = 0; #define SENSOR_POLL_INTERVAL_MS 2000 void svc_sensor_init(void) { sht30_init(); s_data.valid = false; } void svc_sensor_poll(void) { uint32_t now = bsp_tick_get(); if (now - s_last_poll < SENSOR_POLL_INTERVAL_MS) return; s_last_poll = now; sht30_data_t raw; if (sht30_read(&raw) == SHT30_OK) { s_data.temperature = raw.temperature; s_data.humidity = raw.humidity; s_data.timestamp = now; s_data.valid = true; } else { s_data.valid = false; } } const svc_sensor_data_t* svc_sensor_get(void) { return &s_data; }

服务层做了几件事:限流(2秒读一次,避免频繁占用I2C总线)、缓存(数据存在s_data里,应用层随时可取)、状态标记(valid字段表示数据是否有效)。

应用层只需要在main循环里调用svc_sensor_poll(),然后随时用svc_sensor_get()拿数据。传感器什么时候读、读失败怎么办,应用层完全不用关心。

3.4 应用层的调度逻辑

应用层是最终的业务逻辑,它把各个服务模块串联起来。

// app_main.c #include "svc_sensor.h" #include "svc_display.h" #include "svc_network.h" void app_init(void) { svc_sensor_init(); svc_display_init(); svc_network_init(); } void app_loop(void) { svc_sensor_poll(); svc_display_poll(); svc_network_poll(); const svc_sensor_data_t *data = svc_sensor_get(); if (data->valid) { svc_display_show(data); svc_network_upload(data); } }

main.c变得极其简单:

#include "app_main.h" #include "bsp_clock.h" #include "bsp_gpio.h" int main(void) { bsp_clock_init(); bsp_gpio_init(); app_init(); while (1) { app_loop(); } }

这就是模块化的威力:main.c只有十几行,但整个系统的逻辑清晰可见。想知道系统干什么,看app_loop就够了。想知道传感器怎么读,去看svc_sensor.c。想知道I2C怎么操作,去看bsp_i2c.c。每一层各司其职,修改一层不影响其他层。

4. 模块化实践中的常见坑与排查技巧

4.1 头文件循环包含问题

这是模块化最常见的坑。a.h包含了b.h,b.h又包含了a.h,编译器直接报错。

// a.h #include "b.h" typedef struct { b_t b; } a_t; // b.h #include "a.h" typedef struct { a_t a; } b_t; // 循环依赖

解决办法有两种:

方案一:前向声明。如果只是指针或引用,不需要完整类型定义。

// b.h struct a_t; // 前向声明 typedef struct { struct a_t *a; } b_t;

方案二:提取公共类型。把共享的类型定义放到第三个头文件里。

// common_types.h typedef struct { int x; int y; } point_t; // a.h #include "common_types.h" typedef struct { point_t p; } a_t; // b.h #include "common_types.h" typedef struct { point_t p; } b_t;

我个人的经验是:头文件里尽量少#include其他头文件。能用前向声明就用前向声明,能提取公共类型就提取。头文件之间的依赖越少,模块化越干净。

4.2 全局变量引发的模块耦合

全局变量是模块化的隐形杀手。你定义了一个g_system_state,十个模块都在读写它,出了问题根本查不到是谁改的。

我踩过的一个坑:一个项目里有个g_uart_rx_flag,串口中断里置1,主循环里检查并清零。后来加了一个新模块,也在主循环里检查这个标志,结果两个模块互相抢,串口数据时不时丢失。查了两天才定位到问题。

解决办法:用接口替代全局变量。把状态封装在模块内部,对外提供get/set接口。

// 不好的做法 extern volatile bool g_uart_rx_flag; // 好的做法 // svc_uart.h bool svc_uart_has_data(void); uint8_t svc_uart_read_byte(void); // svc_uart.c static volatile bool s_rx_flag = false; static uint8_t s_rx_buf[256]; static volatile uint16_t s_rx_head = 0; static volatile uint16_t s_rx_tail = 0; bool svc_uart_has_data(void) { return s_rx_head != s_rx_tail; } uint8_t svc_uart_read_byte(void) { uint8_t byte = s_rx_buf[s_rx_tail]; s_rx_tail = (s_rx_tail + 1) % sizeof(s_rx_buf); return byte; }

这样串口模块内部用环形缓冲区管理数据,外部只能通过has_data和read_byte访问,不会出现多个模块抢标志的问题。

4.3 中断与模块化的冲突处理

中断服务函数是模块化里的特殊存在。ISR不能有返回值,不能传参数,而且必须尽可能短。如果ISR里直接调用模块接口,可能会破坏模块的封装性。

我的做法是:ISR只做最少的硬件操作,然后通过标志或队列通知模块

// bsp_uart.c static volatile uint8_t s_rx_byte; static volatile bool s_rx_done = false; void USART2_IRQHandler(void) { if (USART2->SR & USART_SR_RXNE) { s_rx_byte = USART2->DR; s_rx_done = true; } } bool bsp_uart_get_byte(uint8_t *byte) { if (!s_rx_done) return false; *byte = s_rx_byte; s_rx_done = false; return true; }

ISR只负责把数据从寄存器搬到变量里,模块层通过bsp_uart_get_byte轮询获取。这样ISR极短,不会阻塞其他中断,模块的封装性也保住了。

注意:ISR和模块之间共享的变量必须加volatile,否则编译器优化后可能读不到最新值。这是嵌入式C的经典坑,我见过不止一个项目栽在这上面。

4.4 模块初始化顺序的依赖管理

模块之间有依赖关系,初始化顺序不能乱。比如I2C没初始化,SHT30驱动初始化就会失败。

常见做法是在app_init里按顺序调用:

void app_init(void) { bsp_clock_init(); // 时钟最先 bsp_gpio_init(); // GPIO其次 bsp_i2c_init(); // I2C依赖GPIO bsp_uart_init(); // UART依赖GPIO svc_sensor_init(); // 传感器依赖I2C svc_display_init(); // 显示依赖I2C svc_network_init(); // 网络依赖UART }

但这种手动排序容易出错,尤其是模块多了之后。更好的做法是让每个模块自己处理依赖。比如svc_sensor_init里先检查I2C是否就绪,没就绪就返回错误。

void svc_sensor_init(void) { if (!bsp_i2c_is_ready()) { bsp_log_error("I2C not ready, sensor init failed"); return; } sht30_init(); }

这样即使初始化顺序有误,也能通过日志快速定位问题,而不是莫名其妙地死机。

4.5 常见问题速查表

问题现象可能原因排查方法解决方案
链接报重复定义头文件里定义了变量检查头文件是否有变量定义改为extern声明,.c里定义
模块函数调用后无反应初始化顺序错误在初始化函数里加日志调整顺序或加依赖检查
中断里数据丢失共享变量未加volatile检查ISR共享变量加volatile修饰
修改一个模块影响其他模块全局变量耦合搜索全局变量引用封装为接口函数
编译报未定义类型头文件循环包含检查include关系前向声明或提取公共类型
Flash占用过大条件编译未生效检查宏定义确认config.h被正确包含
模块无法在PC上测试头文件依赖硬件检查头文件include隔离硬件相关头文件

5. 模块化带来的可测试性与可维护性提升

5.1 在PC上测试嵌入式模块

模块化做得好,最大的好处之一是可以在PC上测试业务逻辑。因为驱动层和服务层不依赖具体硬件,你可以写一个PC端的模拟层,把bsp_i2c替换成文件读写或内存模拟。

// test/pc_bsp_i2c.c int bsp_i2c_write(uint8_t addr, const uint8_t *data, int len) { // PC端模拟,直接返回成功 return 0; } int bsp_i2c_read(uint8_t addr, uint8_t *data, int len) { // 返回模拟的温湿度数据 data[0] = 0x61; data[1] = 0x00; data[2] = 0x00; data[3] = 0x80; data[4] = 0x00; data[5] = 0x00; return 0; }

然后写一个测试程序:

// test/test_sensor.c #include <stdio.h> #include "svc_sensor.h" int main(void) { svc_sensor_init(); svc_sensor_poll(); const svc_sensor_data_t *data = svc_sensor_get(); printf("Temp: %.2f, Humi: %.2f\n",>// 桩函数示例 sensor_err_t sht30_read(sht30_data_t *data) { >// 旧接口保留 const svc_sensor_data_t* svc_sensor_get(void); // 新接口新增 sensor_err_t svc_sensor_get_ex(svc_sensor_data_t *data);

旧项目继续用旧接口,新项目用新接口,互不影响。等所有项目都迁移完了,再考虑删除旧接口。

6. 从模块化到架构演进的思考

6.1 什么时候该拆模块

模块化不是越细越好。我见过一个项目,一个LED驱动拆了三个文件,结果代码量没减少,调用关系反而更复杂了。

拆模块的判断标准:这个功能是否会被复用?是否会被独立修改?是否有明确的边界?

  • LED闪烁逻辑,如果只是状态指示,放应用层就行,不需要单独模块
  • 但如果LED要显示多种状态(运行、故障、配置模式),而且不同产品线的LED行为不同,那就值得拆成独立模块
  • 传感器读取,几乎一定会被复用,而且不同传感器驱动不同,必须拆
  • 协议解析,如果协议会变,拆成独立模块,改协议不影响其他部分

我的经验是:先写在一起,等感觉到痛了再拆。过早模块化会导致过度设计,过晚模块化会导致代码腐烂。一般来说,一个.c文件超过500行,或者一个功能被三个以上地方调用,就该考虑拆了。

6.2 模块化与RTOS的结合

裸机环境下,模块化主要解决代码组织问题。上了RTOS之后,模块化还要考虑任务划分。

我的做法是:一个模块不一定对应一个任务,但一个任务应该只操作一个模块。比如传感器模块可以有自己的任务,定期读取数据并放入队列;显示模块有自己的任务,从队列取数据并刷新屏幕。模块之间通过RTOS的队列、信号量通信,而不是直接函数调用。

// 传感器任务 void sensor_task(void *param) { svc_sensor_init(); while (1) { svc_sensor_poll(); const svc_sensor_data_t *data = svc_sensor_get(); if (data->valid) { xQueueSend(g_sensor_queue, data, portMAX_DELAY); } vTaskDelay(pdMS_TO_TICKS(100)); } } // 显示任务 void display_task(void *param) { svc_display_init(); svc_sensor_data_t data; while (1) { if (xQueueReceive(g_sensor_queue, &data, portMAX_DELAY)) { svc_display_show(&data); } } }

这样模块之间的耦合进一步降低,传感器任务挂了不影响显示任务,系统更健壮。

6.3 模块化代码的评审要点

代码评审时,我重点关注这几个模块化相关的点:

  • 头文件是否干净:有没有不该出现的include、变量定义、函数实现
  • static是否用足:所有内部函数是否都加了static
  • 接口是否最小:对外暴露的函数是否都是必要的,有没有暴露内部实现
  • 依赖方向是否正确:上层是否依赖下层,有没有反向依赖
  • 初始化顺序是否明确:模块初始化是否有依赖检查
  • 错误处理是否完整:每个接口是否有明确的错误码,调用者是否处理了错误

这些点看起来琐碎,但每一条都是踩坑踩出来的。尤其是依赖方向,一旦出现反向依赖(驱动层调用应用层的函数),整个架构就乱了。

6.4 一个真实项目的模块化改造记录

最后分享一个我做过的改造案例。一个工业控制板,原来的代码是一个8000行的main.c,功能包括:4路ADC采集、2路PWM输出、Modbus RTU通信、LCD显示、按键处理、EEPROM参数存储。

改造前的问题:改Modbus协议会影响ADC采集,因为中断优先级和全局变量纠缠在一起;加一路PWM输出要改十几个地方;代码无法单元测试。

改造过程分三步:

第一步:识别模块边界。把功能分成:bsp_adc、bsp_pwm、bsp_uart、drv_modbus、drv_lcd、drv_key、drv_eeprom、svc_analog、svc_control、svc_comm、app_main。

第二步:定义接口。每个模块先写头文件,确定对外接口。这一步花了三天,但值得——接口定好之后,后面就是填实现。

第三步:逐个迁移。从最底层的bsp开始,每迁移一个模块就编译测试一次,确保功能不变。全部迁移完用了两周。

改造后的效果:main.c从8000行降到80行;加一路PWM输出只需要改bsp_pwm.c和app_main.c各几行;Modbus协议解析可以在PC上单独测试;新同事接手一周就能看懂整体架构。

这个项目让我深刻体会到:模块化不是目的,而是手段。目的是让代码可维护、可复用、可测试。如果模块化之后代码更难懂了,那一定是模块化做错了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询