十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 C语言面向对象编程实践:构建可扩展的按键驱动框架

STM32 C语言面向对象编程实践:构建可扩展的按键驱动框架 1. 项目概述与核心价值在嵌入式开发尤其是STM32这类资源受限的单片机项目中一提到“面向对象”很多工程师的第一反应可能是摇头“单片机跑个裸机要什么面向对象C语言又不是C这不是自找麻烦吗” 我刚开始接触STM32时也是这么想的直到在一个复杂的工控项目里面对十几个功能各异、长短按、连按、组合键需求层出不穷的物理按键和编码器时那一坨超过500行的、充满了if-else和全局标志位的按键扫描函数彻底让我陷入了维护的泥潭。今天我想和你分享的就是如何用最纯粹的C语言在STM32上实践面向对象OOP的思想来编写清晰、可扩展、易维护的按键驱动程序。这不仅仅是换一种写法而是一种从根本上提升代码质量、应对未来需求变化的编程思维。这个项目的核心价值在于它将软件工程中优秀的模块化设计思想引入到我们熟悉的嵌入式裸机开发中。我们不会使用任何C编译器也不依赖复杂的第三方框架仅仅利用C语言的结构体struct和函数指针就能构建出具有“封装”、“继承”、“多态”特性的按键对象。最终你将得到一个按键驱动模块增加一个新按键就像定义一个结构体变量一样简单修改按键行为如滤波算法只需改动一个地方所有按键自动生效。无论是独立按键、矩阵键盘还是编码器都能用同一套优雅的模型来管理。接下来我将彻底拆解这个过程的每一步从思想到实现从原理到避坑。2. 面向对象思想在C语言中的映射在开始写代码之前我们必须统一思想在C语言中实现面向对象是一种“模拟”或“映射”而非语言原生支持。我们需要明确几个核心概念在C中的对应物。2.1 封装将数据与操作绑定封装是隐藏对象的属性和实现细节仅对外公开接口。在C语言中我们使用结构体struct来封装数据使用与结构体相关的函数来公开操作接口。关键在于这些函数的第一参数通常是指向该结构体的指针即this指针。例如一个按键对象的数据可能包括引脚状态、滤波计数器、按键事件标志等。操作则包括初始化、状态扫描、事件获取等。我们将它们打包在一起。2.2 继承与多态实现接口统一与行为扩展继承允许我们基于现有类创建新类多态允许使用父类指针来调用子类的函数。在C语言中这可以通过结构体内嵌和函数指针来实现。继承子结构体的第一个成员是父结构体。这样指向子结构体的指针在内存布局上其起始地址就是父结构体的内容可以被当作父结构体指针来使用。这实现了数据的继承。多态在父结构体中定义函数指针成员如pScan。在初始化子对象时将子对象特有的扫描函数地址赋值给这个指针。之后通过父类指针调用pScan时实际执行的是子类的函数。这实现了行为的“多态”。这种模式在驱动开发中极其有用。比如我们可以定义一个抽象的“输入设备”基类然后派生出“独立按键”、“矩阵键盘”、“编码器”等子类。上层应用只需要调用基类定义的统一扫描接口pScan而不用关心底层具体是什么设备。2.3 为何在STM32上需要这样做你可能觉得裸机程序简单直接就好。但当项目规模增长以下痛点会愈发明显代码重复每个按键的扫描逻辑几乎相同但需要为每个引脚写一遍。紧耦合按键处理逻辑和硬件引脚定义、应用层业务逻辑混杂在一起。难以扩展想增加一个双击功能或改变消抖算法需要到处修改代码。状态管理混乱多个按键的状态、标志位散落在不同的全局变量中。面向对象的设计正是为了解决这些问题。它通过提高代码的模块化、可复用性和可维护性让程序结构更清晰长期来看极大降低了开发和调试成本。3. 按键对象的核心模型设计理论说完了我们开始动手设计。一个健壮的按键驱动核心是状态机。我们将状态机模型与面向对象思想结合设计出按键对象的“蓝图”。3.1 按键状态机建模一个经典的按键过程包括空闲、消抖、按下确认、等待释放、释放消抖等状态。我们用一个枚举来定义这些状态typedef enum { KEY_STATE_IDLE, // 空闲状态 KEY_STATE_DEBOUNCE, // 消抖状态检测到下降沿或上升沿 KEY_STATE_PRESSED, // 按下确认状态 KEY_STATE_RELEASE, // 释放等待状态 KEY_STATE_LONG_PRESS, // 长按状态 // 可以根据需要扩展如 KEY_STATE_DOUBLE_CLICK } KeyState_t;状态机的迁移由定时扫描比如每10ms一次触发根据当前物理电平与状态决定下一个状态。3.2 定义按键对象结构体这是封装的核心。我们将所有属于一个按键的数据封装进一个结构体。// 按键对象结构体 typedef struct Key { // 1. 属性数据成员 GPIO_TypeDef *port; // 按键连接的GPIO端口如 GPIOA uint16_t pin; // 按键连接的引脚如 GPIO_PIN_0 KeyState_t state; // 当前按键状态机状态 uint8_t debounce_cnt; // 消抖计数器 uint8_t long_press_cnt; // 长按计时计数器 uint32_t last_scan_tick; // 上次扫描的时间戳用于非阻塞延时 // 2. 配置参数也可视为属性 uint8_t debounce_th; // 消抖阈值扫描次数如 2 - 20ms uint16_t long_press_th; // 长按阈值扫描次数如 100 - 1000ms uint8_t active_level; // 按键有效电平0 或 1 // 3. 事件标志输出给应用层 uint8_t event_single; // 单次按下事件标志 uint8_t event_long; // 长按事件标志 // uint8_t event_double; // 可扩展双击事件标志 // 4. 方法函数指针成员- 实现多态的关键 void (*pScan)(struct Key *this); // 扫描函数指针 void (*pInit)(struct Key *this); // 初始化函数指针 // void (*pGetEvent)(struct Key *this, KeyEvent_t *event); // 另一种设计获取事件的函数指针 } Key_t;设计解析port和pin将硬件依赖封装在对象内部而不是使用宏定义散落在各处。更换引脚只需修改对象初始化参数。state和计数器状态机的核心数据对外完全隐藏。debounce_th和long_press_th将配置参数化。不同的按键可以有不同的消抖时间和长按时间比如一个需要快速响应的按键和一个需要防止误触的按键。active_level兼容低电平有效和高电平有效的按键通用性更强。event_xxx标志这是对象对外提供的“接口”。应用层定期读取这些标志来判断是否有按键事件发生。这是一种松耦合的设计应用层不关心状态机如何运转只关心结果。函数指针pScan和pInit这是实现多态的“钩子”。基类Key_t定义了这些指针具体的初始化函数和扫描函数在创建具体按键对象时绑定。如果我们未来想换一种更先进的扫描算法比如基于硬件定时器捕获只需要写一个新的扫描函数并绑定给对象而无需修改对象结构或其他代码。3.3 对象初始化与构造函数在C中构造函数自动调用。在C中我们需要显式地编写一个初始化函数来扮演构造函数的角色。/** * brief 按键对象初始化构造函数 * param this: 按键对象指针 * param gpio_port: GPIO端口 * param gpio_pin: GPIO引脚 * param active_lvl: 有效电平 (0或1) * retval 无 */ void Key_Init(Key_t *this, GPIO_TypeDef *gpio_port, uint16_t gpio_pin, uint8_t active_lvl) { // 初始化属性 this-port gpio_port; this-pin gpio_pin; this-active_level active_lvl; this-state KEY_STATE_IDLE; this-debounce_cnt 0; this-long_press_cnt 0; this-event_single 0; this-event_long 0; // 初始化配置参数这里使用默认值也可通过参数传入 this-debounce_th 2; // 默认消抖2次即20ms假设10ms扫描一次 this-long_press_th 100; // 默认长按1秒 // 绑定方法函数指针 this-pInit Key_Init; // 指向自身有点递归意味通常这里绑一个硬件GPIO初始化函数 this-pScan Key_Scan_StateMachine; // 绑定默认的状态机扫描函数 // 调用硬件初始化如果需要配置上拉/下拉输入可以在这里调用HAL_GPIO_Init // 例如_Key_GPIO_Init(this); }注意事项这个Key_Init函数本身也被赋值给了pInit指针这通常用于对象的“重置”或“重新初始化”。更常见的做法是pInit指向一个只进行硬件初始化的私有函数而Key_Init作为对外的“构造函数”。我们将默认的扫描算法Key_Scan_StateMachine绑定到了对象上。这是我们的“默认实现”。4. 核心状态机扫描算法的实现这是按键驱动的灵魂。我们将实现绑定到pScan指针的那个默认扫描函数。4.1 算法流程详解我们假设该函数被一个定时器中断或主循环中的定时任务调用周期为10ms。/** * brief 按键状态机扫描函数默认实现 * param this: 按键对象指针 * retval 无 */ void Key_Scan_StateMachine(Key_t *this) { uint8_t current_level HAL_GPIO_ReadPin(this-port, this-pin); // 读取当前物理电平 uint8_t active_pressed (current_level this-active_level); // 判断是否为有效按下 switch (this-state) { case KEY_STATE_IDLE: { if (active_pressed) { // 检测到潜在按下进入消抖状态 this-state KEY_STATE_DEBOUNCE; this-debounce_cnt this-debounce_th; // 装载消抖计数器 } } break; case KEY_STATE_DEBOUNCE: { if (active_pressed) { // 持续为有效电平 if (--(this-debounce_cnt) 0) { // 消抖完成确认按下 this-state KEY_STATE_PRESSED; this-long_press_cnt this-long_press_th; // 装载长按计数器 // 注意此时不立即触发事件等待释放或长按 } } else { // 消抖期间电平变化了认为是抖动回到空闲 this-state KEY_STATE_IDLE; } } break; case KEY_STATE_PRESSED: { // 按下确认状态 if (!active_pressed) { // 如果已经释放进入释放消抖状态 this-state KEY_STATE_RELEASE; this-debounce_cnt this-debounce_th; } else { // 持续按下检查长按 if (--(this-long_press_cnt) 0) { // 达到长按阈值 this-state KEY_STATE_LONG_PRESS; this-event_long 1; // 触发长按事件 } } } break; case KEY_STATE_RELEASE: { if (!active_pressed) { // 持续为无效电平已释放 if (--(this-debounce_cnt) 0) { // 释放消抖完成 this-state KEY_STATE_IDLE; // 只有在非长按的情况下才触发单次按下事件 if (this-event_long 0) { this-event_single 1; } // 如果需要这里可以清零event_long为下一次识别做准备 } } else { // 释放消抖期间又按下了回到按下状态处理快速连按 this-state KEY_STATE_PRESSED; this-long_press_cnt this-long_press_th; // 重置长按计数器 } } break; case KEY_STATE_LONG_PRESS: { // 长按保持状态 if (!active_pressed) { // 长按后释放 this-state KEY_STATE_RELEASE; this-debounce_cnt this-debounce_th; } // 如果需要长按连续触发如音量持续增加可以在这里定期设置事件标志 // 例如if((--this-repeat_cnt) 0) { this-event_long_hold 1; this-repeat_cnt REPEAT_TH; } } break; default: this-state KEY_STATE_IDLE; break; } }4.2 设计要点与避坑指南事件触发时机这是最容易出错的地方。注意event_single单击事件是在释放消抖完成KEY_STATE_RELEASE - KEY_STATE_IDLE后才置位的。而event_long长按事件是在达到长按阈值KEY_STATE_PRESSED - KEY_STATE_LONG_PRESS时立即置位的。这符合大多数交互逻辑长按触发后即使手指不松开动作也已生效单击则必须等手指松开才算一次完整操作。消抖计数器递减消抖和长按的判断都采用“递减到0”的模式而不是“递增到阈值”。这样做的好处是阈值debounce_th,long_press_th可以直接作为初始值装载逻辑清晰。状态迁移的完整性每个case里都要考虑所有可能的输入active_pressed为真或假并给出明确的状态迁移路径避免状态机“卡死”在某个状态。资源占用这个扫描函数本身只包含简单的判断和赋值计算量极小10ms调用一次对STM32来说毫无压力。所有按键对象共享同一个函数代码只有数据不同内存效率高。5. 多态与继承扩展更复杂的输入设备面向对象最大的威力在于扩展性。假设我们现在需要支持一个旋转编码器。编码器有A、B两个相位信号判断逻辑和独立按键完全不同。但我们希望应用层能用类似的方式处理它比如调用一个统一的Scan方法然后检查event_left或event_right标志。5.1 定义基类输入设备接口首先我们抽象出一个所有输入设备的基类。它只定义最通用的接口。// 输入设备事件类型 typedef enum { EVT_NONE 0, EVT_PRESS, EVT_LONG_PRESS, EVT_ROTATE_CW, // 顺时针旋转 EVT_ROTATE_CCW, // 逆时针旋转 EVT_CLICK, // ... 其他事件 } InputEvent_t; // 输入设备基类结构体 typedef struct InputDevice { // 公共数据可选 uint32_t id; char name[16]; // 方法函数指针- 定义接口 void (*Init)(struct InputDevice *dev); void (*Scan)(struct InputDevice *dev); InputEvent_t (*GetEvent)(struct InputDevice *dev); void (*ClearEvent)(struct InputDevice *dev); // 可以预留一个指针指向设备私有数据 void *private_data; } InputDevice_t;5.2 实现编码器子类现在我们创建编码器类它“继承”自InputDevice_t。// 编码器私有数据结构 typedef struct { GPIO_TypeDef *port_a; uint16_t pin_a; GPIO_TypeDef *port_b; uint16_t pin_b; uint8_t last_state; // 上次AB相的状态编码 (00, 01, 10, 11) uint8_t cw_count; // 顺时针计数 uint8_t ccw_count; // 逆时针计数 uint8_t threshold; // 计次阈值用于软件滤波 InputEvent_t pending_event; // 待获取的事件 } EncoderPrivate_t; // 编码器对象结构体继承 typedef struct Encoder { InputDevice_t base; // 第一个成员必须是基类这是实现继承的关键。 EncoderPrivate_t priv; // 私有数据 } Encoder_t;关键技巧Encoder_t结构体的第一个成员是InputDevice_t base。这意味着Encoder_t对象的内存起始部分就是一个完整的InputDevice_t。因此一个Encoder_t *类型的指针可以安全地强制转换为InputDevice_t *类型而不会访问到错误的数据。这就是C语言实现“继承”的经典方法。接下来实现编码器特有的函数static void _Encoder_Scan(InputDevice_t *dev) { Encoder_t *enc (Encoder_t *)dev; // 通过基类指针转换为子类指针 EncoderPrivate_t *p enc-priv; uint8_t a HAL_GPIO_ReadPin(p-port_a, p-pin_a); uint8_t b HAL_GPIO_ReadPin(p-port_b, p-pin_b); uint8_t current_state (a 1) | b; // 组合成2位状态 // 简单的状态机判断旋转方向 switch (p-last_state) { case 0: if(current_state 2) p-cw_count; else if(current_state 1) p-ccw_count; break; case 1: if(current_state 0) p-cw_count; else if(current_state 3) p-ccw_count; break; case 2: if(current_state 3) p-cw_count; else if(current_state 0) p-ccw_count; break; case 3: if(current_state 1) p-cw_count; else if(current_state 2) p-ccw_count; break; } p-last_state current_state; // 判断是否达到触发阈值 if (p-cw_count p-threshold) { p-pending_event EVT_ROTATE_CW; p-cw_count 0; p-ccw_count 0; // 清空另一个方向的计数防止干扰 } else if (p-ccw_count p-threshold) { p-pending_event EVT_ROTATE_CCW; p-ccw_count 0; p-cw_count 0; } } static InputEvent_t _Encoder_GetEvent(InputDevice_t *dev) { Encoder_t *enc (Encoder_t *)dev; InputEvent_t evt enc-priv.pending_event; enc-priv.pending_event EVT_NONE; // 取出后清零 return evt; }最后编写编码器的“构造函数”它将子类的函数绑定到基类的函数指针上void Encoder_Init(Encoder_t *enc, GPIO_TypeDef *port_a, uint16_t pin_a, GPIO_TypeDef *port_b, uint16_t pin_b) { // 初始化基类部分 enc-base.Init NULL; // 或者指向一个默认初始化函数 enc-base.Scan _Encoder_Scan; // 绑定 enc-base.GetEvent _Encoder_GetEvent; // 绑定 enc-base.ClearEvent NULL; enc-base.private_data enc-priv; // 初始化私有数据 enc-priv.port_a port_a; enc-priv.pin_a pin_a; // ... 初始化其他私有成员 enc-priv.threshold 4; // 默认计次4次触发一次事件用于滤波 enc-priv.pending_event EVT_NONE; }5.3 统一管理与应用层调用现在我们可以用统一的方式管理所有输入设备。创建一个设备数组#define MAX_INPUT_DEVICES 5 InputDevice_t *input_devices[MAX_INPUT_DEVICES] {NULL}; uint8_t dev_count 0; void Input_RegisterDevice(InputDevice_t *dev) { if (dev_count MAX_INPUT_DEVICES) { input_devices[dev_count] dev; if (dev-Init) dev-Init(dev); } }在定时中断或主循环中统一扫描所有设备void Input_Scan_All(void) { for (int i 0; i dev_count; i) { if (input_devices[i] input_devices[i]-Scan) { input_devices[i]-Scan(input_devices[i]); } } }应用层处理事件void Application_Task(void) { InputEvent_t evt; for (int i 0; i dev_count; i) { if (input_devices[i] input_devices[i]-GetEvent) { evt input_devices[i]-GetEvent(input_devices[i]); switch (evt) { case EVT_PRESS: // 处理按键按下来自Key_t对象 break; case EVT_ROTATE_CW: // 处理编码器顺时针旋转来自Encoder_t对象 Volume_Increase(); break; // ... 处理其他事件 default: break; } } } }至此我们完成了面向对象设计的闭环应用层Application_Task完全不知道它处理的是按键还是编码器它只面对统一的InputDevice_t接口。新增一种输入设备比如触摸传感器只需要按照同样的模式创建新的XXX_t结构体和对应的函数然后注册到系统即可。原有代码一行都不用改。这就是面向对象带来的强大扩展性。6. 在STM32项目中的集成与优化实践理论模型很美好但要落地到真实的STM32项目还需要考虑一些工程细节。6.1 对象存储与生命周期管理在资源紧张的嵌入式系统中动态内存分配malloc通常是被避免的。因此我们的对象最好使用静态内存分配。// 在文件作用域或头文件中定义具体的对象实例 Key_t key_user; // 用户按键 Key_t key_menu; // 菜单按键 Encoder_t encoder_volume; // 音量编码器 // 在系统初始化时构造这些对象 void System_Input_Init(void) { Key_Init(key_user, GPIOA, GPIO_PIN_0, 0); // 低电平有效 Key_Init(key_menu, GPIOC, GPIO_PIN_13, 0); Encoder_Init(encoder_volume, GPIOB, GPIO_PIN_6, GPIOB, GPIO_PIN_7); // 注册到统一的管理器 Input_RegisterDevice((InputDevice_t*)key_user); // 注意需要强制类型转换 Input_RegisterDevice((InputDevice_t*)key_menu); Input_RegisterDevice((InputDevice_t*)encoder_volume); }注意将Key_t*转换为InputDevice_t*之所以安全是因为在Encoder_t中我们确保了基类base是第一个成员。但对于Key_t它本身并不是从InputDevice_t派生的。为了让按键也能接入这个统一框架我们需要让Key_t也“继承”InputDevice_t或者采用适配器模式——为Key_t单独创建一个适配器结构体里面包含一个InputDevice_t和一个Key_t指针。这是工程实践中更常见的做法保持了Key_t的独立性和框架的通用性。6.2 定时扫描的驱动面向对象的状态机需要一个稳定的时间基准来驱动。最佳实践是使用一个硬件定时器如SysTick或通用定时器产生精确的时基中断例如10ms在中断服务程序ISR中只设置标志位。volatile uint8_t g_key_scan_tick 0; // 在10ms定时器中断中 void TIMx_IRQHandler(void) { if(__HAL_TIM_GET_FLAG(htimx, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htimx, TIM_FLAG_UPDATE); g_key_scan_tick 1; // 设置标志 } }在主循环中检查并执行扫描while(1) { // ... 其他任务 if(g_key_scan_tick) { g_key_scan_tick 0; Input_Scan_All(); // 扫描所有注册的输入设备 } Application_Task(); // 应用层处理事件 // ... }绝对禁止在中断服务程序中进行复杂的扫描和状态判断这会导致中断执行时间过长影响系统实时性。中断里只做最轻量的标记工作。6.3 功耗与性能考量扫描频率10ms是一个常用值兼顾了响应速度和CPU占用。对于电池供电设备在休眠模式下可以关闭定时器通过GPIO外部中断唤醒后再开启定时扫描。对象数量每个对象占用几十字节的RAM。对于有几十个按键的大型矩阵键盘可以为整个矩阵创建一个对象其扫描函数内部遍历所有行列而不是为每个键都创建一个对象。这体现了设计模式的灵活性组合模式。你可以创建一个MatrixKeypad_t对象它内部管理一个键值映射表对外仍然产生统一的按键事件。事件标志处理应用层在读取事件标志如event_single后必须及时清除否则会重复触发。可以在GetEvent函数中自动清除也可以要求应用层显式调用一个ClearEvent函数。7. 常见问题、调试技巧与进阶思考即使设计再完善调试阶段也总会遇到问题。这里分享一些实战中积累的经验。7.1 典型问题排查表现象可能原因排查步骤与解决方案按键无任何反应1. 扫描函数未被定期调用。2. GPIO端口或引脚配置错误输入模式、上拉/下拉。3. 按键硬件故障或接触不良。1. 检查定时器配置和g_key_scan_tick标志位是否正常置位。2. 使用调试器或printf在扫描函数入口打印信息确认函数被执行。3. 用万用表或调试器直接读取GPIO IDR寄存器值确认物理电平变化正确。按键反应迟钝或过快扫描周期设置不当。调整定时器中断周期。10ms是起点可根据手感微调。消抖阈值debounce_th也要相应调整例如周期5ms阈值可设为4次20ms。单击偶尔触发长按长按阈值long_press_th设置过小。增大long_press_th值。通常长按需要800ms-1000ms如果扫描周期10ms阈值应设为80-100。快速连按只识别一次状态机从RELEASE到IDLE后没有足够快的响应下一次按下。或者消抖时间过长。检查KEY_STATE_RELEASE状态确保释放消抖时间不宜过长通常和按下消抖一致即可。可以尝试在IDLE状态立即响应新的下降沿而不是必须等释放消抖完全结束。编码器旋转方向反了A、B相序接反或状态机判断逻辑写反。交换代码中p-cw_count和p-ccw_count的累加条件。最好在初始化时打印出A、B相的状态变化序列对照编码器真值表检查逻辑。多个按键对象相互干扰对象数据被意外修改通常是数组越界或指针错误。确保每个对象在初始化时地址正确。使用调试器观察对象在内存中的数据变化。检查Input_RegisterDevice时是否超过了数组最大数量。7.2 调试心得善用调试器与日志观察对象内存在IDE如Keil MDK、STM32CubeIDE的调试模式下将key_user等对象添加到Watch窗口。单步执行时你可以清晰地看到state、debounce_cnt、event_xxx等成员是如何随着扫描变化的。这是理解状态机运行最直观的方式。添加简易日志在状态机切换状态的关键点通过一个串口打印函数输出信息如printf([KEY] PortA.0 State: %d - %d\n, old_state, new_state);。注意要用条件编译宏如#ifdef KEY_DEBUG包裹方便发布时关闭。模拟输入在硬件不稳定时可以在代码中暂时屏蔽真实的HAL_GPIO_ReadPin用软件模拟一个固定的电平序列来测试状态机逻辑是否正确。7.3 进阶思考更多设计模式的应用这个面向对象的按键框架已经初具雏形你还可以在此基础上引入更多设计模式使其更强大观察者模式按键对象不是设置事件标志而是维护一个观察者列表。当事件发生时自动通知所有观察者如界面更新任务、声音播放任务。这彻底解耦了事件产生和消费。命令模式将每个按键事件与一个具体的“命令对象”绑定。命令对象包含一个Execute()函数。这样改变按键功能只需要改变绑定的命令对象而无需修改任何状态机或应用逻辑代码。非常适合实现可配置的按键映射。模板方法模式在基类InputDevice_t的Scan函数中定义算法的骨架例如读取硬件-滤波-状态判断-设置事件而将“读取硬件”、“滤波”等步骤定义为抽象方法由子类去实现。这可以进一步统一不同设备的处理流程。从一堆散乱的全局变量和条件判断到一个个职责清晰、易于管理的对象这种转变带来的代码整洁度和可维护性的提升是巨大的。它迫使你从“实现功能”的思维转向“设计结构”的思维。第一次尝试可能会觉得多写了很多代码但当你需要修改、调试、尤其是增加新功能时你会感谢自己当初所做的设计。在嵌入式开发中良好的软件架构不是奢侈品而是应对复杂性和长期维护的必需品。希望这个基于STM32的面向对象按键驱动设计能为你打开一扇新的大门。
返回列表