代码解耦思路与示例

发布时间:2026/6/28 3:50:44
代码解耦思路与示例 前言从大一入坑单片机算下来摸硬件写代码已经整整三年。回头复盘才发现大部分时间都在原地打转真正沉淀、能复用的底层设计思路少得可怜。刚入门写驱动最典型的陋习就是CtrlC/V复制粘贴硬编码今天调试串口 1把时钟、GPIO、波特率配置完整写一遍后续项目要用到串口 3直接 CtrlC/V 复制整段初始化函数寄存器、引脚、时钟使能位逐行手动修改。 一旦后续需求变更换串口、改引脚、换主控芯片、调整波特率所有分支 if 判断里的代码都要同步改动一处漏改直接导致外设罢工维护成本指数级上涨。比如像这样void My_USART_SendString(My_USART_Typedef *USARTx,char *str) { if(USARTx My_USART1) { USARTx My_USART1; } if (USARTx My_USART3) { USARTx My_USART3; } while (*str) { My_USART_SendByte(USARTx,(unsigned char)*str); } }就像文中贴出的 USART 初始化代码通过不停的if分支区分不同串口发送、打印函数也要重复写相同的判断逻辑。代码重复度极高、耦合严重项目迭代几个版本后代码臃肿混乱可读性和可维护性完全崩盘。本文专门拆解单片机驱动层代码解耦的三种落地方案单元封装、查表法、通用接口抽象。循序渐进解决硬编码、重复造轮子、底层与业务强绑定三大痛点实现一键解耦。一、先搞懂代码解耦的目标是为了什么1.降低维护成本避免一处修改全部废掉木大悲。2.停止重复造轮子实现换一个芯片或者库函数旧代码依旧能够使用一部分而不是全部重头再来。二、方案1单元封装用struct把函数参数给封装起来易上手老代码示例void gpio_init(GPIO_TypeDe *port,uint8_t pin,GPIO_ModeTypeDef mode) { gpio_enable_clock(port);//启动对应时钟 if(pin8) { port-CRL ~(0xFU(pin * 4)); //低8位引脚 port-CRL | (me-mode(pin * 4)); } else { uint8_t p pin -8; port-CRH ~(0xFU (p * 4));//高8位引脚 port-CRH | (me-mode (p * 4)); } }原始的GPIO初始化逻辑端口、引脚、模式、速度全部散落在函数内作为硬常量写死每次新增引脚都要复制函数、改寄存器偏移代码冗余严重。改造思路用结构体封装所有变量// GPIO单元封装结构体收拢该引脚全部硬件信息 typedef struct{ GPIO_TypeDef *GPIOx; // 端口基地址 GPIOA/GPIOB uint8_t Pin; // 引脚号 0~15 GPIO_ModeTypeDef Mode; }GPIO_UnitTypeDef; // 统一初始化函数只操作结构体无硬编码 void GPIO_Init(GPIO_UnitTypeDef *gpio_cfg) { // 1. 开启对应端口时钟从结构体读取端口 if(gpio_cfg-GPIOx GPIOA) RCC-APB2ENR | BIT(2); else if(gpio_cfg-GPIOx GPIOB) RCC-APB2ENR | BIT(3); uint8_t pin_pos gpio_cfg-Pin; // 低8位引脚CRL寄存器 if(pin_pos 8) { gpio_cfg-GPIOx-CRL ~(0x0F (pin_pos * 4)); gpio_cfg-GPIOx-CRL | (gpio_cfg-Mode (pin_pos * 4)); } // 高8位引脚CRH寄存器 else { pin_pos - 8; gpio_cfg-GPIOx-CRH ~(0x0F (pin_pos * 4)); gpio_cfg-GPIOx-CRH | (gpio_cfg-Mode (pin_pos * 4)); } }那么调用的话只需要初始化结构体即可// 定义led PA1的引脚 GPIO_UnitTypeDef led_t { .GPIOx GPIOA, .Pin 1, .Mode GPIO_AF_PP }; // 初始化新增引脚只需要新建结构体变量不用改底层函数 GPIO_Init(led_t );总结一下关于结构体封装参数的优缺点优点上手简单改动成本低适合中小项目硬件配置集中新增外设只需要新增结构体实例底层函数完全不用修改。缺点外设数量多的情况无法批量管理一组关联外设。三、方案二查表法在方案一的基础上将全部外设的参数还有配置存进结构体数组里面然后通过枚举enum设置列表顺序然后通过查询列表获取硬件参数、配置彻底删除臃肿的if-else多分支判断。老代码示例void My_USART_Init(My_USART_Typedef *USARTx) { if(USARTx My_USART1) { // 1. 开 GPIOA、AFIO、USART1 时钟 My_RCC-APB2ENR | BIT(2); // IOPAEN My_RCC-APB2ENR | BIT(14); // USART1EN // 2. 配置 PA9 USART1_TX // PA9 在 CRH[7:4] // MODE9 11: 50MHz 输出 // CNF9 10: 复用推挽输出 My_GPIOA-CRH ~(0xFU 4); My_GPIOA-CRH | (0xBU 4); // 3. 配置 PA10 USART1_RX // PA10 在 CRH[11:8] // MODE10 00: 输入 // CNF10 01: 浮空输入 My_GPIOA-CRH ~(0xFU 8); My_GPIOA-CRH | (0x4U 8); // 4. 配置波特率 // PCLK2 72MHz, baud 9600 // USARTDIV 72000000 / (16 * 9600) 468.75 // BRR 468 4 0.75*16 0x1D4C My_USART1-BRR 0x1D4C; // 5. 8位数据无校验1位停止位 My_USART1-CR1 0; My_USART1-CR2 0; My_USART1-CR3 0; // 6. 使能发送、接收、USART My_USART1-CR1 | USART_CR1_TE; My_USART1-CR1 | USART_CR1_RE; My_USART1-CR1 | USART_CR1_UE; } if(USARTx My_USART3) { My_RCC-APB2ENR | BIT(3); My_RCC-APB1ENR | BIT(18); My_GPIOB-CRH ~(0xFU8); My_GPIOB-CRH | (0xBU8);//PB10复用推挽输出TX My_GPIOB-CRH ~(0XFU12); My_GPIOB-CRH | (0x4U12);//PB11浮空输入RX // 4. 配置波特率 // APB1 36MHz, baud 9600 // USARTDIV 36000000 / (16 * 9600) 234.375 // BRR 234 4 0.375*16 0x0EA6 My_USART3-BRR 0x0EA6; // 5. 8位数据无校验1位停止位 My_USART3-CR1 0; My_USART3-CR2 0; My_USART3-CR3 0; // 6. 使能发送、接收、USART My_USART3-CR1 | USART_CR1_TE; My_USART3-CR1 | USART_CR1_RE; My_USART3-CR1 | USART_CR1_UE; } }一层一个if极其臃肿项目一多一大一改起来全部都要改出bug率极高。以 USART 串口完整结构体数组封装举例一次性收拢串口时钟、TX/RX 引脚、波特率、寄存器基地址全部信息// 串口枚举对外使用的索引 typedef enum { USART_ID_1 0, USART_ID_3, USART_ID_MAX } USART_ID_e; // 单个串口完整硬件配置结构体 typedef struct { My_USART_Typedef *usart; // 串口寄存器指针 My_GPIO_Typedef *gpio; // 对应GPIO组 uint32_t rcc_apb2_en; // APB2时钟位(GPIO/AFIO/USART1) uint32_t rcc_apb1_en; // APB1时钟位(USART3) uint8_t tx_crh_shift; // TX引脚在CRH中的偏移位 uint8_t rx_crh_shift; // RX引脚在CRH中的偏移位 } USART_HW_Config_t; // 【查表数组】所有串口硬件配置表只在这里配置硬件业务函数无硬编码 static const USART_HW_Config_t g_usart_hw_table[USART_ID_MAX] { // USART1: PA9(TX) PA10(RX) [USART_ID_1] { .usart My_USART1, .gpio My_GPIOA, .rcc_apb2_en BIT(2) | BIT(0) | BIT(14), // IOPAAFIOUSART1 .rcc_apb1_en 0, .tx_crh_shift 4, // PA9: CRH[7:4] 偏移4 .rx_crh_shift 8 // PA10: CRH[11:8] 偏移8 }, // USART3: PB10(TX) PB11(RX) [USART_ID_3] { .usart My_USART3, .gpio My_GPIOB, .rcc_apb2_en BIT(3), // IOPB .rcc_apb1_en BIT(18),// USART3 APB1时钟 .tx_crh_shift 8, // PB10: CRH[11:8] 偏移8 .rx_crh_shift 12 // PB11: CRH[15:12] 偏移12 } };那么初始化// 通用串口初始化波特率可配置彻底解耦 void My_USART_Init(USART_ID_e usart_id, uint32_t baudrate) { // 合法性校验 if(usart_id USART_ID_MAX) return; // 查表取出当前串口所有硬件信息 const USART_HW_Config_t *hw g_usart_hw_table[usart_id]; My_USART_Typedef *usart hw-usart; My_GPIO_Typedef *gpio hw-gpio; // 1. 使能时钟 My_RCC-APB2ENR | hw-rcc_apb2_en; My_RCC-APB1ENR | hw-rcc_apb1_en; // 2. 配置TX引脚复用推挽 50MHz (0xB) gpio-CRH ~(0xFU hw-tx_crh_shift); gpio-CRH | (0xBU hw-tx_crh_shift); // 3. 配置RX引脚浮空输入 (0x4) gpio-CRH ~(0xFU hw-rx_crh_shift); gpio-CRH | (0x4U hw-rx_crh_shift); // 4. 配置波特率72MHz 主频 // 你原来的固定9600这里可做成通用计算也可固定 if(baudrate 9600) { if(usart_id USART_ID_1) usart-BRR 0x1D4C; else usart-BRR 0x0EA6; } // 5. 清空控制寄存器8数据位、无校验、1停止位 usart-CR1 0; usart-CR2 0; usart-CR3 0; // 6. 使能收发 串口 usart-CR1 | USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; }优点1.上层调用极其简单直接传入枚举列表里的数即可调用。2.更改方便在枚举与结构体里添加相关外设参数与配置即可删除冗余的if缺点1.复用性差串口查表只能给串口I2CSPI这类外设 - 引脚固定绑定的外设用。LED、按键不能复用这套结构体四、通用接口抽象跨库、跨芯片必用1.前两种方案只解决了同类外设的代码重复问题通用接口抽象更进一步把驱动分为「接口抽象层」和「硬件实现层」彻底隔离上层业务与底层寄存器实现跨芯片、跨库无缝移植。核心结构用函数指针结构体定义统一外设接口上层业务只调用接口函数底层针对不同芯片 / 外设单独实现接口。接口层// 统一驱动抽象接口Driver结构体 typedef struct Driver { const char *name; // 初始化硬件 int (*init)(void *hw_param); // 读数据 int (*read)(void *hw_param, uint8_t *buf, size_t len); // 写数据 int (*write)(void *hw_param, const uint8_t *buf, size_t len); // 注销/关闭外设 void (*deinit)(void *hw_param); } Driver;底层驱动层// 串口硬件资源封装端口、引脚、时钟、寄存器 typedef struct { GPIO_TypeDef *tx_port; uint8_t tx_pin; GPIO_TypeDef *rx_port; uint8_t rx_pin; USART_TypeDef *uart_reg; uint32_t apb2_clk; uint32_t apb1_clk; uint32_t baudrate; } UART_HwConfig; // 静态底层初始化操作CRH、开时钟、配置GPIO static int uart_init(void *hw_param) { UART_HwConfig *hw (UART_HwConfig *)hw_param; // 1. 开启GPIO串口时钟 RCC-APB2ENR | hw-apb2_clk; RCC-APB1ENR | hw-apb1_clk; // 2. 复用你之前的gpio_me结构体初始化TX/RX引脚 gpio_me tx_cfg {hw-tx_port, hw-tx_pin, Alternate_Push_pull}; gpio_me rx_cfg {hw-rx_port, hw-rx_pin, Input_floating}; GPIO_Init(tx_cfg); GPIO_Init(rx_cfg); // 3. 串口寄存器配置波特率、停止位、收发使能 hw-uart_reg-BRR SystemCoreClock / hw-baudrate; hw-uart_reg-CR1 | USART_CR1_TE | USART_CR1_RE; hw-uart_reg-CR1 | USART_CR1_UE; return 0; } // 底层读函数串口接收 static int uart_read(void *hw_param, uint8_t *buf, size_t len) { UART_HwConfig *hw (UART_HwConfig *)hw_param; for(size_t i0; ilen; i) { while(!(hw-uart_reg-SR USART_SR_RXNE)); buf[i] hw-uart_reg-DR; } return len; } // 底层写函数串口发送 static int uart_write(void *hw_param, const uint8_t *buf, size_t len) { UART_HwConfig *hw (UART_HwConfig *)hw_param; for(size_t i0; ilen; i) { while(!(hw-uart_reg-SR USART_SR_TXE)); hw-uart_reg-DR buf[i]; } return len; } // 注销串口 static void uart_deinit(void *hw_param) { UART_HwConfig *hw (UART_HwConfig *)hw_param; hw-uart_reg-CR1 ~USART_CR1_UE; RCC-APB2ENR ~hw-apb2_clk; } // 【关键】生成串口驱动实例填充接口函数指针 const Driver uart_driver { .name STM32F1 UART Driver, .init uart_init, .read uart_read, .write uart_write, .deinit uart_deinit };调用层// 业务函数只接收通用Driver指针和硬件解耦 void data_transfer(const Driver *drv, void *hw_cfg) { uint8_t send_buf[] hello decouple; uint8_t recv_buf[32] {0}; drv-init(hw_cfg); // 初始化不用管是串口还是SPI drv-write(hw_cfg, send_buf, sizeof(send_buf)); drv-read(hw_cfg, recv_buf, 10); drv-deinit(hw_cfg); } int main(void) { // 定义UART3硬件配置PB10 TX PB11 RX你之前写的代码 UART_HwConfig uart3_hw { .tx_port GPIOB, .tx_pin 10, .rx_port GPIOB, .rx_pin 11, .uart_reg USART3, .apb2_clk RCC_APB2ENR_IOPBEN, .apb1_clk RCC_APB1ENR_USART3EN, .baudrate 9600 }; // 传入串口驱动实例硬件参数执行业务 data_transfer(uart_driver, uart3_hw); while(1) { } }优点分层解耦可以实现跨芯片、跨平台的调用适合大型产品、多产品复用一套逻辑。缺点代码分层多小项目会增加代码量上手难度高。结尾大二后半期那时候开始感觉到很奇怪明明自己写的代码能跑但是就还是停留在复制粘贴驱动的阶段。每天花了很多时间在学习上但其实是在原地踏步。因为很多时候就是ctrlC/V然后查问题要查半天重复造轮子。后来我接触到了解耦的思路才知道怎么写出写出可复用、易维护、跨平台的模块化代码本文三种解耦方案覆盖从小到大所有项目场景大家可以拿自己手上的串口、I2C 驱动动手重构一遍重构完就能直观感受到代码维护效率的巨大提升。