资讯动态

STM32嵌入式C++实战:从GPIO类封装到串口调试

发布时间:2026/9/29 1:08:16 来源:尧图企业网站定制
“看了三篇了一行都没让我写呢”——这句留言是前天深夜看到的当时我正犹豫第五篇该从哪里切入结果被这句话一语点醒。这是“基于STM32的嵌入式C编程之旅”系列连载的第五篇前四篇我把大半篇幅花在了嵌入式C能解决什么问题、STM32芯片内部是什么结构、开发环境怎么选、C里哪些特性在单片机上真正管用这些“务虚”的事情上。说实话自己回头看也觉得有点“纸上谈兵”。所以今天这一篇咱们把键盘敲起来。目标定得小一点但足够戳中痛点在STM32上用C封装一个GPIO控制类把LED点灯这件事做成像模板一样可复用的代码然后通过串口把程序跑起来的状态打出来。整个过程我会把手伸到代码背后讲清楚每一行为什么要这么写而不是丢一段能跑就完事的例程。这篇更适合已经看过前面几篇、心里对C和STM32有基本框架的读者如果你是零基础刚点进来的也完全可以跟着走我会尽量把每个概念都掰开揉碎。1. 先别急着骂我前三篇为什么没让你写代码这篇既然开篇就动手写那不妨先把旧账翻一翻。你可能会觉得学编程这种事老师上来让敲个Hello World才算正常哪有讲了四篇还在讲概念的。但嵌入式C偏偏就是这么个不太一样的领域它有它绕不开的弯。1.1 嵌入式C和写应用层C完全是两码事很多从应用开发转过来的人最初都会有一个错觉觉得嵌入式就是“大点儿的单片机编程”代码嘛无非是if、else、循环那一套。可真上手之后会发现应用层开发和嵌入式中间隔着的不是一层窗户纸而是一整条硬件栈。做应用层开发你写的代码跑在厚厚的操作系统上面内存管理、进程调度、文件系统、网络协议栈这些全都有系统帮你托底。你不需要知道变量在内存里究竟放在哪不用管设备驱动怎么初始化写C就是跟类和对象打交道程序崩溃了也有日志可以翻。但嵌入式不一样你写的每一行C最终都会落成对某一块寄存器地址的读写。你把GPIO的模式配置位写错了灯就是不亮你把串口的波特率分频系数算错了输出的就是一堆乱码中断服务函数没写好可能整个系统直接死机。换句话说这里没有“兜底”。所以前几篇我一直在讲概念其实就是在做一件事帮你建立一张“硬件地图”。寄存器的模型是什么样的时钟树怎么分频外设挂在哪条总线上GPIO为什么有输入输出和复用模式——这些知识就像炒菜之前的备菜环节。菜没备好就下锅炒出来多半是夹生的。1.2 没有前四篇的铺垫抄代码你都不知道往哪改我常说一句话嵌入式最不缺的就是代码。网上随便一搜STM32点灯、串口、智能小车、平衡车各种现成工程铺天盖地下载下来烧进去基本就能跑。但你拷过来想改成自己的功能麻烦就来了我想把LED从PC13改到PA0要改哪里串口波特率想要更高的时钟树怎么调想在点灯基础上加个定时器中断中断服务函数怎么写这些问题光靠抄代码是抄不会的。前面四篇虽然没动手但已经把这张地图画好了。第一篇讲了嵌入式C的优势和C语言的关系第二篇拆解了STM32的芯片架构和外设模型第三篇对比了开发工具链第四篇把C的封装、重载、模板这些特性在嵌入式语境下翻译了一遍。有了这些基础今天你跟着我写代码就会发现每一步都有据可循初始化结构体的每一个成员我告诉你对应的是哪个寄存器、控制的是硬件的什么行为封装成类的时候你也能看出为什么这样设计是合理的。2. 热身第一行代码前的工具链与工程准备动手之前先把家伙事儿备齐。嵌入式开发不像纯软件装个IDE就能写硬件、下载器、工程配置一个都不能少。我在这里直接把目前性价比最高的方案列给你照着配就行。2.1 板子与芯片选型STM32F103C8T6依然是教学首选这一系列已经开了五篇开发板的选择在前面的文章里其实已经给过结论但考虑到新读者会从这篇开始跟我再说一下理由。STM32F103C8T6这颗片子简直是为学嵌入式C量身定做的价格常年稳定在十几块级别一条街的小吃都比它贵拥有64KB Flash和20KB SRAM跑一个中等复杂度的C工程完全够用外设资源丰富GPIO、串口、I2C、SPI、定时器、ADC一应俱全后面要学的通信协议基本都覆盖了。而且资料多到爆炸。芯片是ST出货量最大的型号之一你遇到任何问题大概率早就有人踩过坑并且把解决方案发在论坛上了。我自己的习惯是但凡要验证一个新想法新库第一块试验板永远是这颗芯片外加一个ST-Link下载器。整套加起来不到五十块钱比奶茶便宜能帮你打开嵌入式C的大门这笔投资值。2.2 从CubeMX生成工程到C化改造的完整步骤现在正式创建工程。我用STM32CubeMX做初始化配置生成MDK-ARM工程然后改造为支持C的模式。这个流程是目前最主流、也最不容易出错的配置方式。第一步打开STM32CubeMX芯片型号搜索STM32F103C8T6选中后进入配置页。在System Core里配置RCCHSE选择Crystal/Ceramic Resonator时钟树把HCLK设为72MHz它会自动算好PLL参数。然后在Pinout Configuration里把PC13配置为GPIO_Output再配置一个串口USART1Mode选Asynchronous波特率设1152008位数据无校验1位停止位。这些都配置完之后Toolchain/IDE选MDK-ARMProject Name填个工程名点生成即可。第二步用Keil MDK打开生成的工程。右键Source Group 1选择Add New Item新建两个文件一个app_user.cpp一个app_user.hpp。这里有个小巧思我故意不推荐把main.c直接改名为main.cpp而是另起炉灶保留厂家生成的main.c作为入口自己写的C代码全部放app_user.cpp里。原因有两个一是CubeMX的图形化配置以后改动重新生成工程时如果修改了main.c可能会被覆盖辛辛苦苦写的代码就白费了二是C和C代码分离哪些是工具生成的、哪些是自己写的一目了然。第三步配置编译选项。在Keil里打开Options for Target在C/C标签页里确认C模式相关选项已经打开。Keil对C的支持是自动识别的只要源文件后缀是.cpp编译器就会按C来编译不需要额外开关。然后在Target标签页里勾选Use MicroLIB这一步对后面串口printf输出非常关键。2.3 C工程里绕不开的三个配置点新手第一次建C工程最容易在编译器选项上卡住我把三个关键的配置点列成一个表你对照着检查一遍配置项正确做法背后的原因C文件头文件包含在.cpp文件开头用extern C包裹HAL库头文件C编译器会对函数名做符号改编name mangling不包起来就找不到HAL_GPIO_WritePin这类C函数的符号MicroLIB开关Keil Target标签页勾选Use MicroLIB标准C库的printf依赖半主机模式semihosting裸机上没有操作系统支持MicroLIB剔除了这部分逻辑配合重定向才能让printf走串口堆(Heap)大小CubeMX默认0x200基本够用用new必须改大STM32只有20KB SRAM默认堆只有512字节随便new一个对象就可能崩这三个配置点本质上都是围绕着C与C库、C运行时的协作在打转。你以后自己搭建任何嵌入式C工程都会遇到同样的问题把这套配置理解透了换芯片、换IDE也能举一反三。提示MicroLIB对C标准库的支持是受限的如果你后面用到了std::string或者iostream这种重货可能会出问题。实际项目里大家更习惯自己封装一版轻量输出或者用C库的printf就算交差一来资源紧张二来串口输出本来也不需要那么重的抽象。3. 核心实操用C封装第一个LED类工程环境和工具链都就绪了现在进入重头戏。我们从最经典的点灯程序开始看看C语言写法和C写法到底差在哪然后亲手写出第一个属于你自己的LED类。3.1 先看C语言的常规写法再谈封装的必要性先把HAL库的C语言点灯代码摆出来让大家有个参照物void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_13; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, GPIO_InitStruct); }然后在main函数里这样用int main(void) { HAL_Init(); SystemClock_Config(); LED_Init(); while (1) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); HAL_Delay(500); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); HAL_Delay(500); } }这段代码没问题功能完全正确但我每次看到一堆散落在主程序里的GPIO操作就头疼。如果只是点一个灯确实无所谓但实际产品里从来不会只点一个灯。三个LED、两个按键、一个蜂鸣器你试试把这些初始化代码全部堆在main函数开头再顺着代码逻辑一个个搜GPIO_PIN_x、GPIOC维护起来简直就是灾难。更致命的是这种写法把所有细节都暴露给了调用者。“PC13是高电平点亮还是低电平点亮”“哪个引脚接了哪个外设”这些问题全都需要读代码的人自己记着。一旦把灯从PC13挪到PA0你就要在满屏代码里找所有出现GPIOC和GPIO_PIN_13的地方改了又改漏改一处就是Bug。这是典型的面向过程写法的天花板代码本身好懂但不好用、不好改、不好扩展。3.2 LED类的设计思路与完整实现C的封装就是来解决这类问题的。我们把LED相关的硬件操作收拢到一个类里面把会变化的参数接在哪个端口、哪个引脚通过构造函数传进去把稳定的行为亮、灭、翻转、状态查询封装成方法。先看头文件LED.hpp#pragma once extern C { #include stm32f1xx_hal.h } class LED { public: explicit LED(GPIO_TypeDef* port, uint16_t pin); void on(); void off(); void toggle(); bool isOn() const; private: GPIO_TypeDef* port_; uint16_t pin_; };再看实现文件LED.cpp#include LED.hpp LED::LED(GPIO_TypeDef* port, uint16_t pin) : port_(port), pin_(pin) { GPIO_InitTypeDef init {0}; init.Pin pin; init.Mode GPIO_MODE_OUTPUT_PP; init.Pull GPIO_NOPULL; init.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(port, init); } void LED::on() { HAL_GPIO_WritePin(port_, pin_, GPIO_PIN_SET); } void LED::off() { HAL_GPIO_WritePin(port_, pin_, GPIO_PIN_RESET); } void LED::toggle() { HAL_GPIO_TogglePin(port_, pin_); } bool LED::isOn() const { return HAL_GPIO_ReadPin(port_, pin_) GPIO_PIN_SET; }使用的时候非常简洁#include LED.hpp LED boardLed(GPIOC, GPIO_PIN_13); int main(void) { HAL_Init(); SystemClock_Config(); while (1) { boardLed.toggle(); HAL_Delay(500); } }注意我把LED对象放在了全局位置。这是嵌入式C里常见的做法——全局静态对象在main执行之前就会完成构造构造函数里就把硬件初始化做了。这样main函数一进来LED就已经处于可用状态少了一堆调用初始化的步骤。再拆解几个设计的细节。构造函数用explicit修饰是为了防止GPIO_TypeDef*和uint16_t发生隐式类型转换。嵌入式开发里我特别强调这一点因为我们面对的是指针和硬件地址一旦隐式转换把引脚号搞错硬件上表现出来就是引脚乱动甚至短路这种问题极难排查。成员变量port_和pin_带下划线是C代码中比较通行的命名习惯避免构造函数参数和成员变量撞名。isOn()方法用const修饰说明这个查询方法不修改对象状态设计上语义更严谨。这样封装完之后main里的代码就变得极其好读创建一个对象调用方法控制LED。至于内部是怎么写寄存器、怎么初始化结构体的调用者完全不用关心我要换引脚只需要改构造参数代码其他地方一行都不用动。3.3 一个类管三个灯看看复用能力如果项目里要用到三个LEDC语言的写法已经可以想象是什么惨样了。但有了LED类代码就变成这样LED led1(GPIOC, GPIO_PIN_13); LED led2(GPIOA, GPIO_PIN_1); LED led3(GPIOB, GPIO_PIN_0); void flow() { led1.on(); HAL_Delay(300); led1.off(); led2.on(); HAL_Delay(300); led2.off(); led3.on(); HAL_Delay(300); led3.off(); }你会发现新增一个LED的成本从“复制十行初始化代码修改引脚参数”缩小到“新写一行对象定义”。这就是封装最直接的收益。不光是LED按键、蜂鸣器、OLED屏幕、温湿度传感器都可以用同样的思路包装成类。你维护的是一个个行为明确的对象而不是一团纠缠不清的寄存器操作代码量可能没少多少但可读性和可维护性完全不是一个量级。4. 让C的优势再明显一点串口输出与编译期技巧点灯跑通了C封装的好处你也亲身体验了但这才刚刚开始。嵌入式的世界里代码不是光让灯亮就完事的你需要把调试信息、运行状态输出到电脑上看这就离不开串口。这一章我们打通串口printf顺便见识一下C模板带来的“零开销抽象”在MCU上到底意味着什么。4.1 串口重定向让printf在开发板上复活CubeMX已经把USART1初始化配置好了接下来要做的就是在C工程里把printf函数“接到”串口硬件上。原理其实很简单C标准库的printf在最终输出字符时会调用一个底层函数fputc我们只要重写这个函数让它把字符通过HAL_UART_Transmit发出去printf就算串口重定向了。在app_user.cpp里加上这段代码extern C { #include stm32f1xx_hal.h #include stdio.h } extern UART_HandleTypeDef huart1; extern C int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }这里有两个细节特别说明。一是为什么fputc也要套extern C因为fputc在标准库头文件里声明为C链接定义时如果按C语法做符号改编链接器就找不到标准库期望的符号名。二是串口配置使用默认波特率115200从CubeMX生成后HAL_UART_Init已经在main.c里被调用过一次全局的huart1句柄已经初始化好了直接在app_user.cpp里extern声明就能用。完成重定向后就可以在代码里打印LED状态了while (1) { boardLed.toggle(); printf(LED is %s\r\n, boardLed.isOn() ? ON : OFF); HAL_Delay(500); }打开串口助手你就能在电脑上实时看到LED的开关状态。这套串口调试手段是嵌入式开发天天要用到的看家本领。后面凡是涉及传感器数据读取、状态机轮转、调试信息监控全都靠它。4.2 构造函数和析构函数的正确打开方式前面已经展示了构造函数做硬件初始化这个套路这是C嵌入式开发里极其好用的思想业界叫RAII资源获取即初始化。它的核心价值在于对象的生命周期和资源的生命周期绑定在一起对象创建时资源自动就绪对象销毁时资源自动释放。但我要特别泼一盆冷水裸机嵌入式环境里析构函数的调用时机和你想的不太一样。应用层的局部对象在函数返回时会自动析构这在桌面端完全没有问题而嵌入式程序里main函数一般不会返回你用new在堆上创建的对象不delete析构函数永远不会调用。全局对象就算程序终止系统也可能直接进入复位。所以我对嵌入式C里构造函数和析构函数的使用建议是这样的构造函数做初始化是完全可以放心大胆用的。析构函数做硬件关闭、释放资源在裸机上要非常谨慎因为你很难保证析构函数会在正确时机被调用。不要依赖析构函数去做关键性的收尾操作比如保存数据、关闭外设这类操作应该在业务逻辑里显式调用方法完成而不是寄托于语言在某个看不见的时刻来清理。这个坑不少从应用层转过来的同事都踩过我当年也交过学费提前说出来希望大家别重蹈覆辙。4.3 模板的零开销抽象编译期就定好的东西绝不拖到运行期C模板在MCU上的价值经常被低估总觉得模板跟嵌入式八竿子打不着。其实只要理解了“零开销抽象”这句话模板在嵌入式里的魅力立马上来。举个最直白的例子延时。我们看到HAL_Delay(500)这种写法参数500是运行期传进去的内部要经过函数调用、形参传递、边界判断等一堆过程然后在运行时才把这个数字参与运算。如果用模板template uint32_t TICKS class DelayLoop { public: static void delay() { for (volatile uint32_t i 0; i TICKS; i) { __NOP(); } } }; // 使用 DelayLoop500000::delay();这里的500000是编译期常量。编译器在编译时就把这个值代入循环生成一段针对这个具体数值调优过的代码运行时完全没有传参、检查的开销连那个类都不需要真实存在。比起运行期传递数值模板方式相当于把一部分计算和决策提前到了编译期生成的机器码更紧凑、执行路径更直接。这跟生活中的提前规划一个逻辑周末出行前把路线查好、备胎检查好、油加满出发当天就不用在路上手忙脚乱。模板就是那个提前规划运行期就是出发当天。嵌入式的资源那么金贵凡是能在编译期定下来的事就不要拖到运行期再做。注意课堂示例用循环延时直观但实际工程里延时一般不这么写大延时用SysTick定时器小延时收藏夹里备一份自己手搓的nop循环也无妨。模板编译期计算的思路是我们要学的不是这套延时方案本身。5. 实战中必然踩坑的五个问题和排查记录写代码不是顺风满帆的事尤其是嵌入式C第一次把两种技术栈缝在一起坑一定不会少。我把新手阶段最容易踩的五个问题集中整理一下每个问题都按“现象-原因-排查思路”来拆看完能帮你省掉好几个晚上的调试时间。5.1 HardFault嵌入式新手的第一个“错误地狱”现象基本是统一的编译能过、烧录能进、板子一跑就死调试器停下来发现卡在HardFault_Handler的while(1)里面。C工程里HardFault最常见的原因有三个。第一个是全局对象的构造顺序问题。刚才我推荐了全局对象写法但有代价C标准规定全局对象的构造在main之前完成可它没规定不同编译单元之间谁先谁后。如果你写了多个全局对象它们各自构造函数里都依赖硬件初始化而硬件初始化又彼此相关比如UART初始化依赖了GPIO复用配置这个配置又恰好是LED对象干的活对不起谁来先谁就翻车。第二个原因是栈溢出CubeMX默认给的栈大小0x400也就是1KBHAL_Delay调用层级深一点再来几个printf格式缓存和局部变量栈很容易就顶穿了。第三个原因是空指针和野指针封装类传了错误的端口指针或者引脚号。排查的时候先不要慌在HardFault_Handler里打断点然后去寄存器窗口看LR和PC值通常能定位到是哪个函数触发的异常。如果没有头绪优先查全局构造把全局LED对象移到main函数里面在初始化硬件之后再创建看看问题是否消失。这个方法虽然土但判断构造顺序问题特别有效。5.2 extern C和命名空间的冲突这是C和C混编时最经典的绊脚石。有新手喜欢用namespace来组织代码把HAL库头文件和自己的类都放进namespace里结果编译直接报错。根因在于extern C只能出现在全局作用域或者namespace外面。如果你把它嵌在namespace内部就是语法错误。正确姿势是保持extern C块在全局作用域然后namespace里面用引用或者using声明extern C { #include stm32f1xx_hal.h } namespace app { using GPIO_TypeDef ::GPIO_TypeDef; class LED { ... }; }一句话总结凡是C语言写的头文件和函数统统用extern C包在全局作用域里千万别塞进自己的namespace。5.3 new和delete到底能不能用这个问题几乎每个嵌入式C新手都会问上一嘴我直接给出结论能用但千万别放纵地用。STM32F103C8T6的SRAM只有20KBCubeMX默认堆大小才512字节一个稍微胖一点的对象就可能把它消耗殆尽。C里的new如果分配失败默认行为是调用abort在嵌入式环境里就直接死循环连个错误日志都不会给你留。而且嵌入式应用常驻运行频繁new和delete会制造大量堆碎片碎片积累到一定程度即使理论上剩余内存还够也分配不出连续地址系统一样崩溃。我的建议是养成三个习惯能静态分配就静态分配上面的LED对象写法就是静态分配确实需要可变数量时提前规划一个固定大小对象池比如一个容器数组对象从池里取、用完还回去有一天实在逃不过动态构造就用placement new在已经分配好的静态缓冲区上构造对象把C的对象生命周期管理拉回到自己可控的范围。5.4 优化等级一上来行为全变了嵌入式里的编译优化问题简直是玄学现场。明明-O0调试时一切正常改成-O2、-Oz之后LED不闪了、串口乱码了、中断干脆不触发了。这背后十个里面有九个不是编译器坏了而是代码里藏着违反C标准未定义行为的东西最常见的就是未初始化的变量和缺少volatile修饰的寄存器访问。编译器在优化时默认你的代码没有任何未定义行为然后放心大胆地对变量做缓存、删除“多余”读写。可嵌入式的硬件寄存器是读一次变一次的东西如果读写它的指针前不加volatile优化器认为这块内存从头到尾没变过直接把读操作优化掉你拿到手的永远是一个旧值。处理办法有几条涉及硬件寄存器读写的指针统一加volatile中断回调函数和主循环共用的标志变量也加volatile开发阶段用-O0或者-O1等所有功能验证通过了再开-O2如果优化后行为异常先怀疑未初始化变量再检查位操作和指针类型转换百分之九十的问题都出在这两处。5.5 在线调试的几条实用经验最后给几条我日常调试使用频率最高的技巧。在Keil里断点可以直接打在LED::on()这类成员函数内部然后观察传入的this指针指向哪个端口、哪个引脚硬件过失一目了然。Watch窗口添加port_、pin_这些成员变量能看到对象内部状态这在调试多个LED对象时特别受用。串口打印和断点结合着用代码里布好printf点看执行流到了哪里、变量是多少然后再针对性地打断点深挖。裸机调试不必追求高级的trace功能朴素的打印加断点足够解决百分之九十五的问题。6. 从点灯出发下一阶段的路我帮你画好了这一篇从概念落到代码从第一个LED类写到串口输出你手里已经有了C操作STM32最基础的一套模板。但“会点灯”在嵌入式世界里只是幼儿园毕业接下来往哪个方向走我把自己踩过的路重新描一遍给你。6.1 已经做完的C封装可以怎么继续扩展现在你掌握了LED类的封装套路这个套路有非常强大的迁移能力。按键本质上就是反向的LED——引脚从输出模式改成输入模式轮询读状态还要加上消抖逻辑你完全可以做一个Button类构造函数初始化输入模式用一个pressed()方法返回按键状态定时器可以封成Timer类把中断回调函数绑定到对象方法上比嵌入式里到处挂着“土土的”全局中断标志优雅得多串口可以继续封装成UART类带一个发送一个接收后面还能往环形缓冲区上靠。再往后通信协议就是绕不开的战场。嵌入式里常见的五种通信协议——UART、SPI、I2C、CAN、USB——每一个都能用今天的思路做C封装把初始化过程塞进构造函数把读写操作变成一个个语义清晰的方法。比如用I2C驱动OLED屏幕用SPI驱动Flash芯片用USB实现虚拟串口和上位机通信用CAN把多块板子连成总线网络。这些选题每一个都够写成一整篇系列文章咱们后面一篇篇来。6.2 我这几年的实操心得C不是给嵌入式添乱的文章写到这里按惯例我分享一点个人真实的项目体会而不是网上下载的“经验之谈”。我前前后后用C写了四五年嵌入式代码最大的感受是C在嵌入式里的收益曲线跟你使用它的姿势高度挂钩。如果只把C当成“带class的C”那它确实比C多了一堆语法糖价值不大但如果你能克制地用好封装、模板、RAII这几个特性项目规模上去之后维护成本会比纯C低非常明显。同一段业务逻辑C版本的代码可能散落在各个函数里靠全局变量传递状态C版本能把状态收拢成对象让每个模块的边界变得清晰多人协作的时候特别重要。我踩过最大的坑是有一段时间为了“面向对象”而面向对象给外设建立了一套包括基类、虚函数、抽象接口的继承体系。结果就是代码是优雅了调试起来惨不忍睹——在线断点根本看优雅不优雅只知道派生类方法绕来绕去根本定位不到具体硬件操作。后来我给自己定了一条规矩一个类的职责最多只能对应一个硬件外设超过两个立刻拆分。保持简单C在嵌入式里才能真正发挥价值。学习上我的建议也很简单先把HAL库用顺手理解清楚每个外设的初始化和读写逻辑再上C封装否则封装出来的就是一层华丽的壳。每次只学一个特性Led类学会了下一个类你试试把串口也收进去再下一个类试试模板步子别跨太大。每完成一个封装你会对自己的代码更有一分掌控感这就是嵌入式路上最实实在在的正反馈。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑