ARTICLE DETAIL

资讯详情

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

深度解析CMSIS-5:嵌入式开发的标准骨架与工程实践

深度解析CMSIS-5:嵌入式开发的标准骨架与工程实践 直接上结论CMSIS-5绝对不是“装了就完事”的代码库它是一套让你在ARM Cortex-M上把代码写出“工程感”的底层骨架。对于刚接触嵌入式的新手它像一份“硬件抽象层标准答案”对于已经在做产品的中级工程师它更像一套帮你治理老项目、统一外设驱动写法、把DSP算法从“能用”优化到“好用”的参考范式。这篇文章我打算用源码阅读的视角配合几个实际用过的工程案例把CMSIS-5的架构分层、模块职责、工程接入方式以及最重要的——你在项目里到底该不该用它、怎么选型一次性讲清楚。1. CMSIS-5不是“一个库”是一整套平台标准很多刚入门的朋友会把CMSIS误认为“ST官方库”或者“某个外设驱动包”。其实这么理解也不完全错但视野太窄了。CMSIS全称是Cortex Microcontroller Software Interface Standard是ARM官方为Cortex-M系列处理器定义的一套软件接口标准。CMSIS-5在CMSIS的历史版本中属于比较成熟的迭代它从“内核寄存器定义”这种最底层的东西一路管到了DSP算法库、神经网络推理函数、RTOS内核封装、甚至系统启动文件的标准写法。它的定位我习惯用一个比喻来解释CMSIS-5就像一套“精装修房的施工规范”。开发商ARM不直接帮你装修不直接实现业务功能但它规定了水电怎么走、墙面怎么做、插座高度是多少。你按这个规范施工跟任何施工队编译器、调试器、RTOS、第三方库对接都不会出大问题。如果你不按规范来自己拉一条飞线过去短期也能跑长期维护和换人接手时麻烦就大了。从源码评测的角度看CMSIS-5的核心特点可以归纳为三个维度标准骨架定义Cortex-M全系内核的系统寄存器地址、位段含义、中断控制接口。这部分在CMSIS/Core/Include目录下核心文件是core_cm0plus.h、core_cm3.h、core_cm4.h、core_cm7.h、core_cm33.h等。内核直通提供访问NVIC、SysTick、MPU、FPU、Cache、TrustZone等底层硬件的C语言API。不用写一行汇编就能操作特权级功能。生态粘合把编译器差异ARMCC、GCC、IAR、调试器接口ITM、DAP、RTOS调度接口CMSIS-RTOS2统一成一套标准。做到“一次编写多编译器/多调试器/多RTOS兼容”。我特别想强调第三点。在实际项目里如果你用GCC编译器和Keil MDK分别去编译同一个基于CMSIS-5的工程你会发现几乎不用改代码——顶多启动文件的汇编语法有细微差别。这就是CMSIS-5存在的最大价值它不是帮你写业务代码的库而是让“换工具链”这个动作从“伤筋动骨”变成“改个启动文件”的缓冲层。1.1 源码目录全景每一个文件夹都不是白给的我们先拉一下CMSIS-5的仓库结构。我以5.9.0版本为例这也是目前使用最广的稳定版本之一。核心目录如下CMSIS/ ├── Core/ # Cortex-M内核抽象层必须使用 │ ├── Include/ # 内核寄存器定义、内联函数、系统初始化函数声明 │ ├── Source/ # 系统时钟初始化模板、GCC/ARMCC/IAR启动文件模板 ├── DAP/ # Debug Access Port调试探针固件一般开发用不到 ├── Driver/ # 统一外设驱动API如CAN、ETH、Flash、GPIO等 ├── DSP/ # DSP库包含基础数学、矩阵运算、滤波器、变换 ├── NN/ # 神经网络推理函数基于CMSIS-DSP优化的卷积/池化/全连接 ├── RTOS/ # CMSIS-RTOS v1/v2 API定义适配FreeRTOS、RTX5等 ├── SVD/ # System View Description调试器外设寄存器描述文件 ├── Utilities/ # 一些辅助工具脚本和配置头文件模板在真实工程中你会接触最多的其实是Core、DSP和RTOS这三块。Driver目录往往被厂商二次封装后以独立包的方式提供比如Keil的MDK中间件而SVD更多是调试器比如Keil、IAR、PyOCD在图形化查看外设寄存器时用的描述文件并不参与编译。1.2 为什么说它解决了“嵌入式工程治理”的头号痛点做嵌入式最头疼的事互相独立却又缠在一起内核寄存器操作方式不统一、外设驱动各有各的写法、不同工程师写的代码风格天差地别、换个编译工具链就要修一堆头文件路径。CMSIS-5从标准层面解决了前两点内核访问全部通过core_cmX.h提供的接口完成外设寄存器地址规范在device.h头文件里启动文件模板由官方维护。剩下的工程规范问题CMSIS也通过文件分层和命名约束给出了一套隐形的“最佳实践”。我在老项目里见过最混乱的写法有人直接在业务代码里操作*(volatile unsigned int *)0xE000ED00有人用NVIC_EnableIRQ()还有人自己封装了一个nvic_init()。这三个方案都能让中断跑起来但出了问题追踪起来就一言难尽了。CMSIS-5提供的标准API本质上是给“能不能直接操作寄存器”这个问题一个官方答案能但不建议。你用标准接口出错概率和排查成本都更低。2. 模块分层从Core到NN每一层都有明确的职责边界CMSIS-5的模块设计非常讲究“依赖单向”。越底层的模块被越上层的模块依赖但底层模块绝不反向依赖上层模块。理解这种分层思维你在组织自己项目的源码目录时也会受益。2.1 CMSIS-Core一切嵌入式软件的“地基”CMSIS-Core是整个CMSIS-5中最核心、也最绕不开的部分。它做的事情可以拆成四个层次寄存器定义层通过core_cm4.h等头文件将Cortex-M内核的寄存器SCB、NVIC、SysTick、MPU、FPU等映射为C语言结构体。这样你用NVIC-ISER[0] | (1UL pos)就能操作中断使能跟直接写汇编等价但可读性和可维护性大幅提升。内联函数层以__STATIC_INLINE即static inline方式提供大量API。比如__enable_irq()、__disable_irq()、__DMB()、__WFI()、__REV()等。这些函数往往会被优化为单条或几条汇编指令没有额外调用开销。系统初始化层规范了SystemInit()函数和SystemCoreClock全局变量。开发板BSP包在启动文件跳转到main之前会调用SystemInit()完成时钟树初始化SystemCoreClock变量则记录当前的系统时钟频率供SysTick、延时函数和串口波特率计算使用。中断处理规范层定义了中断服务函数IRQ Handler的统一命名规范。比如UART0_IRQHandler、SysTick_Handler每个外设中断向量对应的处理函数名都写在启动文件里。CMSIS本身不实现这些函数但规范了“你必须在工程里定义这个符号”。我遇到不少中级工程师有一个误区以为CMSIS-Core只有在使用官方SDK时才需要。实际上哪怕你用的是完全自主开发的BSP只要你的芯片是Cortex-M内核core_cmX.h就应该出现在你的工程里——自己重复造一套寄存器结构体定义除了增加维护成本没有任何收益。2.2 CMSIS-DSP让MCU也能跑正经的数学运算CMSIS-DSP是CMSIS-5中体积最大、算法最密集的模块之一。它提供了超过60个函数的DSP库覆盖以下几个大类基础数学函数加法、减法、乘法、点积、绝对值、偏移、缩放、负数等。针对q7_t、q15_t、q31_t、f32_t四种数据类型做了重载。快速数学函数正余弦、平方根、反正切等全部基于查找表和多项式逼近实现比math.h里的同名函数快一个数量级。矩阵运算矩阵加法、乘法、转置、求逆同样覆盖多种数据类型。实测在Cortex-M4上做4x4浮点矩阵求逆比手写版本快2~3倍。变换函数主要包含FFT快速傅里叶变换和DCT离散余弦变换。支持实数FFT、复数FFT、浮点/定点各种变体。滤波器函数FIR有限脉冲响应、IIR无限脉冲响应、FIR格型、IIR格型、Biquad级联等。这是我在电机控制项目里最常用的模块。插值函数线性插值、双线性插值、三次样条插值主要用于传感器校准曲线拟合。统计函数均值、方差、标准差、均方根、最大值最小值查找等。最开始接触CMSIS-DSP时我犯过一个很典型的错误直接用arm_sin_f32()做大量三角函数运算发觉确实比标准sinf()快但没注意它需要调用初始化函数arm_sin_cos_f32_init()预先分配查找表。后来查阅源码才发现CMSIS-DSP的正余弦是基于float32_t的查表加插值实现的查找表的精度直接影响输出精度。这类“看起来一样实际要先初始化”的API在CMSIS-DSP里非常多使用前建议把头文件里的函数注释通读一遍。2.3 CMSIS-NN嵌入式设备上的轻量级神经网络推理CMSIS-NN是CMSIS-5里比较“年轻”的模块定位是在Cortex-M系列处理器上跑卷积神经网络CNN的推理计算。它不是像TensorFlow Lite Micro那样完整的推理框架而是提供了一系列高效的原语卷积、深度可分离卷积、全连接、池化、softmax、激活函数等。从源码评测角度看CMSIS-NN的精髓在于充分利用了Cortex-M4/M7的SIMD单指令多数据和DSP指令以及M33的可选DSP扩展。比如arm_convolve_HWC_q7_RGB()函数内部会用SMLAD、SMLALD这类DSP指令同时处理多个乘加操作效率比纯C实现高出数倍。实际在Cortex-M7上跑MobileNet v1单次推理可以从纯C实现的十几秒缩短到1~2秒这个量级的提升已经让“MCU上跑轻量级视觉模型”真正落地成为可能。不过要泼一盆冷水CMSIS-NN不是“装上就能跑模型”的高级框架。大量函数假定输入数据已经是量化后的q7_t8位定点或q15_t16位定点格式也就是说你模型里的权重和激活值必须经过量化还要自行管理内存地址对齐。我自己试过把TensorFlow训练的模型转成CMSIS-NN可用的C数组最麻烦的不是推理函数而是权重重排和内存布局。这一点后文选型部分我会再展开。2.4 CMSIS-RTOS2给RTOS移植打了个“通用接口补丁”如果CMSIS-Core解决的是“我该在哪个头文件里找寄存器定义”CMSIS-RTOS2解决的就是“我换了RTOS业务代码能不能少改一点”。它定义了线程创建、消息队列、信号量、互斥量、事件标志、内存池的通用API底层可以接FreeRTOS、RTX5、ThreadX等任意RTOS。我实际用CMSIS-RTOS2接FreeRTOS的项目里最直观的感受是业务层代码完全不用关心你用的是哪个RTOS。你只需要osThreadNew()创建线程osMessageQueuePut()发消息换RTOS时只动底层适配文件业务代码一行不改。这种“接口隔离”在团队协作时特别有用——A工程师负责业务逻辑B工程师负责RTOS移植两人之间只需要约定CMSIS-RTOS2 API的用法不需要互相深入对方的代码。当然它的代价是API多了一层封装理论上会比直接调用FreeRTOS原生API多一点点开销。但固定开销通常在几个us以内对于绝大多数业务都无感知。如果你做的是硬实时控制每个微秒都寸土必争那你可以直连RTOS原生API。但当项目规模上来了我依然推荐先CMSIS-RTOS2在性能瓶颈处再局部绕过。3. 工程治理CMSIS-5如何帮你把嵌入式代码“管”起来代码能跑只是起点代码能被团队稳定维护才是工程。CMSIS-5在“治理”层面的贡献我认为是它最容易被低估的价值。3.1 启动文件与链接脚本官方模板降低迁移成本每个使用CMSIS-5的工程都会从CMSIS/Core/Source目录下找到对应内核的启动文件模板。以GCC为例gcc_arm_ cortex-m4.c模板定义了中断向量表、堆栈初始化、Reset_Handler、SystemInit调用流程。你只需把这个模板复制到工程里再根据具体芯片修改中断向量表中外设中断的名称即可。链接脚本.ld文件CMSIS本身不直接提供但绝大多数芯片厂商的SDK包里会附带。注意一点如果你从零开始建一个CMake工程链接脚本的堆栈大小、堆大小、内存区域划分Flash/RAM地址范围是需要根据芯片手册填写的。CMSIS不背这个锅但CMSIS的启动文件假设你已经正确配置了堆栈指针和向量表位置这两个不一致会导致上电后“神秘死机”。实际踩坑经历之前把一个LM3SCortex-M3老项目的代码换到GCC工具链编译直接用Keil的ARMCC启动文件改后缀硬编结果编译通过上电就是进不了main。后来老老实实换用CMSIS提供的GCC启动文件模板问题当场消失。原因就是ARMCC和GCC的汇编器对段SECTION名的处理方式不同向量表对应的段名不一致导致链接时向量表没被放到起始地址。这个坑只要是跨工具链迁移的人几乎都会遇到CMSIS官方模板是少走弯路的最优解。3.2 编译宏管理与代码分层工程治理里一个很实际的痛点一个项目要支持多个芯片型号或者同一芯片要启用/禁用不同外设功能代码怎么写才不乱CMSIS-5给出的标准方案是在编译器全局宏里定义芯片型号宏例如STM32F407VG。在device.h头文件里通过#if defined(STM32F407VG)分支来包含正确的外设寄存器定义头文件。CMSIS-Core头文件通过__CORTEX_M如__CORTEX_M4和__FPU_USED等宏来决定编译哪段内核逻辑。这种“全局宏条件编译分层包含”的模式是CMSIS体系最核心的工程治理智慧。你在自己写BSP时也应该沿用底层寄存器映射放在device.h外设驱动按模块拆分业务代码只依赖驱动接口不依赖具体寄存器。从维护角度我强烈建议在CMakeLists.txt或Keil工程里把宏定义集中管理不要分散在源码文件的#define里。CMSIS-5的官方示例工程里编译选项和宏定义一般都在工程配置面板统一处理这个习惯值得所有嵌入式项目学习。3.3 代码风格与命名规范无意识中养成的“标准味”阅读CMSIS-5源码时你会发现它的命名风格极其统一函数前缀arm_、NVIC_、SysTick_数据类型后缀_t常量宏全大写加下划线。更重要的是每个函数头都有一段完整的注释包含函数功能描述、参数说明、返回值说明、参考函数、注意事项。这种风格表面上看是“代码洁癖”实际上对工程治理很有价值。有一次我在维护团队项目时外设驱动被三个不同风格的工程师改过一个用匈牙利命名法一个用下划线风格一个直接用拼音缩写。结果过了几个月连写代码的人自己也忘了某个变量是什么意思。CMSIS-5从底层的命名规范出发让所有基于它构建的上层代码都会不自觉地“对齐”这套风格因为你要跟它交互就必须适配它的数据结构和函数签名。所以如果你问我“新项目要不要引入CMSIS-5”我其实更想问的是“你希望你的项目从头建立起一种可延续、可协作的代码风格吗”如果答案是肯定的那CMSIS-5不只是工具还是一种工程约束。4. 选型落地什么时候该用CMSIS-5什么时候该绕开CMSIS-5不是银弹它有明确的适用边界。选型判断要基于芯片平台、团队能力和项目需求做权衡。4.1 适合直接用CMSIS-5的场景你用的是Cortex-M0/M0/M3/M4/M7/M23/M33等ARM内核MCU这是最基础的条件。如果是RISC-V内核或者专用DSP芯片CMSIS-5天然不适用。芯片厂商的SDK已经基于CMSIS构建。目前ST、NXP、Nuvoton、GD、华大、极海等主流厂商的SDK底层清一色用CMSIS-Core做内核抽象。这种情况下你不用CMSIS反而是“逆流”会导致厂商的驱动库、例程、配置工具全都用不上。项目要用到CMSIS-DSP或CMSIS-NN。电机控制中的PIDFOC、音频处理中的FFT/IFFT、振动分析中的带通滤波、轻量级AI识别CMSIS-DSP/NN能帮你省掉大量算法移植和优化时间。项目里要跑RTOS且你期望产品代码能在不同RTOS之间迁移。通过CMSIS-RTOS2的抽象层业务逻辑和调度内核解耦后续换RTOS成本极低。团队有新工程师加入希望快速建立代码规范。CMSIS-5本身就是一个标准范例新人在阅读和编写过程中会潜移默化地吸收规范。4.2 建议谨慎或绕开CMSIS-5的场景非ARM内核芯片比如国产的一些RISC-V MCU如CH32V系列、GD32VF103。虽然有些厂商提供了CMSIS兼容层但这不是ARM官方支持路径直接用可能会遇到坑。资源极度受限的极简项目Flash小于16KB、RAM小于4KB的老旧8位/16位平台比如PIC、AVR、STM8CMSIS-5的抽象层和启动文件反而浪费有限的资源不如直接用寄存器操作。对时间/空间开销极其敏感且完全依赖寄存器级调优的硬实时控制。虽然CMSIS-5的内联函数几乎零开销但如果你计划把每一个内存字节、每一条指令周期都掌控在手里底层寄存器直接操作更适合你。项目和工具链完全封闭且不需要生态对接例如产品永远只用IAR自有库固定芯片团队成员也稳定不动。这时候CMSIS-5带来的“可移植性红利”你可能感受不到引入它反而增加一层学习成本。4.3 从零到一GCC CMake工程集成CMSIS-5实操这里给出一套我实测可用的最小工程集成方法使用GCC工具链和CMake构建系统。第一步获取源码。可以直接通过git拉取以下命令仅作示例版本号按需调整git clone --branch 5.9.0 https://github.com/ARM-software/CMSIS_5.git CMSIS_5也可以单独拷贝CMSIS目录下的Core/Include、Core/Source、DSP/Include、DSP/Source等子目录到你的项目目录里。第二步在CMakeLists.txt中定义CMSIS路径和源文件列表set(CMSIS_CORE_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/third_party/CMSIS/Core/Include) set(CMSIS_DSP_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/third_party/CMSIS/DSP/Include) set(CMSIS_CORE_SOURCE ${PROJECT_SOURCE_DIR}/third_party/CMSIS/Core/Source/system_stm32f4xx.c ) include_directories( ${CMSIS_CORE_INCLUDE_DIR} ${CMSIS_DSP_INCLUDE_DIR} ${PROJECT_SOURCE_DIR}/board/include ) add_executable(${PROJECT_NAME}.elf ${CMSIS_CORE_SOURCE} ${PROJECT_SOURCE_DIR}/board/startup/gcc/startup_stm32f407xx.s ${PROJECT_SOURCE_DIR}/board/Src/main.c ... ) target_link_libraries(${PROJECT_NAME}.elf ...)第三步处理好芯片型号宏。以STM32F407为例编译器全局宏需要定义STM32F407xx一般由启动文件或头文件条件编译使用。在CMake里target_compile_definitions(${PROJECT_NAME}.elf PRIVATE STM32F407xx USE_HAL_DRIVER )第四步在你的main.c里包含核心头文件并调用初始化#include main.h #include cmsis_os.h int main(void) { HAL_Init(); SystemClock_Config(); __enable_irq(); // 业务代码 while (1) { // 循环 } }这套流程跑通后后续加DSP库、RTOS组件就都是“往里塞文件和改CMakeLists”的事了。如果你用的是Keil MDK步骤只会更简单勾选CMSIS组件点几下界面即可。4.4 项目选型自查表我把关键决策点整理成一张表方便你对照判断判断维度建议使用CMSIS-5建议谨慎使用建议绕开芯片内核ARM Cortex-M系列Cortex-A/R、M0超小资源RISC-V、MIPS、8051SDK基础官方SDK基于CMSISSDK自带寄存器库但可共存老项目纯寄存器开发算法需求FFT/滤波器/矩阵/神经网络少量简单数学运算无复杂算法团队规模多人协作、代码维护周期长两三人小项目个人极简项目工具链跨IDE/编译器需求明确单一工具链固定使用工具链封闭5. 数据手册之外的实战心得CMSIS-5源码阅读路线图如果你决定深入学习CMSIS-5我建议按下面的顺序读源码效率会高很多读core_cm4.h或core_cm33.h根据你常用的内核选择重点看结构体定义、中断使能禁用API、__STATIC_INLINE函数。这个文件能让你彻底明白“以后操作内核寄存器应该用哪个函数”。读cmsis_gcc.h或cmsis_armcc.h看编译器适配层。你会发现__ASM、__INLINE、__ALIGNED这些关键字是怎么被“翻译”成不同编译器语法的。理解了这层你自己写跨编译器代码时就会有意识地用这些宏。读system_stm32f4xx.c或其他厂商同名文件看系统时钟初始化流程。SystemInit()里调用了SystemCoreClockUpdate()更新时钟变量这与你后面的延时函数、串口波特率设置直接相关。读arm_math.h中的函数注释和arm_math_types.h的数据类型定义。CMSIS-DSP所有接口都在头文件里声明注释里会写明输入输出范围、注意事项。另外arm_math.h顶部有大量条件编译宏比如ARM_MATH_CM4、ARM_MATH_CM7、ARM_MATH_DSP这些宏决定了库的优化级别。读一个具体的DSP函数源码比如arm_fir_f32.c。你会看到CMSIS-DSP是怎么通过指针别名、循环展开和SIMD指令把滤波器运算做到极致的。这个理解对你日后做算法移植非常有帮助。读cmsis_os2.h和其中一个RTOS的适配层。以FreeRTOS适配为例看osThreadNew是怎么映射到xTaskCreate的。这能让你理解“统一API背后是适配层的魔法”。在阅读过程中请随时打断自己多问几个“为什么这么做”。比如为什么NVIC_EnableIRQ用__STATIC_INLINE而不是普通函数为什么arm_fir_f32的数据缓冲区要求按4字节对齐我的经验是每个“为什么”背后都是一次对ARM架构更深入的理解。这种理解才是工程落地时真正支撑你解决问题的能力。6. 常见问题与排查技巧实录最后这部分我按实战中遇到的高频问题整理一份速查表。现象原因解决办法调用arm_*函数链接报错“undefined reference”CMSIS-DSP源文件未添加进工程或者宏定义顺序不对确认把DSP/Source目录加入编译路径在arm_math.h依赖的宏如ARM_MATH_CM4已定义且该宏必须定义在所有包含arm_math.h的源文件之前程序上电后卡在HardFault_Handler通常是指针错误或栈溢出也可能是启动文件向量表放错了位置先使用__disable_irq()缩小范围检查链接脚本中向量表是否放到了Flash起始地址调试时打开HardFault的寄存器窗口BFAR、MMFAR辅助定位使用CMSIS-RTOS2创建线程后线程不调度可能没有开启SysTick中断或者RTOS适配层没有正确初始化时间基准检查是否调用了osKernelInitialize()和osKernelStart()确认SysTick的SystemCoreClock值正确在FreeRTOS适配层中开启configTICK_RATE_HZ对应的中断优先级DSP库的FFT算出来的频率和预期相差很大FFT点数、采样率、窗函数选择不对或输入数据没有做定标处理确认FFT实例参数大小、正反变换、数据格式检查输入数据是否归一化用已知正弦波信号做自检编译器版本和CMSIS-5不兼容编译报语法错误某些古老的编译器不支持C99或__STATIC_INLINE的写法升级编译器到较新版本或者关闭“强制遵循C89”之类的兼容选项中断函数名与CMSIS期望名称不一致导致中断不触发启动文件里向量表的中断服务函数名称必须和实际定义完全一致打开启动文件里的向量表注释对照芯片参考手册逐项确认IRQ名称我再单独讲一个我踩过很多次的坑CMSIS-DSP库默认是高度依赖Cortex-M4/M7硬件FPU和DSP指令的。如果你的芯片是Cortex-M0或M3没有硬件浮点单元编译时就必须指定ARM_MATH_CM0PLUS或ARM_MATH_CM3这类宏并且使用q15_t或q31_t定点类型否则即使编译过了运行效率也会非常难看甚至因为FPU指令非对齐访问产生硬错误。另一个常见误区是内存对齐。CMSIS-DSP很多函数要求输入、输出、状态缓冲区都按32位4字节或64位对齐。如果你用普通数组定义而未指定__ALIGNED(4)或alignas(4)部分芯片上会出现性能骤降甚至HardFault。在GCC下推荐用__ALIGNED(4) static float32_t input[256];或更通用static float32_t input[256] __attribute__((aligned(4)));这类细节在官方头文件注释里其实都有提及但很多时候大家都急着跑功能忽略了这些看似不起眼的说明。等你真正在目标硬件上遇到问题时再回去翻注释往往答案就在第一行。我越来越觉得CMSIS-5不只是一个代码库更像是一面镜子你怎么对待它它就怎么反馈你的工程素养。老老实实按规范用你的嵌入式项目就拥有了一个坚实的基础急功近利瞎改乱用它也会在某个深夜里用一屏幕的HardFault日志来提醒你回头补课。如果你正准备开启一个基于ARM Cortex-M的新项目花点时间把CMSIS-5的源码结构和设计思想吃透这笔投入绝对稳赚不赔。
返回列表