⚠️ 阅读前必看尤其如果你是刚了解嵌入式的新人本文是一篇“思想笔记”不是一份“可抄作业的工程模板”。整篇文章的核心目的是让你理解嵌入式 C 语言封装背后的动机和演进逻辑——为什么要分层、为什么用函数指针、为什么要“继承”。文中的代码都是为说明思想而刻意简化的最小示例伪代码级别省略了大量工业级项目里必须考虑的细节比如外设的初始化时钟、GPIO 配置对象的实例化this 指针、构造函数错误处理、参数校验代码的跨平台与可移植性工程实践这些“壳子”是为了让你以后省事但现在故意找麻烦的。本文只负责把“为什么要找这个麻烦”讲清楚至于“怎么系统地找这个麻烦完整实现”那是下一阶段的事情。如果你是新人请不要误以为这就是 C 语言 OOP 的全部也不要直接把这些示例代码不加修改地用在真实项目里。⚠️ 关于函数指针的一点重要提醒新手必读下面介绍的函数指针封装是一个很强大的工具但它不是免费的午餐。什么时候用会很舒服你的 MCU 资源比较充裕比如 STM32、ESP32、ARM Cortex-M 系列ROM 32KBRAM 4KB你需要运行时动态切换硬件实现比如同时兼容 STM32 和 GD32你写的驱动库要给多个项目复用什么时候要慎重考虑你用的是资源极其受限的 MCU例如51 单片机尤其 ROM 8KB、RAM 256BPIC、AVR 的小容量型号任何 RAM 只有几百字节、ROM 只有几 KB 的芯片函数指针会带来两个代价间接调用开销比直接调用函数慢一点虽然通常你感觉不到但在高频中断里可能出问题代码体积增加函数指针表、函数地址存储会额外占用宝贵的 ROM/RAM给新人的一句话总结如果你刚开始学嵌入式用的又是 STM32 这种资源充足的芯片放心用函数指针去理解封装思想。如果你玩到 51 单片机或做极致成本优化时再回来记住函数指针有代价就够了。别因为这个提醒就不敢学也别以后忘了这个提醒滥用它。希望这篇笔记能帮你建立起封装思维而不是给你一个生硬的模板。之前的思想篇《为了以后更省事而现在故意找的麻烦——嵌入式C语言OOP封装思想》主要讲解了继承这个思想。一、先看之前的思想篇简化版有什么问题伪代码// 简化版的操作表typedefstruct{void(*turn_on)(void);// 没有参数void(*turn_off)(void);// 没有参数}light_interface_t;// STM32的实现voidstm32_turn_on(void){GPIOA-BSRRGPIO_BSRR_BS1;// 写死了PA1引脚}这个stm32_turn_on函数里引脚号是写死的它永远只能点亮 PA1 这一个灯。如果你想点亮 PA6 的第二个灯怎么办你得再写一个stm32_turn_on2()函数伪代码voidstm32_turn_on2(void){GPIOA-BSRRGPIO_BSRR_BS6;// 又写死了PA6引脚}如果你有 100 个灯你就得写 100 个turn_on函数这显然是不可接受的。二、怎么解决这个问题加个参数我们给函数加一个pin_num参数不就可以控制任意引脚的灯了吗伪代码// 改进版的操作表typedefstruct{void(*turn_on)(intpin_num);// 加了引脚号参数void(*turn_off)(intpin_num);}light_interface_t;// 现在一个函数就能控制所有灯了voidstm32_turn_on(intpin_num){GPIO_SetPin(pin_num,1);// 不再写死引脚号}但它还有一个小问题只能传一个参数。如果一个灯不止有引脚号这一个属性呢比如有的灯是高电平点亮有的是低电平点亮PWM 灯还有亮度属性I2C 灯还有设备地址属性这时候一个int pin_num参数就不够用了。三、解决方案把整个灯对象传进去既然一个参数不够用那我们就把整个灯对象的指针传进去这样函数就能访问这个灯的所有属性了。伪代码typedefstruct{// 函数接受一个 light_base_t 类型的指针作为参数void(*turn_on)(light_base_t*self);void(*turn_off)(light_base_t*self);}light_base_ops_t;这个self指针是什么它就是当前这个灯对象自己的指针。就像你说我的时候指的是你自己一样这个self指针指的是当前正在被操作的那个灯对象。四、完整的标准写法三步走1. 基类定义通用接口伪代码// ------------------- 第一步定义方法表 -------------------// 规定所有灯必须能点亮和熄灭并且接受一个指向自己的指针作为参数typedefstruct{void(*turn_on)(light_base_t*self);// 点亮我这个灯void(*turn_off)(light_base_t*self);// 熄灭我这个灯}light_base_ops_t;// ------------------- 第二步定义基类对象 -------------------// 所有灯的通用部分只包含一个指向方法表的指针typedefstruct{light_base_ops_t*ops;// 指向我这个灯的操作方法表}light_base_t;伪代码2. 派生类添加私有数据// ------------------- 第三步定义具体的灯类型 -------------------// GPIO 灯除了通用部分还有自己特有的属性typedefstruct{light_base_tbase;// 先把通用部分放进来所谓的继承int32_tpin_num;// GPIO 灯特有的引脚号bool active_high;// GPIO 灯特有的高电平点亮true还是低电平点亮false}light_gpio_t;伪代码3. 构造函数初始化对象// ------------------- 第四步写具体的实现函数 -------------------// GPIO 灯的点亮函数staticvoidlight_gpio_turn_on(light_base_t*self){// 这里的 self 是基类指针我们把它转回原来的 light_gpio_t 指针// 因为我们知道它本来就是一个 light_gpio_tlight_gpio_t*gpio_self(light_gpio_t*)self;// 现在我们可以访问这个灯的所有私有属性了if(gpio_self-active_high){GPIO_SetPin(gpio_self-pin_num,1);// 高电平点亮}else{GPIO_SetPin(gpio_self-pin_num,0);// 低电平点亮}}// GPIO 灯的熄灭函数staticvoidlight_gpio_turn_off(light_base_t*self){light_gpio_t*gpio_self(light_gpio_t*)self;GPIO_SetPin(gpio_self-pin_num,!gpio_self-active_high);}// ------------------- 第五步创建全局的方法表 -------------------// 所有 GPIO 灯共享这一个方法表staticconstlight_base_ops_tgpio_light_ops{.turn_onlight_gpio_turn_on,.turn_offlight_gpio_turn_off,};// ------------------- 第六步构造函数 -------------------// 初始化一个 GPIO 灯对象voidlight_gpio_init(light_gpio_t*self,// 要初始化的灯对象constchar*pin_name,// 引脚名bool active_high// 点亮电平true 高电平亮false 低电平亮){// 初始化这个灯自己特有的属性self-pin_numGPIO_GetPinByName(pin_name);self-active_highactive_high;// 让这个灯的方法表指针指向全局的 GPIO 灯方法表self-base.opsgpio_light_ops;}五、见证奇迹现在我们可以创建任意多个灯了伪代码intmain(void){// 创建第一个灯PA1高电平点亮light_gpio_tred_light;light_gpio_init(red_light,PA1,true);// 创建第二个灯PA6低电平点亮light_gpio_tgreen_light;light_gpio_init(green_light,PA6,false);// 创建第三个灯PB0高电平点亮light_gpio_tblue_light;light_gpio_init(blue_light,PB0,true);while(1){// 点亮红色灯red_light.base.ops-turn_on((light_base_t*)red_light);delay_ms(500);// 熄灭红色灯red_light.base.ops-turn_off((light_base_t*)red_light);// 点亮绿色灯green_light.base.ops-turn_on((light_base_t*)green_light);delay_ms(500);// 熄灭绿色灯green_light.base.ops-turn_off((light_base_t*)green_light);// 点亮蓝色灯blue_light.base.ops-turn_on((light_base_t*)blue_light);delay_ms(500);// 熄灭蓝色灯blue_light.base.ops-turn_off((light_base_t*)blue_light);}}看到了吗我们只写了一个light_gpio_turn_on函数就可以控制任意多个 GPIO 灯每个灯都有自己的引脚号和点亮电平互不干扰。六、最神奇的地方通用函数可以操作任何类型的灯现在我们写一个通用的灯闪烁函数伪代码// 这个函数可以闪烁任何类型的灯// 不管是 GPIO 的、PWM 的还是 I2C 的它都能工作voidlight_blink(light_base_t*light,intdelay_ms){light-ops-turn_on(light);delay_ms(delay_ms);light-ops-turn_off(light);delay_ms(delay_ms);}现在我们用这个通用函数来闪烁刚才的三个灯伪代码intmain(void){light_gpio_tred_light,green_light,blue_light;light_gpio_init(red_light,PA5,true);light_gpio_init(green_light,PA6,false);light_gpio_init(blue_light,PB0,true);while(1){light_blink((light_base_t*)red_light,500);// 闪烁红色灯light_blink((light_base_t*)green_light,500);// 闪烁绿色灯light_blink((light_base_t*)blue_light,500);// 闪烁蓝色灯}}七、现在我们来加一个 PWM 调光灯看看有多简单伪代码// ------------------- PWM 灯的定义 -------------------typedefstruct{light_base_tbase;// 继承通用部分int32_tpwm_channel;// PWM 灯特有的通道号uint8_tbrightness;// PWM 灯特有的亮度0~255}light_pwm_t;// ------------------- PWM 灯的实现 -------------------staticvoidlight_pwm_turn_on(light_base_t*self){light_pwm_t*pwm_self(light_pwm_t*)self;PWM_SetDutyCycle(pwm_self-pwm_channel,pwm_self-brightness);}staticvoidlight_pwm_turn_off(light_base_t*self){light_pwm_t*pwm_self(light_pwm_t*)self;PWM_SetDutyCycle(pwm_self-pwm_channel,0);}// ------------------- PWM 灯的方法表 -------------------staticconstlight_base_ops_tpwm_light_ops{.turn_onlight_pwm_turn_on,.turn_offlight_pwm_turn_off,};// ------------------- PWM 灯的构造函数 -------------------voidlight_pwm_init(light_pwm_t*self,constchar*pwm_name,uint8_tbrightness){self-pwm_channelPWM_GetChannelByName(pwm_name);self-brightnessbrightness;self-base.opspwm_light_ops;}现在我们可以用同一个light_blink函数来闪烁 PWM 灯了伪代码intmain(void){// 创建一个 GPIO 灯light_gpio_tred_light;light_gpio_init(red_light,PA5,true);// 创建一个 PWM 灯亮度 50%light_pwm_tblue_light;light_pwm_init(blue_light,PA6,128);while(1){// 用同一个函数闪烁不同类型的灯light_blink((light_base_t*)red_light,500);light_blink((light_base_t*)blue_light,500);}}这个设计的灵魂就是那个light_base_t *self参数。它让函数可以访问当前对象的所有私有数据不需要知道对象的具体类型同一个函数可以操作任意多个同类型的对象