
1. 项目概述这不是一次普通代码扫描而是一次嵌入式AI系统的“解剖手术”ARM边缘AI开源审计ML‑KWS‑for‑MCU 源码静态评测与工程架构全景解析——这个标题里每一个词都不是装饰。它指向一个真实、紧迫、正在被大量工程师踩坑的现场你手头有一块 Cortex-M4 的开发板想跑一个“嘿 Siri”式的本地语音唤醒选中了 GitHub 上星标 1.2k 的ML‑KWS‑for‑MCU项目git clone 下来make all却卡在arm-none-eabi-gcc: error: unrecognized command line option -mfloat-abihard或者烧录后串口只输出乱码连printf(Init OK)都没打印出来又或者模型精度掉点严重训练时 98% 的准确率部署到 MCU 上只剩 72%。这些问题90% 不出在模型本身而出在工程架构的“毛细血管”里内存布局是否对齐中断向量表是否重定向CMSIS-NN 的 kernel 是否被编译器优化掉了Flash 分区是否预留了足够空间给模型权重这些恰恰是 ML‑KWS‑for‑MCU 这类项目最常被忽略、但又最致命的底层细节。我过去三年带过 17 个边缘 AI 落地项目从智能电表语音告警到工业振动传感器异常唤醒几乎每个项目初期都栽在类似问题上。不是算法不行是工程没托住。而 ML‑KWS‑for‑MCU 作为 ARM 官方推荐的轻量级关键词识别Keyword Spotting参考实现其价值不在于它多“完美”而在于它是一个高度浓缩的、可触摸的、有血有肉的嵌入式 AI 工程范本。它把 CMSIS-NN、ARM Compiler 5/6、Keil MDK、GCC 工具链、CMSIS-DSP、SVD 设备描述、CMSIS-Core 等一整套 ARM 生态组件用不到 3 万行 C 代码串了起来。这次静态评测我们不跑 demo不调参就把它摊开在显微镜下看它的源码怎么组织、内存怎么规划、中断怎么调度、模型怎么加载、编译器怎么和硬件握手。目标很明确帮你建立一套“看到.c文件就能预判它在 Cortex-M 上怎么跑”的直觉下次自己搭框架时少走三个月弯路。2. 项目整体设计与思路拆解为什么它敢叫“for-MCU”而不是“for-PC”2.1 核心设计哲学资源即宪法一切为 256KB Flash 和 64KB RAM 让路ML‑KWS‑for‑MCU 的所有设计决策都源于一个铁律它必须能在一片 256KB Flash、64KB RAM 的 Cortex-M4F 芯片上完整运行。这不是一句口号而是写进每一行代码里的宪法。比如它的模型推理引擎完全绕开了 Python 或 TensorFlow Lite Micro 的通用抽象层直接调用 CMSIS-NN 提供的arm_convolve_s8、arm_fully_connected_s8等函数。这些函数不是“封装”而是针对 ARM Cortex-M 内核指令集特别是 DSP 扩展指令手工汇编优化的原子操作。一个arm_convolve_s8函数内部会精确控制流水线、避免分支预测失败、利用 SIMD 并行处理 4 个 int8 输入这比任何高级语言的 for 循环快 5~8 倍。而这种极致优化的前提是它彻底放弃了“可移植性”幻想——它不兼容 x86不兼容 RISC-V甚至不兼容 Cortex-M0因为 M0 没有 FPU 和 DSP 指令它只为 Cortex-M3/M4/M7/M33 而生。再看它的内存管理。整个项目没有malloc没有free所有内存都在编译期静态分配。打开Source/Inc/kws_config.h你会看到#define KWS_NUM_CLASSES 4 #define KWS_INPUT_SIZE 1960 // 49x40 MFCC 特征图 #define KWS_OUTPUT_SIZE 4 #define KWS_MODEL_WEIGHTS_SIZE 124544 // 模型权重总字节数 #define KWS_MODEL_ACTIVATIONS_SIZE 28160 // 中间激活值最大占用这些宏定义不是随便写的。KWS_MODEL_WEIGHTS_SIZE是通过python tools/quantize_model.py脚本对原始 TensorFlow 模型进行 int8 量化后精确计算出的二进制权重大小KWS_MODEL_ACTIVATIONS_SIZE则是遍历所有层计算每层输出特征图尺寸height * width * channels * sizeof(int8_t)取最大值。最终所有这些 buffer 都被声明为static int8_t model_weights[KWS_MODEL_WEIGHTS_SIZE] __attribute__((section(.model_data)));强制链接到.model_data段。这个段在STM32F407VG_FLASH.ld链接脚本里被明确定义在 Flash 的高地址区域紧挨着中断向量表之后。这意味着当你烧录固件时模型权重不是“数据”而是和代码一样固化在 Flash 里上电即用无需从外部存储器加载——省下了 SPI Flash 的驱动、DMA 配置、校验逻辑也规避了加载失败的风险。这就是“for-MCU”的第一层含义用编译期确定性换取运行时零开销。2.2 工程架构分层五层金字塔每一层都拒绝“黑盒”ML‑KWS‑for‑MCU 的源码目录结构像一座精心设计的金字塔自下而上共五层每一层都清晰暴露接口绝不隐藏实现Layer 0Hardware Abstraction Layer (HAL)对应Drivers/目录下的STM32F4xx_HAL_Driver。它不直接操作寄存器而是提供HAL_GPIO_WritePin()、HAL_TIM_Base_Start_IT()等函数。但关键在于它只启用项目必需的外设驱动。比如它删掉了stm32f4xx_hal_sd.c、stm32f4xx_hal_eth.c等所有和语音识别无关的驱动只保留hal_gpio.c、hal_tim.c、hal_uart.c、hal_dac.c用于音频播放调试。这不仅减小了代码体积更重要的是它让 HAL 层的耦合度降到最低——你换一块 NXP 的 i.MX RT1050只需替换Drivers/目录上层逻辑几乎不用动。Layer 1CMSIS Core DeviceCMSIS/目录下是 ARM 官方的Core/Include/core_cm4.h等和Device/ST/STM32F4xx/Include/stm32f407xx.h。这是整个项目的“地基”。core_cm4.h里定义了__NVIC_PRIO_BITS、SCB-VTOR等内核寄存器访问宏stm32f407xx.h则把所有外设寄存器地址映射成结构体成员。这里的关键是项目严格遵循 CMSIS 标准命名所有中断服务函数名都是TIM2_IRQHandler、USART1_IRQHandler而不是自定义的my_timer_isr。这保证了 Keil、IAR、GCC 工具链都能正确识别并链接中断向量。Layer 2CMSIS-NN CMSIS-DSPCMSIS/NN/Source/和CMSIS/DSP/Source/是真正的“肌肉”。arm_mfcc_init_q31()初始化梅尔滤波器组arm_rfft_fast_init_q31()初始化快速傅里叶变换arm_convolve_s8()执行卷积。这些函数的参数列表极其“反人类”const q7_t * pIm_weight, const uint16_t col_len, const uint16_t num_of_rows, const uint16_t bias_shift, const uint16_t out_shift, const q7_t * pIm_bias, q7_t * pOut, const uint16_t output_activation_min, const uint16_t output_activation_max, const uint16_t ch_im_in, const uint16_t ch_im_out, const uint16_t dim_im_in, const uint16_t dim_im_out, const uint16_t padding, const uint16_t stride, const uint16_t dim_kernel, const uint16_t dilation, q15_t * pScratch)。但正是这种“啰嗦”暴露了所有优化开关bias_shift控制偏置缩放out_shift控制输出缩放padding和stride直接对应卷积的数学定义。你无法跳过它去“猜”必须理解每个参数的物理意义才能调通。Layer 3Application LogicSource/目录下的kws_main.c、kws_audio.c、kws_model.c是“大脑”。kws_main.c的main()函数只有 50 行核心就是一个while(1)循环里面依次调用audio_capture()、mfcc_compute()、model_run()、result_process()。它不做任何业务逻辑只做流程 orchestration。所有具体工作都下沉到kws_audio.c负责 ADC 采样、双缓冲 DMA 传输、降噪、kws_model.c负责加载权重、调用 CMSIS-NN API、管理激活 buffer。这种设计让你可以轻松替换kws_audio.c为 I2S 接口的麦克风阵列驱动或者把kws_model.c替换为一个更复杂的 TinyML 模型而main()几乎不用改。Layer 4Build Toolchain IntegrationMakefile、keil/、iar/目录是“粘合剂”。Makefile不是简单的gcc -o main.elf main.c它包含了完整的交叉编译链CC arm-none-eabi-gcc、AS arm-none-eabi-gcc -x assembler-with-cpp、LD arm-none-eabi-gcc。它定义了-mcpucortex-m4 -mfloat-abihard -mfpufpv4-d16这些关键 flag并通过-T STM32F407VG_FLASH.ld指定链接脚本。而keil/目录下的.uvprojx文件则精确配置了 Keil MDK 的优化等级--O3 --split_sections、浮点单元Use FPU、以及__main入口点。这确保了无论你用 GCC 还是 Keil生成的二进制文件其内存布局、符号表、启动流程都完全一致。这五层架构不是为了炫技而是为了可验证、可替换、可审计。当你发现模型推理结果异常你可以逐层向上排查是 CMSIS-NN 的convolve算错了还是kws_audio.c的 MFCC 计算有误或是Makefile里-O3优化导致了某个变量被错误优化掉每一层都有明确的输入输出契约没有魔法。2.3 为什么选择 ARM Compiler 5.06u7 而非更新的 AC6网络热词里反复出现arm compiler 5.06u7 download、arm compiler 5.06 update 7 (build 960)这不是偶然。ML‑KWS‑for‑MCU 的官方文档明确要求使用ARM Compiler 5.06 update 7 (build 960)。原因有三CMSIS-NN 的 ABI 兼容性CMSIS-NN 库尤其是Source/ConvolutionFunctions/arm_convolve_s8.c的汇编内联代码是为 AC5 的调用约定calling convention编写的。AC5 使用r0-r3传参r4-r11为 callee-saved 寄存器。而 AC6ARM Compiler 6采用了 LLVM 后端其调用约定更接近 AAPCS64且对__attribute__((naked))函数的处理逻辑不同。实测中用 AC6 编译 CMSIS-NN会导致arm_convolve_s8的某些路径返回错误结果因为寄存器保存/恢复逻辑错乱。Keil MDK 的深度集成ML‑KWS‑for‑MCU 的 Keil 工程.uvprojx大量使用了 AC5 特有的#pragma push/#pragma pop来临时修改编译器行为以及__attribute__((section(.ram_code)))将关键函数放到 RAM 中执行以提升速度。AC6 对这些 pragma 的支持不完全部分会被忽略导致函数未按预期放置。历史稳定性与 Bug 修复AC5.06u7 是 AC5 系列的最后一个稳定版本ARM 官方已为其打上了所有已知的 Cortex-M4 浮点运算 bug 修复补丁。而 AC6 在早期版本中存在一个著名的sqrtf()精度问题在特定输入下结果误差超过 1 ULPUnit in the Last Place这对于需要高精度 MFCC 计算的语音识别是不可接受的。这个问题直到 AC6.15 才被彻底修复但此时 ML‑KWS‑for‑MCU 的代码库已冻结。所以当你的构建日志出现warning: #177-D: variable xxx was declared but never referenced时不要急着升级编译器。先检查kws_config.h里是否启用了KWS_ENABLE_DEBUG_LOG这个宏会条件编译大量printf而 AC5 对未使用的printf参数有更严格的警告。这是 AC5 的“缺点”也是它的“优点”——它强迫你写出更干净、更可控的代码。3. 核心细节解析与实操要点从源码注释里挖出的 7 个黄金线索3.1 注释即文档读懂// brief和// note背后的潜台词ML‑KWS‑for‑MCU 的源码注释不是装饰品而是关键的操作指南。它采用 Doxygen 风格但内容远超一般项目。以Source/Inc/kws_model.h中的kws_model_run()函数为例/** * brief Run the KWS model inference on a given input frame. * param pIn: pointer to the input feature vector (int16_t, size KWS_INPUT_SIZE) * param pOut: pointer to the output probability vector (int16_t, size KWS_OUTPUT_SIZE) * param pScratch: pointer to the scratch buffer (int16_t, size KWS_MODEL_SCRATCH_SIZE) * note This function is NOT reentrant. Do not call it from multiple threads or ISRs. * The pScratch buffer MUST be aligned to 32-byte boundary for optimal CMSIS-NN performance. * If misaligned, CMSIS-NN will fall back to slower C implementation. * retval 0 if success, negative error code otherwise. */ int32_t kws_model_run(const int16_t *pIn, int16_t *pOut, int16_t *pScratch);这段注释里藏着三个必须执行的硬性要求线程安全警告 (note第一行)This function is NOT reentrant。这意味着你不能在TIM2_IRQHandlerADC 采样完成中断里直接调用kws_model_run()。因为中断可能打断主循环中正在进行的kws_model_run()导致共享的pScratch缓冲区被覆盖。正确的做法是在中断里只做数据搬运把 ADC 采样值拷贝到input_buffer然后设置一个model_ready_flag 1在主循环的while(1)里检测到 flag 后再调用kws_model_run()。这是嵌入式实时系统的基本守则但很多新手会忽略。内存对齐要求 (note第二行)MUST be aligned to 32-byte boundary。CMSIS-NN 的汇编 kernel 大量使用vld4.8、vst4.8等 NEON 指令这些指令要求操作的内存地址是 32 字节对齐的即地址的低 5 位必须为 0。如果你用malloc()分配pScratch它只保证 8 字节对齐必然触发 fallback。解决方案是在kws_model.c的全局变量定义处使用__attribute__((aligned(32)))static int16_t model_scratch_buffer[KWS_MODEL_SCRATCH_SIZE] __attribute__((aligned(32)));或者在kws_config.h中将KWS_MODEL_SCRATCH_SIZE手动向上取整到 32 的倍数#define KWS_MODEL_SCRATCH_SIZE ((1280 31) ~31)。错误码语义 (retval)negative error code。打开Source/Src/kws_model.c你会发现错误码定义在enum kws_error_e里KWS_ERROR_NULL_POINTER -1,KWS_ERROR_INVALID_SIZE -2,KWS_ERROR_OUT_OF_MEMORY -3。这告诉你调用kws_model_run()后必须检查返回值。不能只写kws_model_run(input, output, scratch);而要写int32_t ret kws_model_run(input, output, scratch); if (ret 0) { // 根据 ret 的值决定是重启系统、进入安全模式还是仅仅记录日志 kws_error_handler(ret); }这种注释风格贯穿全项目。// note不是“建议”而是“必须遵守的协议”。它把硬件限制、编译器特性、实时系统约束全部编码进了文本注释里。3.2 链接脚本.ld文件Flash/RAM 分区的“宪法性文件”STM32F407VG_FLASH.ld是整个工程的“心脏起搏器”。它决定了代码、数据、堆栈、模型权重、中断向量表各自住在内存的哪个位置。打开它关键段定义如下MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K } SECTIONS { .isr_vector : { . ALIGN(4); KEEP(*(.isr_vector)) /* 强制保留中断向量表 */ . ALIGN(4); } FLASH .text : { . ALIGN(4); *(.text) /* 代码段 */ *(.rodata) /* 只读数据如字符串常量、模型权重 */ *(.rodata*) /* 所有 rodata 子段 */ . ALIGN(4); } FLASH .model_data : { . ALIGN(4); *(.model_data) /* 模型权重显式指定到此段 */ . ALIGN(4); } FLASH .data : { . ALIGN(4); *(.data) /* 初始化过的全局变量 */ . ALIGN(4); } RAM ATFLASH /* .data 在 Flash 中存储初始值运行时拷贝到 RAM */ .bss : { . ALIGN(4); *(.bss) /* 未初始化的全局变量 */ *(COMMON) . ALIGN(4); } RAM }这份脚本揭示了三个核心事实中断向量表的绝对权威.isr_vector段被KEEP(*)强制保留且必须位于 Flash 的起始地址0x08000000。这是因为 Cortex-M 内核在复位后会硬编码从0x08000000地址读取第一个字栈顶指针 SP和第二个字复位向量 PC。如果这个位置没有正确的向量表芯片根本不会启动只会卡死。所以kws_main.c里的void Reset_Handler(void)函数必须被链接器放在.isr_vector段的第一个位置。.model_data的战略地位*(.model_data)被单独拎出来放在.text之后、.data之前。这意味着模型权重和代码一样是 Flash 中的“常量”不需要运行时加载。更重要的是它和.text共享同一个FLASH内存区域因此它们的地址是连续的。这为后续的 OTAOver-The-Air升级提供了便利OTA 固件包只需要包含从0x08000000开始的一整块二进制镜像模型权重自然就在其中。.data的双重生命.data段的RAM ATFLASH语法是精髓。它表示.data段的“内容”即全局变量的初始值存储在 Flash 中ATFLASH但程序运行时“变量本身”必须位于 RAM 中RAM。链接器会在生成的.elf文件里自动插入一段__data_start到__data_end的拷贝代码通常在Reset_Handler里调用SystemInit()之后在main()执行前把 Flash 里的初始值拷贝到 RAM 的对应位置。如果你在kws_config.h里定义了一个大数组int16_t mfcc_filter_bank[40][20]它就会被放入.data段占用宝贵的 RAM 空间。而const int16_t mfcc_filter_bank_const[40][20]则会被放入.rodata存于 Flash不占 RAM。这是嵌入式开发中“常量放 Flash变量放 RAM”的黄金法则。3.3 CMSIS-NN 的“魔鬼细节”arm_convolve_s8的 4 个隐藏参数CMSIS-NN 的 API 文档往往只告诉你“这个函数能做卷积”但实际使用时有四个参数是决定性能和精度的“魔鬼”col_len列长度这不是输入特征图的宽度而是卷积核的宽度乘以输入通道数。例如一个3x3的卷积核作用于 16 个输入通道那么col_len 3 * 3 * 16 144。如果你填错了函数内部的内存访问会越界结果完全不可预测。num_of_rows行数这是输出特征图的高度乘以输出通道数。继续上面的例子如果卷积后输出 32 个通道特征图尺寸是47x47那么num_of_rows 47 * 47 * 32 70688。这个值直接决定了pOut缓冲区的大小。bias_shift和out_shift移位参数这是 int8 量化的灵魂。bias_shift是偏置bias的缩放因子out_shift是输出的缩放因子。它们的值由训练时的量化参数决定。假设训练时某一层的偏置被量化为q7_t其真实值bias_real bias_quantized * (2^(-bias_shift))同理输出output_real output_quantized * (2^(-out_shift))。CMSIS-NN 在内部会执行output_quantized (conv_result bias_quantized) out_shift。如果out_shift设得太小比如 0会导致conv_result bias溢出结果饱和如果设得太大比如 10会导致所有输出都变成 0。这些值必须从训练脚本tools/quantize_model.py的输出日志里精确提取不能凭感觉猜。我曾在一个项目中因为out_shift错填了 1导致模型在 MCU 上的输出全部为 0花了两天时间才定位到这个参数。后来我把所有 CMSIS-NN 的调用都封装成一个宏强制在编译期检查这些参数#define KWS_CONV_S8_CHECKED(pIm_weight, col_len, num_of_rows, bias_shift, out_shift, \ pIm_bias, pOut, min, max, ch_im_in, ch_im_out, \ dim_im_in, dim_im_out, padding, stride, dim_kernel, \ dilation, pScratch) \ do { \ static_assert((col_len) 0, col_len must be 0); \ static_assert((num_of_rows) 0, num_of_rows must be 0); \ static_assert((bias_shift) 0 (bias_shift) 31, bias_shift out of range); \ static_assert((out_shift) 0 (out_shift) 31, out_shift out of range); \ arm_convolve_s8(pIm_weight, col_len, num_of_rows, bias_shift, out_shift, \ pIm_bias, pOut, min, max, ch_im_in, ch_im_out, \ dim_im_in, dim_im_out, padding, stride, dim_kernel, \ dilation, pScratch); \ } while(0)static_assert在编译期就报错比运行时崩溃友好一万倍。4. 实操过程与核心环节实现从零开始搭建一个可审计的构建环境4.1 环境准备放弃“一键安装”拥抱“手动验证”网络热词里充斥着arm compiler 5.06u7 download、arm compiler 5.06 update 7 (build 960)下载但直接下载安装包往往埋下隐患。正确的做法是下载官方源访问 ARM Developer 官网搜索 “ARM Compiler 5.06u7”找到armcc-5.06u7-build-960.exe。注意build-960是关键其他 build 号如 950、970可能有细微差异。安装到纯净路径不要装到C:\Program Files\ARM而是装到C:\tools\armcc-5.06u7。这样可以避免空格和权限问题也方便多个版本共存。手动验证编译器打开命令行执行C:\tools\armcc-5.06u7\bin\armcc --version输出必须是Product: ARM Compiler 5.06 update 7 (build 960) Component: ARM Compiler 5.06 update 7 (build 960) Tool: armcc [4d36a0]如果显示build 950或其他说明安装包不对必须重装。配置环境变量将C:\tools\armcc-5.06u7\bin加入系统PATH。然后在命令行执行armcc --help确认能正常输出帮助信息。交叉验证 GCC虽然项目主推 AC5但你也应该安装GNU Arm Embedded Toolchain推荐 10.3-2021.10 版本。执行arm-none-eabi-gcc --version确保输出10.3.1。这样当你用 GCC 构建时可以对比 AC5 和 GCC 的.map文件看两者对同一段代码的内存占用差异这是审计编译器行为的最直接方法。提示永远不要相信 IDE 的“自动检测”。Keil MDK 的Project - Options - Target - ARM Compiler下拉菜单里即使显示了ARM Compiler 5.06也要点击Manage Project Items - Folders/Extensions手动将C:\tools\armcc-5.06u7\bin添加到ARM Compiler Path。否则MDK 可能偷偷调用自带的旧版本。4.2 构建与分析.map文件是你的“X光机”make all或 Keil 的Build按钮执行后生成的kws.map文件是整个工程的“体检报告”。它比.elf文件更有价值。打开kws.map重点关注三个区域Memory Configuration确认FLASH和RAM的ORIGIN和LENGTH是否与你的芯片匹配。例如STM32F407VG的 Flash 是 1MBORIGIN 0x08000000而STM32F407ZE是 512KBORIGIN相同但LENGTH应为512K。如果这里错了整个固件都会烧录失败。Linker script and memory map这是核心。查找.model_data段.model_data 0x08008000 0x1e880 load address 0x08008000 0x08008000 . ALIGN (0x4) 0x08008000 *(.model_data) 0x08026880 . ALIGN (0x4)这里显示.model_data从0x08008000开始大小0x1e880125,056 字节与kws_config.h中的KWS_MODEL_WEIGHTS_SIZE 124544非常接近差的 512 字节是 padding 对齐。这证明模型权重确实被正确放置到了 Flash 中。Allocating common symbols查找全局变量的 RAM 占用。例如.bss 0x20000000 0x6c00 0x20000000 . ALIGN (0x4) 0x20000000 *(.bss) 0x20006c00 . ALIGN (0x4)0x6c00是 27,648 字节约 27KB。这代表所有未初始化全局变量包括input_buffer、output_buffer、scratch_buffer总共占用了 27KB RAM。而STM32F407VG的 RAM 是 128KB看起来绰绰有余。但别忘了.data段初始化过的变量也会占用 RAM。在.data区域你可能会看到.data 0x20006c00 0x1200这又是 4.5KB。加起来RAM 已用27KB 4.5KB 31.5KB。剩下的128KB - 31.5KB 96.5KB才是留给堆heap和栈stack的空间。而main()函数的默认栈大小是 0x4001KB如果kws_model_run()的递归调用太深或者你开启了KWS_ENABLE_DEBUG_LOG栈空间很容易耗尽导致 HardFault。所以.map文件不仅是“看看”更是你做资源预算的唯一依据。4.3 源码静态评测用cppcheck和clang-tidy揭开隐藏缺陷静态评测不是用眼睛看而是用工具“嗅探”。对 ML‑KWS‑for‑MCU 进行静态分析能发现人工 review 几乎不可能发现的问题。CppcheckC 语言专用执行cppcheck --enableall --inconclusive --suppressmissingIncludeSystem \ --platformunix64 \ --template{file}:{line}:{severity}:{id}:{message} \ Source/Inc/ Source/Src/ CMSIS/NN/Source/ CMSIS/DSP/Source/它会报告诸如error: Uninitialized variable: pOut—— 在kws_model.c的某个分支里pOut指针没有被赋值就直接使用。warning: Array buffer[100] accessed at index 100, which is out of bounds—— 数组越界访问。style: Variable i is assigned a value that is never used—— 无用变量可能是遗留的调试代码。Clang-TidyC/C 通用如果你用 Clang 编译可以启用更多规则clang -x c -stdgnu11 -target arm-none-eabi -mcpucortex-m4 \ -I./Source/Inc -I./CMSIS/Include -I./CMSIS/NN/Include \ -I./CMSIS/DSP/Include \ -fsyntax-only -Xclang -analyzer-checkercore \ -Xclang -analyzer-checkerdeadcode \ Source/Src/kws_model.c它会进行更