嵌入式AI实战:在RT-Thread与STM32上部署轻量化CNN实现手写数字识别

发布时间:2026/8/8 8:30:37
嵌入式AI实战:在RT-Thread与STM32上部署轻量化CNN实现手写数字识别 1. 项目概述在资源受限的嵌入式端跑通CNN最近在折腾一个挺有意思的项目想在一块资源相当有限的嵌入式开发板上实现手写数字的实时识别。听起来是不是有点“小马拉大车”的感觉毕竟一提到卷积神经网络CNN和MNIST数据集大家的第一反应往往是跑在服务器或者高性能PC上用着TensorFlow或PyTorch背后还有强大的GPU在支撑。但我的目标环境是RT-Thread这个轻量级的实时操作系统运行在STM32F4这类典型的微控制器MCU上RAM可能就一两百KBFlash也就1MB左右。在这个背景下把CNN模型部署上去并成功运行其挑战和成就感是完全不同的。这个项目的核心价值就在于探索和验证深度学习模型在极致边缘端的部署可行性。它解决的不仅仅是“能不能跑”的问题更是“如何高效、稳定地跑起来”的问题。想象一下在智能门锁、工业传感器、可穿戴设备这类产品中如果能够本地完成一些简单的图像分类或异常检测而无需将数据上传到云端那将极大地提升响应速度、保护用户隐私并降低系统功耗。MNIST手写体识别作为一个经典的“Hello World”级计算机视觉任务模型相对小巧数据集规整正是进行这类前沿探索的绝佳起点。整个过程会涉及到几个关键环节首先是模型的设计与训练我们需要一个在PC端足够轻量且精度达标的CNN模型然后是关键的模型转换与量化将浮点模型转换为适合MCU运行的定点或整数模型接着是模型在RT-Thread环境下的集成与推理引擎的搭建最后是实际部署与性能优化。无论你是嵌入式开发者想切入AI领域还是AI算法工程师想了解模型落地亦或是学生想做一个软硬结合的毕业设计这个项目都能给你带来一手经验和深刻理解。接下来我就把自己从零搭建这个“嵌入式AI迷你系统”的全过程、踩过的坑以及总结的技巧毫无保留地分享出来。2. 核心思路与方案选型要在MCU上跑CNN蛮干肯定不行必须有一套清晰的、针对资源受限环境的策略。我的整体思路可以概括为“云端训练边缘推理极致优化”。下面拆解一下每个环节的考量和最终选型。2.1 为什么选择RT-Thread作为运行环境RT-Thread不是一个随意的选择。在众多嵌入式操作系统中如FreeRTOS、μC/OSRT-Thread的优势在于它不仅仅是一个实时内核更是一个完整的物联网操作系统平台。对于这个项目它的几个特性至关重要丰富的软件包生态RT-Thread通过其包管理工具env工具、pkgs命令提供了大量现成的软件包。其中就包括对深度学习推理框架如NNoM、TinyEngine、TFLite Micro的移植或支持。这能极大减少我们从头移植底层数学库和推理引擎的工作量。统一的设备驱动框架如果后续需要接入摄像头采集真实图像或者通过LCD显示识别结果RT-Thread的标准设备驱动框架如Sensor、LCD、Touch能让硬件适配工作变得规范且简单。良好的开发调试支持基于GCC的工具链、支持GDB调试、以及Finsh命令行组件使得在MCU上调试复杂的C程序比如我们的推理代码成为可能可以方便地打印中间变量、查看内存使用情况。适中的内存占用RT-Thread内核本身可以根据需求进行高度剪裁最小系统可能只需要几KB的RAM为我们宝贵的RAM资源留出了更多空间给模型和输入输出缓冲区。对比单纯的裸机编程RT-Thread提供了多任务管理、文件系统、网络协议栈等基础设施让我们的应用逻辑更清晰对比其他RTOS其软件包生态对AI应用更友好。因此它是平衡开发效率与运行效率的优选。2.2 模型设计与训练策略追求极致的“小”在PC端训练模型时目标就和常规的AI项目不同了。我们不再一味追求最高的准确率比如99.5%以上而是要在可接受的精度损失例如98%以上和极致的模型大小、计算量之间找到最佳平衡点。MNIST是28x28的灰度图这本身就是一个巨大的优势。我设计的CNN结构遵循了嵌入式端模型设计的几个黄金法则减少参数量参数量直接决定模型文件大小占用Flash和运行时权重对内存的占用。大量使用3x3的小卷积核并在卷积后紧跟池化层如2x2 MaxPooling来快速降低特征图尺寸。降低计算量FLOPs计算量影响推理速度和功耗。在降低特征图尺寸后适当增加通道数以保持信息容量。避免使用全连接层Fully Connected Layer因为它们是“参数大户”。例如在最后一个卷积层后我使用全局平均池化Global Average Pooling将每个特征图池化成一个值再连接一个小的全连接层或直接使用卷积层等效替代输出10个分类这比直接接一个大尺寸的全连接层参数少得多。利用深度可分离卷积这是MobileNet等高效网络的核心思想。它将标准卷积分解为深度卷积逐通道卷积和逐点卷积1x1卷积能大幅减少计算量和参数。对于MNIST一个简单的2-3层深度可分离卷积模块就能取得很好效果。我最终采用的训练流程是使用PyTorch框架在MNIST训练集上训练一个精简的CNN模型。训练时就会引入一些优化比如权重剪枝将一些不重要的权重置零为后续的量化做准备。训练完成后在测试集上评估精度确保达到98%以上这个基线。2.3 推理框架选型TFLite Micro vs. NNoM vs. 自研模型训练好了怎么在C环境下跑起来这里有几种主流方案TensorFlow Lite for Microcontrollers (TFLite Micro)谷歌官方出品生态最好支持算子较多且有完整的量化工具链。但库文件相对较大需要针对具体MCU平台进行一些移植和裁剪工作。RT-Thread的软件包中可能有社区移植的版本。NNoM一个为MCU量身定做的神经网络推理框架纯C实现不依赖任何第三方库。它的API设计非常简洁部署流程清晰特别适合像我们这样从零开始的入门项目。其核心是将训练好的模型权重和结构转换为静态的C数组直接编译进程序。缺点是高级算子支持可能不如TFLite Micro全面。CMSIS-NNARM专门为其Cortex-M系列处理器优化的神经网络内核函数库。如果你使用的是ARM MCU如STM32并且选择自研推理引擎那么利用CMSIS-NN进行卷积、全连接等关键计算的加速是一个必选项。它可以作为NNoM或自研引擎的底层加速库。自研简易推理引擎对于MNIST这种超小模型理论上可以自己写几个循环来实现卷积和池化。但这只适用于教学和验证思想缺乏扩展性和优化不推荐用于实际项目。综合考量生态、易用性和社区支持我选择了NNoM作为本次项目的推理框架。原因在于它足够轻量与RT-Thread集成简单部署步骤文档清晰非常适合作为第一个嵌入式AI项目的切入点。它的工作流程是用Python脚本将训练好的Keras或PyTorch模型需先转ONNX转换为NNoM所需的模型文件.c和.h里面包含了模型结构和权重。我们将这些文件加入工程调用NNoM的API即可完成推理。2.4 模型量化从FP32到INT8的关键一跃这是决定模型能否在MCU上运行的关键步骤。量化就是将模型权重和激活值从高精度的浮点数如FP32转换为低精度的整数如INT8。其好处是巨大的模型体积缩小约75%从32位到8位理论上模型文件大小变为1/4。内存占用减少推理时的中间激活值也用8位存储大幅降低RAM需求。计算加速许多MCU的硬件指令集对整数运算有更好的支持甚至没有硬件FPU浮点单元整数运算速度远快于软件模拟的浮点运算。NNoM和TFLite Micro都支持训练后量化。我们需要准备一个代表性的数据集可以从MNIST训练集中取几百张图用于校准量化工具会分析模型中各层激活值的动态范围确定合适的缩放比例和零点。最终生成一个INT8模型。这个过程可能会带来轻微的精度损失通常在1%以内但对于MNIST任务是完全可接受的。3. 开发环境搭建与模型转换实操思路理清了现在开始动手。这一部分我会详细记录从软件安装到模型成功转换的每一步。3.1 PC端训练环境配置首先我们需要在电脑上准备好训练和转换模型的环境。我使用的是Anaconda来管理Python环境避免包冲突。# 创建一个新的conda环境 conda create -n rtt-cnn python3.8 conda activate rtt-cnn # 安装PyTorch根据你的CUDA版本选择CPU版本也可以 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu # 安装ONNX和ONNX简化工具用于模型格式转换 pip install onnx onnx-simplifier # 安装NNoM的模型转换工具 pip install nnom注意nnom包的安装可能会依赖其他库如tensorflow或keras。如果你的训练框架是PyTorch可能只需要它的转换功能部分依赖。确保你的环境能成功导入nnom。3.2 训练一个精简的CNN模型下面是我用PyTorch定义和训练的一个示例模型结构非常精简import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader class TinyMNISTCNN(nn.Module): def __init__(self): super(TinyMNISTCNN, self).__init__() # 输入: 1x28x28 self.conv1 nn.Conv2d(1, 8, kernel_size3, stride1, padding1) # 输出: 8x28x28 self.relu1 nn.ReLU() self.pool1 nn.MaxPool2d(kernel_size2, stride2) # 输出: 8x14x14 self.conv2 nn.Conv2d(8, 16, kernel_size3, stride1, padding1) # 输出: 16x14x14 self.relu2 nn.ReLU() self.pool2 nn.MaxPool2d(kernel_size2, stride2) # 输出: 16x7x7 # 全局平均池化替代展平全连接 self.gap nn.AdaptiveAvgPool2d((1, 1)) # 输出: 16x1x1 self.flatten nn.Flatten() # 输出: 16 self.fc nn.Linear(16, 10) # 输出: 10 def forward(self, x): x self.pool1(self.relu1(self.conv1(x))) x self.pool2(self.relu2(self.conv2(x))) x self.gap(x) x self.flatten(x) x self.fc(x) return x # 数据加载与训练循环此处省略细节为标准流程 # 包括数据预处理、损失函数定义CrossEntropyLoss、优化器Adam、训练epoch循环等。 # 训练目标在测试集上准确率 98%。训练完成后将模型保存为PyTorch的.pth文件。3.3 模型转换PyTorch - ONNX - NNoMNNoM目前主要支持从Keras或ONNX模型进行转换。因此我们需要先将PyTorch模型转为ONNX格式。import torch # 加载训练好的模型 model TinyMNISTCNN() model.load_state_dict(torch.load(tinymnist_cnn.pth)) model.eval() # 设置为评估模式 # 创建一个虚拟输入dummy input dummy_input torch.randn(1, 1, 28, 28) # 批大小通道高宽 # 导出为ONNX模型 torch.onnx.export(model, dummy_input, “tinymnist_cnn.onnx”, export_paramsTrue, opset_version11, # 选择一个合适的opset版本 input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}})得到ONNX模型后可以使用onnx-simplifier对其进行优化合并一些操作使模型结构更干净。python -m onnxsim tinymnist_cnn.onnx tinymnist_cnn_sim.onnx接下来使用NNoM提供的转换脚本nnom_onnx2c.py将简化后的ONNX模型转换为C文件。这个脚本会执行量化操作。# 假设转换脚本在PATH中或者使用python -m执行 python nnom_onnx2c.py tinymnist_cnn_sim.onnx --output-dir ./model_output --name mnist_model --quantize 8关键参数解释--quantize 8: 指定进行8位整数INT8量化。--output-dir: 指定输出目录。--name: 生成文件的前缀名。执行成功后在./model_output目录下你会得到两个关键文件mnist_model.c和mnist_model.h。.c文件里包含了模型的所有权重已量化为INT8和网络结构代码.h文件则声明了相关的API最主要的是一个mnist_model结构体和一个mnist_model_init()函数。3.4 RT-Thread工程准备与NNoM集成现在我们将工作重心转移到嵌入式端。首先确保你已经安装好了RT-Thread的开发环境如Env工具、ARM GCC工具链。创建/打开一个RT-Thread项目可以使用scons --dist生成一个标准项目或者基于已有的BSP如STM32F407-ATK-Explorer创建。启用NNoM软件包在项目根目录下使用menuconfig命令进入配置界面。RT-Thread online packages --- miscellaneous packages --- [*] NNoM: A high-level neural network inference library for microcontrollers选中NNoM包保存并退出。然后使用pkgs --update命令下载该软件包到项目的packages文件夹。集成模型文件将上一步生成的mnist_model.c和mnist_model.h复制到你的项目源代码目录下例如applications文件夹。修改SConscript在存放模型文件的目录的SConscript中确保这两个文件被添加到编译源文件列表中。通常是在applications文件夹的SConscript里加上from building import * cwd GetCurrentDir() src Glob(*.c) [mnist_model.c] # 添加模型C文件 CPPPATH [cwd] group DefineGroup(Applications, src, depend [], CPPPATH CPPPATH) Return(group)包含头文件在你的主应用文件如main.c或application.c中包含NNoM和模型的头文件#include rtthread.h #include nnom.h #include “mnist_model.h”至此开发环境和模型集成工作就准备好了。接下来就是编写推理代码并将其嵌入到RT-Thread的任务中。4. RT-Thread下的推理任务实现模型已经就位现在我们需要在RT-Thread中编写代码加载模型处理输入数据并执行推理。4.1 初始化NNoM与模型首先在应用初始化部分比如一个线程的入口函数里我们需要初始化NNoM的运行时并注册我们的模型。// 定义一个静态的模型实例和输入输出缓冲区 static nnom_model_t *model; static int8_t input_buf[28*28]; // MNIST输入量化后为INT8 static int8_t output_buf[10]; // 10个分类的输出INT8 void mnist_thread_entry(void *parameter) { // 1. 初始化NNoM nnom_init(); // 2. 创建并初始化模型 model nnom_model_create(); if(mnist_model_init(model) ! NN_SUCCESS) { rt_kprintf(“Model initialization failed!\n”); return; } rt_kprintf(“MNIST CNN model loaded successfully.\n”); // ... 后续推理循环代码 }mnist_model_init()函数是由转换脚本自动生成的它负责将模型结构各层参数和权重数据填充到我们传入的nnom_model_t结构体中。4.2 输入数据预处理与量化MNIST数据集是28x28的灰度图像素值范围是0-255。我们的模型在训练后量化时已经确定了一个输入层的量化参数缩放比例scale和零点zero point。我们需要在嵌入式端将原始的uint8像素值按照同样的规则转换为INT8。转换公式通常是q round(f / scale) zero_point在NNoM生成的mnist_model.h文件中通常会以宏定义或全局变量的形式提供输入层的量化参数。假设我们找到#define MNIST_INPUT_SCALE 0.003921568859368563 // 近似 1/255 #define MNIST_INPUT_ZEROPOINT -128那么预处理函数如下void preprocess_image(uint8_t *raw_image, int8_t *quantized_input) { float scale MNIST_INPUT_SCALE; int32_t zero_point MNIST_INPUT_ZEROPOINT; for(int i 0; i 28*28; i) { float pixel_f (float)raw_image[i]; // 量化浮点 - 整数 int32_t quantized (int32_t)round(pixel_f / scale) zero_point; // 确保值在INT8范围内并赋值 if(quantized 127) quantized 127; if(quantized -128) quantized -128; quantized_input[i] (int8_t)quantized; } }在实际应用中raw_image可以来自多种途径预先存储在Flash中的测试图片数组、通过文件系统读取的图片文件、或者从摄像头采集并经过二值化/缩放的实时图像。4.3 执行推理与结果解析准备好输入数据后就可以运行模型了。// 假设我们已经将一张图片处理到了 input_buf 中 // 设置模型输入 model-set_input(model, input_buf, 28*28); // 第二个参数是数据第三个是数据大小元素个数 // 运行模型推理 if(model-run(model) ! NN_SUCCESS) { rt_kprintf(“Model run failed!\n”); return; } // 获取输出 int8_t *prediction model-get_output(model, 0); // 获取第0个输出层的缓冲区 // 输出结果是10个INT8值代表每个数字的“分数”logits // 需要找到最大值对应的索引即为预测的数字 int8_t max_val prediction[0]; int max_idx 0; for(int i 1; i 10; i) { if(prediction[i] max_val) { max_val prediction[i]; max_idx i; } } rt_kprintf(“Predicted digit: %d with score: %d\n”, max_idx, max_val);4.4 构建一个完整的测试线程我们将上述步骤整合到一个RT-Thread线程中周期性地对一组测试图片进行推理并统计准确率。// 假设有一个外部定义的测试数据集 extern uint8_t test_images[100][784]; // 100张测试图 extern uint8_t test_labels[100]; // 对应的100个标签 static void mnist_test_thread_entry(void *parameter) { nnom_init(); model nnom_model_create(); if(mnist_model_init(model) ! NN_SUCCESS) { rt_kprintf(“Model init fail.\n”); return; } int correct 0; for(int img_idx 0; img_idx 100; img_idx) { // 1. 预处理 preprocess_image(test_images[img_idx], input_buf); // 2. 推理 model-set_input(model, input_buf, 784); if(model-run(model) ! NN_SUCCESS) { rt_kprintf(“Run fail at image %d\n”, img_idx); continue; } // 3. 解析结果 int8_t *out model-get_output(model, 0); int pred argmax(out, 10); // 4. 判断对错 if(pred test_labels[img_idx]) { correct; } rt_thread_mdelay(10); // 稍作延时避免占用全部CPU } rt_kprintf(“Test Accuracy: %d/%d %.2f%%\n”, correct, 100, (correct*100.0)/100.0); // 清理模型 nnom_model_delete(model); }最后别忘了在main函数或某个初始化函数中创建并启动这个线程rt_thread_t tid rt_thread_create(“mnist”, mnist_test_thread_entry, RT_NULL, 2048, 25, 10); if(tid ! RT_NULL) rt_thread_startup(tid);5. 性能优化与内存管理实战在MCU上运行性能速度和内存是永恒的主题。模型跑起来只是第一步跑得快且稳才是目标。5.1 内存使用分析与优化首先我们需要了解模型运行时的内存消耗。NNoM在初始化模型时会根据网络结构自动分配用于存放中间激活值特征图的工作缓冲区。我们可以通过打印信息来查看// 在模型初始化后调用 rt_kprintf(“Model memory usage:\n”); rt_kprintf(“ - Total weights size: %d bytes\n”, model-weights_size); rt_kprintf(“ - Total activation memory (中间层内存): %d bytes\n”, model-mem_size);对于STM32F407192KB RAM一个轻量级MNIST模型的mem_size可能在20-50KB之间这通常是可接受的。但如果内存紧张可以尝试以下优化调整网络结构减少特征图通道数或尺寸。这是最根本的方法。使用内存复用NNoM默认会尝试复用内存。确保在menuconfig中启用了NNoM的内存管理优化选项。静态内存分配对于确定的模型可以关闭动态内存分配使用静态数组作为工作缓冲区避免堆内存碎片。这需要在NNoM配置中开启NNOM_USING_STATIC_MEMORY并提供一个足够大的静态数组。使用外部RAM如果MCU支持且板载了外部RAM如SDRAM可以将模型权重或工作缓冲区放到外部但这会以访问速度变慢为代价。5.2 计算性能分析与加速推理速度是另一个关键指标。我们可以在推理前后使用RT-Thread的系统时钟来测量耗时。#include rtdevice.h rt_tick_t start, end; start rt_tick_get(); model-run(model); end rt_tick_get(); rt_kprintf(“Inference time: %d ms\n”, (end - start) * 1000 / RT_TICK_PER_SECOND);对于STM32F407 168MHz一个简单的CNN推理单张MNIST图片的时间可能在几十到几百毫秒量级。提升速度的方法启用CMSIS-NN这是为ARM Cortex-M内核优化的神经网络库。NNoM可以配置为使用CMSIS-NN作为后端。在menuconfig的NNoM配置中启用NNOM_USING_CMSIS_NN。这通常能带来数倍的性能提升因为CMSIS-NN使用了SIMD指令和手写的汇编内核。优化编译器选项在项目的rtconfig.py中确保启用了最高级别的优化如-O3和硬件FPU支持如果MCU有FPU但对于INT8模型FPU帮助不大。针对ARM架构的优化选项如-mcpucortex-m4 -mfpufpv4-sp-d16 -mfloat-abihard也需要正确设置。降低时钟频率不对于计算密集型任务提高主频能直接提升速度。确保MCU运行在允许的最高频率。模型层面优化使用深度可分离卷积、减少网络层数、使用更小的卷积核。5.3 功耗考量在电池供电的设备中功耗至关重要。AI推理是计算密集型任务会显著增加瞬时功耗。间歇性工作不要让MCU一直处于全速推理状态。设计为由外部事件如传感器触发唤醒完成一次推理后迅速进入睡眠模式。动态频率调整在空闲时降低系统主频。外设管理推理时关闭所有不必要的外设时钟如未用的UART、SPI等。6. 调试技巧与常见问题排查在嵌入式AI开发中调试比普通嵌入式开发更具挑战性。以下是我在实践中总结的一些技巧和常见问题的解决方法。6.1 精度丢失问题PC端 vs. 嵌入式端这是最常见的问题在PC上测试精度98%部署到板子上准确率却大幅下降。排查步骤1检查输入数据预处理。这是罪魁祸首的常见位置。务必确保嵌入式端的预处理归一化、量化与模型训练/转换时的预处理完全一致。仔细核对缩放比例和零点。一个简单的验证方法是在PC端用Python脚本模拟一遍嵌入式端的预处理流程然后将处理后的数据输入到原始的浮点模型和量化后的模型中对比输出。NNoM的转换脚本通常也会生成一个用于验证的Python脚本一定要用它做交叉验证。排查步骤2检查量化参数。确认模型转换时使用的校准数据集具有代表性。如果校准集太小或分布有偏量化参数可能不准确。排查步骤3检查内存越界。如果输入缓冲区或模型内部缓冲区发生内存越界写操作可能会破坏权重或中间数据导致随机错误。使用RT-Thread的内存保护功能或仔细检查数组索引。排查步骤4逐层调试。如果框架支持可以尝试获取中间层的输出。在PC端和嵌入式端输入相同的数据对比某一层如第一个卷积层后的输出看是从哪一层开始出现差异。6.2 模型运行崩溃或HardFault原因1内存不足。这是最可能的原因。检查链接脚本.ld文件确保RAM区域分配了足够的堆和栈空间。增大RT-Thread主线程或模型运行线程的栈大小。使用rt_memory_info()函数打印内存使用情况。原因2对齐问题。某些CMSIS-NN函数或硬件加速指令要求数据地址按特定字节如4字节、8字节对齐。确保输入输出缓冲区的地址是对齐的。可以使用RT_ALIGN宏来分配对齐的内存。原因3编译器优化导致的问题。尝试将优化等级从-O3暂时调到-O0进行调试看问题是否消失。有时激进的优化会引发意想不到的行为。原因4中断干扰。高优先级的中断服务程序打断了模型推理过程中的长时间计算如果ISR也使用了大量栈或破坏了某些寄存器可能导致上下文恢复出错。可以尝试在推理关键代码段前后加上rt_enter_critical()和rt_exit_critical()进行临界区保护。6.3 性能不达预期检查是否启用了CMSIS-NN在menuconfig中确认并重新编译。检查编译器优化选项确认-O3已开启。使用性能分析工具如果MCU支持使用Segger SystemView或STM32CubeMonitor等工具分析任务执行时间和CPU占用率找出瓶颈。模型本身复杂度回顾模型结构是否还有精简的空间例如将3x3卷积换成1x1卷积加深度可分离卷积。6.4 部署与维护建议版本管理将PC端的训练脚本、转换脚本和嵌入式端的模型文件、推理代码纳入统一的版本管理如Git。记录每次模型迭代对应的精度、大小和性能数据。持续集成可以搭建简单的CI流程在修改代码后自动编译、下载到开发板、运行测试集并报告准确率确保修改不会破坏现有功能。模型更新考虑如何在不重新烧录整个固件的情况下更新模型。可以将模型权重存储在外部Flash或SD卡中上电时加载到RAM或内部Flash的指定区域。这需要设计一个可靠的模型加载和验证机制。从PC上庞大的深度学习框架到MCU上区区几百KB内存中的INT8推理这个过程就像把一头大象装进冰箱不仅需要步骤正确更需要对每一个环节的深刻理解和精细控制。通过这个MNIST手写识别项目我们完整地走通了嵌入式AI部署的全链路模型轻量化、训练后量化、框架集成、嵌入式编程、性能调优和问题排查。这其中的每一个坑每一次性能提升都加深了对“边缘智能”这四个字的理解——它不是在边缘跑一个阉割版的AI而是为了在严苛的资源限制下依然能让AI发挥价值而进行的一系列系统级设计和优化。希望这份详细的记录能为你点亮在微控制器上探索人工智能的第一盏灯。