STM32上C++实战:触摸屏项目中的类封装与零成本抽象
发布时间:2026/9/25 2:01:54来源:尧图网络
1. 从裸机到C为什么要在单片机上折腾这门“重”语言很多人第一次听到“用C写单片机”这个说法第一反应跟我当年一样这不是杀鸡用牛刀吗51单片机那点RAM连个像样的字符串都存不下上C不是自找麻烦我刚开始接触嵌入式那会儿也是这么想的直到后来做的一个项目——基于STM32F103C8T6的触摸屏交互终端——用纯C写到三千行的时候整个人都快疯了。状态机套状态机回调函数满天飞改一个触摸响应逻辑要翻五六个文件。那时候我才真正理解C在单片机上的价值根本不在于“性能”而在于用编译期的抽象换运行期的可维护性。这个系列的第一篇我聊了C在单片机上跑的可行性、工具链怎么搭、哪些特性该用哪些该躲。这一篇是续集重点落在实际项目里怎么把C用出效果。具体来说我会围绕一个完整的案例——触摸屏坐标采集与屏幕内容映射——把C的类封装、模板、const/final/static这些特性怎么落地讲透。同时也会回应一些热词里高频出现的问题比如“单片机C语言没有堆栈吗为什么”“51单片机的哈佛结构对C意味着什么”“冒泡排序在单片机里怎么写才不翻车”。这篇文章适合谁看如果你已经能用C语言点灯、写中断、调串口但项目一复杂就感觉代码要失控那这篇就是写给你的。如果你还在C入门阶段纠结“单片机到底能不能跑C”也可以看但建议先把第一篇的基础部分补一下。我不会讲太多语法教科书上的东西重点放在为什么这么设计、实际怎么操作、踩过哪些坑。先给一个总体的判断在STM32F103C8T6这个级别的芯片上72MHz主频、20KB RAM、64KB FlashC的运行时开销完全可以控制在可接受范围内前提是你得知道哪些特性是零成本的哪些是带隐藏代价的。下面我按项目推进的顺序一层一层拆开讲。2. 项目整体设计与C特性选型思路2.1 为什么选C而不是纯C一个触摸屏项目的真实痛点先把这个项目的需求说清楚。硬件平台是STM32F103C8T6最小系统板外挂一块2.8寸TFT触摸屏ILI9341驱动电阻触摸功能要求是屏幕上显示若干按钮和滑块用户触摸后系统识别触摸点坐标判断落在哪个控件上执行对应动作同时界面要有状态反馈。用纯C写最直接的做法是定义一堆全局变量按钮的x、y、宽、高、状态、回调函数指针然后写一个巨大的touch_handler()函数里面一堆if-else判断坐标落在哪个矩形里。这个方案能跑我第一版就是这么干的。问题是按钮数量从3个加到8个的时候touch_handler()变成了两百多行每加一个控件要改三处地方而且滑块和按钮的触摸逻辑还不一样if-else里开始出现嵌套。C的解法是把“控件”抽象成一个类。每个控件自己知道自己的位置、大小、怎么响应触摸。触摸处理函数只需要遍历控件列表把坐标丢给每个控件问一句“这个点你管不管”谁管谁处理。加新控件类型只需要继承基类实现虚函数不用动主循环。这就是封装和多态在嵌入式里的实际价值——不是为了炫技是为了让代码在需求变化时不崩盘。2.2 零成本抽象原则哪些C特性可以放心用“零成本抽象”是C的核心设计哲学意思是你不用的特性不产生开销你用的特性其开销不超过手写C。但在单片机上这句话要打个折扣因为编译器优化水平和标准库实现质量参差不齐。我实测下来在STM32F103C8T6 arm-none-eabi-gcc的环境下以下特性基本是零成本的类与封装成员函数不虚的话调用和普通函数一样this指针通过寄存器传递没有额外开销。const与constexpr纯编译期概念运行时零开销。constexpr甚至能把计算搬到编译期省Flash省时间。模板实例化后就是具体类型的具体代码和手写没区别。但要注意模板会导致代码膨胀同一模板实例化多次Flash会涨。内联函数比宏安全效果一样。命名空间纯编译期零开销。需要谨慎的是虚函数每个对象多一个vptr4字节每次调用多一次间接跳转。在F103上一次虚函数调用比普通调用多大概十几个时钟周期。控件数量少、触摸响应不是硬实时的话完全可以接受。异常默认开启会显著增大代码体积建议编译时加-fno-exceptions关掉。RTTI运行时类型识别基本用不上加-fno-rtti关掉。动态内存new/delete在单片机上要慎用碎片化是隐形杀手。我倾向于静态分配或者用内存池。提示在Makefile或CMake里加上-fno-exceptions -fno-rtti -fno-threadsafe-statics能省下不少Flash。我实测一个中等规模的C工程光关掉异常就能省4KB左右。2.3 哈佛结构对C的影响51和STM32的区别热词里有人问“51单片机哈佛结构”这里顺带说清楚。51单片机是典型的哈佛结构程序存储器和数据存储器分开编址代码从Flash取数据在RAM里。这对C的影响主要体现在两个方面一是函数指针和虚函数表存在Flash里取指和数据访问走不同总线理论上不会互相抢带宽但51的Flash访问速度慢虚函数调用在51上开销比STM32明显二是常量数据const变量在哈佛结构下可能被放到代码段取的时候走程序存储器这点在写C时要留意。STM32F103虽然也是哈佛架构的变体指令和数据总线分离但它是32位流水线Flash有预取缓冲虚函数那点间接跳转的开销基本被流水线吃掉了。所以我的建议是51上玩C要克制STM32上可以放开一些。51的RAM通常只有128到512字节连一个带虚函数的对象都放不下几个更别说STL了。STM32F103C8T6有20KB RAM放几十个控件对象绰绰有余。3. 核心细节解析类封装、const/final/static在单片机里的实战用法3.1 用类封装触摸控件从结构体到对象先看纯C的结构体写法这是大多数人起步的方式typedef struct { uint16_t x, y, w, h; uint8_t state; void (*on_touch)(void); } Button; Button btn1 {10, 10, 80, 40, 0, btn1_handler};这个写法没问题但on_touch是函数指针每个按钮要单独写一个handler或者用参数区分。按钮多了之后handler里还得判断是哪个按钮触发的又绕回去了。C的类写法class Widget { public: Widget(uint16_t x, uint16_t y, uint16_t w, uint16_t h) : x_(x), y_(y), w_(w), h_(h), state_(0) {} virtual ~Widget() default; bool contains(uint16_t px, uint16_t py) const { return px x_ px x_ w_ py y_ py y_ h_; } virtual void onTouch(uint16_t px, uint16_t py) 0; virtual void draw() const 0; protected: uint16_t x_, y_, w_, h_; uint8_t state_; }; class Button : public Widget { public: Button(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const char* label) : Widget(x, y, w, h), label_(label) {} void onTouch(uint16_t px, uint16_t py) override { state_ 1; // 执行按钮动作 } void draw() const override { // 绘制按钮 } private: const char* label_; };这里有几个关键点值得展开。contains()是const成员函数因为它不修改对象状态编译器能据此做优化也向读代码的人表明意图。onTouch()和draw()是纯虚函数强制每个具体控件实现自己的逻辑。override关键字是C11引入的让编译器帮你检查是不是真的覆盖了基类虚函数写错函数签名会直接报错这个在嵌入式里特别有用因为嵌入式调试成本高编译期能抓的错就别留到运行期。3.2 const的三层含义从编译期检查到Flash优化const在单片机C里有三层用法很多人只用了第一层。第一层是接口约束。像上面的contains()加const表示“我承诺不修改对象”。调用方看到这个签名就知道可以放心传const对象进来。这在团队协作里能省很多扯皮。第二层是编译期常量。constexpr比const更强它要求值在编译期就能算出来。比如屏幕尺寸constexpr uint16_t SCREEN_WIDTH 320; constexpr uint16_t SCREEN_HEIGHT 240;这两个值会被直接内联到代码里不占RAM。如果你写const uint16_t在某些编译器上可能会分配RAM虽然优化开了之后通常也会被优化掉但constexpr是明确的保证。第三层是Flash优化。在哈佛结构的单片机上const数据如果被放到Flash里取的时候走程序存储器总线不占宝贵的RAM。但这里有个坑默认情况下C的const全局变量可能被放到RAM里初始化因为C允许运行期初始化。要确保放到Flash得用constexpr或者加编译器属性。我在STM32上实测用constexpr定义的查找表确实进了Flash用const定义的有时会占RAM具体取决于编译器和优化等级。注意如果你有一张大查找表比如正弦表、字库一定要用constexpr或者static const配合编译器属性否则20KB RAM很快就被吃光。我踩过这个坑一张256项的uint16_t表就占了512字节RAM后来改成constexpr才挪到Flash。3.3 final和static控制继承与生命周期final关键字有两个用法修饰类表示“这个类不能被继承”修饰虚函数表示“这个函数不能被覆盖”。在单片机项目里final的主要价值是给编译器优化机会。如果一个虚函数被标记为final编译器知道不会有更进一步的覆盖可以做去虚化优化把间接调用变成直接调用。这在性能敏感的触摸响应路径上很有用。class Slider final : public Widget { // 这个类不会被继承 };static在类里有两种用法。静态成员变量属于类而不属于对象所有对象共享一份。比如控件总数统计class Widget { public: Widget(...) { count_; } static uint16_t getCount() { return count_; } private: static uint16_t count_; }; uint16_t Widget::count_ 0;这个count_只有一份不占每个对象的空间。静态成员函数没有this指针不能访问非静态成员通常用来做工具函数或者工厂方法。在单片机里static还有一个重要用途是控制生命周期。函数内的static变量在程序运行期间一直存在但只在第一次调用时初始化。C11保证这种初始化是线程安全的但在单片机上通常是单线程加-fno-threadsafe-statics可以省掉那套保护代码。4. 实操过程触摸坐标到屏幕内容的完整映射实现4.1 触摸坐标采集与校准从ADC原始值到屏幕像素电阻触摸屏的原理是分压。触摸时上下两层导电层接触X和Y方向分别产生电压单片机用ADC读这个电压得到原始值。但原始值和屏幕像素坐标不是线性对应的因为屏幕有边框、触摸层有电阻差异必须校准。校准的经典方法是四点校准在屏幕四个角附近显示四个点让用户依次点击记录ADC原始值然后解一个线性方程组得到变换系数。具体来说假设屏幕坐标是(sx, sy)ADC原始值是(ax, ay)关系是sx a * ax b * ay c sy d * ax e * ay f六个未知数四个点提供八个方程用最小二乘法解。实际实现时为了省计算通常简化为sx (ax - ax_min) * SCREEN_WIDTH / (ax_max - ax_min) sy (ay - ay_min) * SCREEN_HEIGHT / (ay_max - ay_min)这个简化版假设X和Y方向独立忽略交叉项。对于大多数电阻屏够用了。我在项目里用的是简化版实测误差在3个像素以内按钮点击完全没问题。采集部分用STM32的ADC配置成规则通道扫描用DMA搬运。触摸中断触发后启动ADCDMA把X和Y的原始值搬到内存然后在中断回调里做坐标变换。这里用C的话可以把校准参数和变换逻辑封装成一个TouchCalibration类class TouchCalibration { public: struct Point { uint16_t raw_x, raw_y; }; void setCorners(Point tl, Point tr, Point bl, Point br) { ax_min_ tl.raw_x; ax_max_ tr.raw_x; ay_min_ tl.raw_y; ay_max_ bl.raw_y; } void transform(uint16_t raw_x, uint16_t raw_y, uint16_t sx, uint16_t sy) const { sx static_castuint32_t(raw_x - ax_min_) * SCREEN_WIDTH / (ax_max_ - ax_min_); sy static_castuint32_t(raw_y - ay_min_) * SCREEN_HEIGHT / (ay_max_ - ay_min_); } private: uint16_t ax_min_, ax_max_, ay_min_, ay_max_; };注意这里用了static_castuint32_t做中间类型防止raw_x - ax_min_乘以屏幕宽度时溢出。uint16_t最大65535乘以320就超了必须先转成32位。这个坑我踩过坐标算出来全是乱的查了半天才发现是溢出。4.2 控件树的遍历与命中检测多态的实际开销有了屏幕坐标接下来要判断落在哪个控件上。我的做法是维护一个控件数组触摸发生时从后往前遍历后添加的控件在上层调用每个控件的contains()第一个返回true的就处理。Widget* widgets[MAX_WIDGETS]; uint8_t widget_count 0; void handleTouch(uint16_t sx, uint16_t sy) { for (int i widget_count - 1; i 0; --i) { if (widgets[i]-contains(sx, sy)) { widgets[i]-onTouch(sx, sy); return; } } }这里的widgets[i]-contains()是虚函数调用每次遍历都要查虚表。假设有10个控件最坏情况要查10次虚表。在72MHz的F103上一次虚表查找加调用大概几十个时钟周期10次也就几百个周期相对于触摸响应的人体感知延迟几十毫秒完全可以忽略。所以在触摸这种非硬实时场景多态的开销不用纠结。但如果你要做的是电机控制、PWM调制这种硬实时任务虚函数就要慎用。我的原则是人机交互层随便用控制层尽量用模板或静态多态。4.3 界面刷新与状态同步观察者模式的轻量实现控件状态变化后要刷新屏幕。最简单的做法是每次触摸后全屏重绘但ILI9341全屏刷新要几十毫秒闪烁明显。更好的做法是只重绘变化的控件。这里可以用一个轻量的观察者模式控件状态变化时标记自己为“脏”主循环检查脏标记只重绘脏控件。class Widget { public: void invalidate() { dirty_ true; } bool isDirty() const { return dirty_; } void clearDirty() { dirty_ false; } virtual void draw() const 0; private: bool dirty_ true; }; void refreshLoop() { for (int i 0; i widget_count; i) { if (widgets[i]-isDirty()) { widgets[i]-draw(); widgets[i]-clearDirty(); } } }这个模式的好处是解耦控件不需要知道谁在刷新它主循环也不需要知道控件内部怎么变。加新控件只要实现draw()就行。dirty_标志用bool一个字节10个控件也就10字节RAM开销可以忽略。实操心得draw()里尽量用局部刷新只重绘控件自己的矩形区域。ILI9341支持设置窗口只往那个窗口写像素。全屏320x240是76800个像素局部刷新一个80x40的按钮只有3200个像素速度快20多倍。5. 常见问题与排查技巧实录5.1 单片机C语言没有堆栈吗为什么会有这个疑问热词里“单片机c语言没有堆栈吗为什么”这个问题其实反映了一个常见误解。单片机有堆栈但和PC上的概念不完全一样。51单片机的堆栈在内部RAM里由SP寄存器指向空间很小通常几十字节而且堆栈向下增长还是向上增长取决于具体型号。STM32的堆栈在RAM里由MSP主堆栈指针和PSP进程堆栈指针管理空间在启动文件里定义通常几KB。那为什么有人觉得“没有堆栈”可能是因为单片机C语言里很少用递归函数调用层次浅堆栈用得少存在感低。另一个原因是中断会占用堆栈如果中断嵌套深堆栈可能溢出但溢出后行为不可预测不像PC上会报段错误。我在项目里遇到过堆栈溢出导致变量被莫名改写的问题查了很久才发现是中断里调用了太深的函数。C对堆栈的影响主要是对象构造和析构。局部对象在栈上构造离开作用域析构。如果对象很大比如包含大数组栈可能不够。我的建议是大对象用静态分配或者放在全局区小对象才放栈上。另外虚函数调用本身不额外占栈但虚析构函数会。5.2 冒泡排序在单片机里的写法与优化热词里“冒泡排序算法c”出现频率很高这里给一个单片机友好的版本。冒泡排序在单片机里主要用于小数组排序比如把ADC采样值排序后取中值滤波。templatetypename T, uint8_t N void bubbleSort(T (arr)[N]) { for (uint8_t i 0; i N - 1; i) { bool swapped false; for (uint8_t j 0; j N - 1 - i; j) { if (arr[j] arr[j 1]) { T tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) break; } }用模板的好处是数组大小在编译期确定循环边界是常量编译器能展开优化。swapped标志让已经有序的数组提前退出最好情况O(n)。在F103上排16个uint16_t大概几十微秒做中值滤波完全够用。注意模板参数N是uint8_t如果数组超过255个元素会溢出。单片机里一般不会排这么大的数组但心里要有数。5.3 常见问题速查表问题现象可能原因排查方法解决方案触摸坐标跳变ADC参考电压不稳用万用表测VREF加滤波电容软件做中值滤波坐标算出来全是0整数溢出检查中间变量类型用uint32_t做中间计算虚函数调用后跑飞对象被析构或未初始化检查对象生命周期确保对象在调用期间有效Flash不够用模板实例化过多看map文件合并模板参数减少实例化RAM不够用const数据放到了RAM看map文件改用constexpr中断里调用虚函数异常中断上下文问题检查中断优先级中断里只做标记主循环处理屏幕刷新闪烁全屏重绘观察刷新区域改局部刷新按钮响应错位校准参数错误重新校准检查四点校准顺序5.4 独家避坑技巧第一个坑是虚析构函数。如果你的类会被通过基类指针删除基类必须有虚析构函数。但在单片机上我基本不用delete对象都是静态分配或者放在内存池里所以虚析构函数不是必须的。但如果你用了记得它会增加虚表大小。第二个坑是静态初始化顺序。C全局对象的构造函数在main()之前调用但不同编译单元的初始化顺序不确定。如果全局对象A的构造函数用了全局对象B而B还没构造就会出问题。单片机上我建议避免全局对象用函数内静态对象或者显式初始化函数。第三个坑是模板代码膨胀。同一个模板用不同参数实例化多次每次都会生成一份代码。比如bubbleSortuint16_t, 16和bubbleSortuint16_t, 32是两份代码。如果Flash紧张可以考虑用运行期参数代替模板参数牺牲一点性能换空间。第四个坑是中断里的C。中断服务函数里尽量别用C的高级特性虚函数、异常、动态内存都不安全。我的做法是中断里只做最少的硬件操作和标记具体处理放到主循环。这样既安全又能用C的抽象。6. 工具链与调试环境VSCode配置与常见问题6.1 VSCode配置C/C环境的关键步骤热词里“vscode配置c/c环境”和“vscode c配置”出现很多次这里给一个针对STM32嵌入式开发的配置方案。核心是三个文件c_cpp_properties.json、tasks.json、launch.json。c_cpp_properties.json里配置头文件路径和编译器路径{ configurations: [ { name: STM32, includePath: [ ${workspaceFolder}/**, ${workspaceFolder}/Drivers/CMSIS/Include, ${workspaceFolder}/Drivers/STM32F1xx_HAL_Driver/Inc ], defines: [USE_HAL_DRIVER, STM32F103xB], compilerPath: arm-none-eabi-gcc, cStandard: c11, cppStandard: c17, intelliSenseMode: gcc-arm } ], version: 4 }tasks.json里配置编译任务调用make{ version: 2.0.0, tasks: [ { label: build, type: shell, command: make, group: {kind: build, isDefault: true}, problemMatcher: [$gcc] } ] }launch.json里配置调试用OpenOCDGDB{ version: 0.2.0, configurations: [ { name: Debug STM32, type: cppdbg, request: launch, program: ${workspaceFolder}/build/${workspaceFolderBasename}.elf, miDebuggerPath: arm-none-eabi-gdb, miDebuggerServerAddress: localhost:3333, serverStarted: Open On-Chip Debugger, setupCommands: [ {text: target extended-remote localhost:3333}, {text: monitor reset halt}, {text: load}, {text: monitor reset init} ] } ] }这套配置我用了两年多稳定可靠。关键是compilerPath要指向正确的arm-none-eabi-gccintelliSenseMode选gcc-arm这样代码补全和跳转才准。6.2 编译选项与优化等级的选择C在单片机上编译优化等级很关键。-O0调试方便但代码大、速度慢-O2或-Os适合发布。我的做法是调试用-OgGCC的调试优化等级兼顾调试和优化发布用-Os优化体积。必须加的选项CXXFLAGS -fno-exceptions -fno-rtti -fno-threadsafe-statics CXXFLAGS -fno-use-cxa-atexit -fno-unwind-tables-fno-use-cxa-atexit是关掉全局对象析构的注册单片机上程序不会正常退出不需要这个。-fno-unwind-tables是关掉异常展开表进一步省空间。链接选项里加-specsnano.specs用newlib-nano-specsnosys.specs关掉系统调用。这两个能显著减小代码体积。6.3 调试C代码的特殊技巧调试C比调试C麻烦一点主要是虚函数和模板的符号名很长。GDB里可以用info vtbl查看虚表用p *obj查看对象内容。如果对象有虚函数p *obj会显示vptr可以据此判断对象类型。另一个技巧是用GDB的set print demangle on让C符号名可读。默认是开的但有时候被关了。如果程序跑飞先看HardFault。在HardFault_Handler里加断点看LR和PC寄存器能定位到出错地址。如果是虚函数调用出错通常是对象指针为空或者对象被破坏。我遇到过一次是数组越界把相邻对象的vptr覆盖了查了很久。7. 从C到C的迁移策略与性能实测7.1 渐进式迁移先封装再继承如果你有一个现成的C项目想迁移到C别想着一次重写。我的策略是渐进式先把相关的函数和数据结构用类包起来保持接口不变编译通过后再逐步引入继承和多态。第一步把Button结构体和它的操作函数包成一个类成员函数就是原来的函数只是把Button*参数变成this。这一步几乎零风险编译过了行为就不变。第二步把多个类似的控件抽象出基类用虚函数统一接口。这一步要小心虚函数调用比函数指针慢一点但通常可接受。第三步用模板替换一些宏和重复代码。比如原来用宏定义的数组大小、用宏写的循环改成模板。这个策略的好处是每一步都可测试、可回退。我在项目里用这个方法把一个三千行的C项目迁到C全程没有大停机。7.2 性能实测虚函数、模板、constexpr的实际开销我在STM32F103C8T6上做了几组实测数据如下测试项纯C实现C实现差异10个控件遍历命中检测约1200周期约1500周期25%冒泡排序16个uint16_t约800周期约800周期0%坐标变换含溢出保护约200周期约200周期0%代码体积完整项目约28KB约32KB14%RAM占用约6KB约6.5KB8%虚函数带来的25%开销在触摸场景完全可接受因为触摸响应预算是几十毫秒1500周期在72MHz下只有20微秒。代码体积增加14%主要是虚表和模板实例化64KB Flash够用。RAM增加8%主要是vptr每个对象4字节10个对象40字节。实操心得如果你的Flash特别紧张比如用STM32F030只有16KBC要克制。如果Flash有64KB以上放心用。RAM同理20KB以上随便用8KB以下要精打细算。7.3 什么时候不该用C说了这么多C的好话也得说说什么时候不该用。第一资源极度受限的平台比如51单片机、PIC10系列RAM只有几十字节C的vptr和虚表都放不下老老实实用C。第二硬实时控制回路比如电机FOC控制每个周期只有几微秒虚函数的间接跳转和不确定性可能影响控制质量用C或者C的模板静态多态。第三团队全是C背景且没有学习意愿强行上C会导致代码风格混乱不如统一用C。我的判断标准很简单如果项目代码量预计超过2000行且需求会变化就值得上C否则C就够了。触摸屏交互、菜单系统、协议栈这些场景C的优势明显。点灯、读传感器、简单控制C更直接。8. 后续扩展方向与个人经验这个触摸屏项目做完之后我又在几个项目里复用了这套C框架。一个是用ESP32做的带ADC采集的控制器把控件类扩展成了支持滑块的版本另一个是用STC单片机做的简单菜单系统因为STC的RAM也不大我把虚函数去掉了改用函数指针加模板效果也不错。如果要把这套东西继续扩展我觉得有几个方向值得做。一是引入轻量级的事件系统用std::function的替代品比如自己实现的函数包装器来解耦控件和业务逻辑。二是把控件描述做成数据驱动用一张表定义界面布局代码只负责解释这张表这样改界面不用改代码。三是加入简单的动画效果用定时器驱动控件的过渡状态提升交互体验。最后分享一个我在实际项目中体会最深的点C在单片机上的价值不在于语言本身而在于它强迫你思考代码的组织方式。用C的时候我经常是想到哪写到哪全局变量满天飞。用C之后每个东西该放在哪个类里、接口怎么设计、生命周期怎么管理这些问题在写代码之前就得想清楚。这种思考习惯的改变比语言特性带来的性能差异重要得多。如果你也在单片机上折腾C我的建议是从小项目开始先把一个模块用类封装好跑通了再推广。别一上来就搞大框架容易翻车。遇到编译不过或者跑飞的情况先检查虚函数和对象生命周期这两个是C在单片机上的主要坑点。工具链方面VSCode加arm-none-eabi-gcc加OpenOCD这套组合我用下来最顺手配置一次能用很久。
网站建设高端定制企业官网