K210开发板学习笔记写到第三篇,前两篇分别折腾了环境搭建和摄像头基础采集,这次直接上了一个相对完整的组合方案:STM32做逻辑主控,K210负责图像采集和人脸检测识别,SD卡用来存注册人脸照片和比对数据,三者通过串口和文件系统串起来,最终实现一个能跑起来的人脸识别门禁雏形。
先说清楚这套东西能干什么:K210内部集成了KPU(神经网络处理器),跑轻量级人脸检测模型非常流畅,适合做边缘端的实时视觉识别。STM32则负责按键、继电器、蜂鸣器、显示屏逻辑这类外围控制,两个芯片各干各的,K210识别到指定人脸后通过串口把结果发给STM32,STM32再决定开不开锁、放不放行。SD卡在这个设计里承担的是“注册照库”的角色,K210上电后从SD卡读取已注册的人脸照片,实时画面与注册库做比对。
这篇文章适合三类人:一是K210入门后想进阶做完整项目的开发者,二是毕业设计选题涉及人脸识别的同学,三是想把边缘AI识别和传统MCU控制结合起来做产品的创客。代码部分我尽量给出可落地的实现,同时把踩过的坑一并整理出来,省得你在串口通信和FATFS文件系统上反复折腾。
1. 整体方案思路:K210、STM32和SD卡各自该干什么
1.1 为什么不能让K210“一个人全干”
很多人拿到K210第一反应是:这板子又带摄像头又能跑模型,一个人全搞定不就行了,为什么要加一块STM32?这个想法在纯Demo阶段完全没问题,K210用MaixPy固件十几行代码就能实现人脸检测,甚至识别。但一旦到了实际产品或者说一个“稍微完整一点”的系统里,单靠K210就会碰到几个现实问题。
第一是外设资源不够用。K210虽然接口不少,但它的定位是AI计算加速器,GPIO、定时器、中断、低功耗管理的资源都比较有限。你想让它一边跑人脸识别模型,一边控制继电器、驱动电磁锁、读取指纹模块、管理按键输入,还要处理日志记录,代码耦合度会非常高,后面改一版需求和排一个bug都痛苦。
第二是安全性问题。人脸识别门禁这种场景,如果主控挂了,摄像头采集和识别就会一起挂,整机直接瘫痪。用STM32单独做逻辑控制,即使K210因为程序跑飞或者温度过高不正常工作,STM32依然可以执行一些基本的安全逻辑——比如超时报警、进入应急放行模式。两套系统之间互相独立,这在产品设计里是很重要的冗余思路。
第三是开发效率。K210的AI推理代码和人机交互逻辑混在一起,调试的时候经常要反复烧录固件,很浪费时间。分开之后,K210的代码几乎不用大改,你只专注跑模型和出结果;STM32那边想加什么功能就加什么功能,两个芯片之间约定好串口协议就行,团队协作时也能分人并行开发。
所以我的建议很明确:如果只是学习验证,随意;如果目标是做一个功能相对完整的人脸识别门禁、考勤机或者智能储物柜,老老实实把系统拆成视觉模块和控制模块,两边各司其职,后面扩展起来会舒服很多。
1.2 SD卡在这里的角色:注册照片库与日志存储
SD卡不是必须的,但加上之后这个项目的实用性和“完成度”会明显上一个台阶。为什么?
首先,如果没有SD卡,K210的人脸识别基本只能靠板载Flash里预置的特征数据,最多也就存几个人,换个用户还得重新烧录固件,这对实际使用来说太不友好了。加入SD卡后,注册人的照片以文件的形式放在SD卡里,新增一个人就是往里拷一张照片,完全不用动代码,用户管理和数据更新变成了纯文件操作,门槛大大降低。
其次,SD卡还可以顺手做一块“事件记录仓库”。我在这套项目里将K210的识别结果、出入记录、比对耗时写成一个文本日志追加到SD卡里,STM32那边依然有日志,但STM32的Flash容量有限,不适合频繁擦写。K210这边把每一次识别事件带时间戳存下来,用上位机或者读卡器直接就能打开看谁在什么时间通过了认证,这个体验非常接近商品化考勤门禁产品。
另外,从数据流角度看,SD卡也承担了“冷启动数据恢复”的功能。K210上电后先检查SD卡里有没有注册库文件夹,没有就提示“未注册用户”,有就加载并构建索引。掉电之后重新上电,注册库依然存在,不需要重新注册登记,这对实际工程落地很重要。
2. 硬件接线与SD卡文件系统基础
2.1 板子与模块的物理连接
这套项目我用的是Sipeed Maix Bit开发板加OV2640摄像头模块,STM32那边用F103ZET6最小系统板,SD卡直接用K210板载Micro SD卡槽。供电方面提醒一句,K210板载TF卡槽和摄像头如果同时工作,瞬时电流会上来不少,不要指望USB线那点电能把三块板子都带起来,最好用5V/2A以上的电源给STM32供电,再从STM32的5V引脚引给K210。
K210和STM32之间只需要三根线就能通信:K210的TX接STM32的RX,K210的RX接STM32的TX,然后两块板子共地。我用的具体引脚是K210的IO10作为UART TX,IO11作为UART RX,在STM32那边接USART1的PA9和PA10,逻辑电平都是3.3V,可以直接连接,不用电平转换。如果你手头的STM32板子IO口引出了5V电平(比如某些老式板子),就得加电平转换模块或者用二极管钳位,否则K210的IO口容易被打坏。
摄像头和数据接口方面,Maix Bit的DVP接口直接插OV2640就行,注意插的时候排线方向别插反了,金属触点朝哪一侧每个板子略有不同,买的时候问卖家要一下板子手册确认,插反的后果轻则黑屏,重则烧摄像头模组。
2.2 FATFS在K210侧的集成要点
K210官方有两种开发方式,一种是MicroPython(MaixPy)脚本,一种是用C/C++标准SDK。如果你做小型验证,用MaixPy没问题;但如果你想做项目级交付,强烈建议切换到C/C++标准SDK,毕竟MicroPython的解释执行效率、内存占用和崩溃可控性在大负载应用里都不占优势。
SD卡文件系统用的是FATFS(FatFs文件系统),这部分官方SDK里的例程做得比较完善,自带了SD卡驱动和FATFS移植好的接口,你在工程里直接引入ff.h和diskio.h就能用。使用前需要在ffconf.h里做几个关键配置,否则会出现莫名其妙的问题:
#define FF_USE_MKFS 1 #define FF_USE_STRFUNC 1 #define FF_USE_LFN 1 #define FF_MAX_LFN 255 #define FF_VOLUMES 1其中FF_USE_LFN特别重要,K210的SDK默认配置可能把这个开关关掉了,导致你访问长文件名或者中文文件名时会报FR_INVALID_NAME错误。建议还有一个容易忽略的配置:FF_FS_MINIMIZE保持默认0,这样你才能用f_stat、f_mkdir这类函数,后面检查目录和创建文件夹都会用到。
SD卡挂载代码基本是固定流程:
FIL file; FATFS fs; FRESULT res; res = f_mount(&fs, "0:", 1); if (res != FR_OK) { printf("SD card mount failed, error: %d\n", res); return; } res = f_mkdir("0:/faces"); if (res != FR_OK && res != FR_EXIST) { printf("Create faces dir failed, error: %d\n", res); return; }这里f_mount返回不是FR_OK的话,多半是卡没插好、卡格式不对,或者SPI时钟频率太高导致通信不稳定,建议入门阶段先把SPI时钟频率降下来,用2MHz左右的低速初始化,挂载成功之后再提速,这样能避免很多玄学问题。
2.3 文件名管理与目录规划
SD卡里的文件结构建议固定下来,代码才不会写得乱:
SD卡根目录 ├── faces │ ├── user_001.jpg │ ├── user_002.jpg │ └── ... ├── logs │ └── access.log └── model └── face_detect.kmodelfaces目录放注册照片,文件名前缀建议统一用user_xxx.jpg,方便代码按序遍历。model目录放KPU要加载的人脸检测模型,把模型文件从Flash挪到SD卡也是一种很实用的做法,换模型不用重烧固件,但SD卡读取速度比Flash慢不少,所以我的建议是开发阶段放SD卡方便迭代,产品化阶段还是把模型烧进Flash,启动速度能快很多。
日志文件access.log就是简单的文本追加,每次识别事件往里面写一行,时间可以用K210的RTC或者通过串口从STM32那边获取时间,我这里图省事用的脱机时间戳,没有接外部RTC芯片,实际产品建议加一颗SD3031这类RTC模块保证时间准确。
3. K210核心代码实现:从摄像头采集到人脸比对
3.1 摄像头与LCD的初始化顺序
K210上摄像头初始化和LCD刷新这几个外设之间是有先后逻辑的,不是随便调几个初始化函数就能出画面。我把完整的初始化代码贴出来,关键位置加了注释:
#include "kpu.h" #include "fpioa.h" #include "lcd.h" #include "nt35310.h" #include "ov2640.h" #include "dvp.h" #include "plic.h" #include "sysctl.h" #include "uart.h" #include "ff.h" #include "image.h" #define DISPLAY_WIDTH 320 #define DISPLAY_HEIGHT 240 #define CAM_WIDTH 320 #define CAM_HEIGHT 240 static uint32_t lcd_buf[LCD_X_MAX * LCD_Y_MAX]; static uint8_t cam_buf[CAM_WIDTH * CAM_HEIGHT * 2]; static uint8_t display_buf[CAM_WIDTH * CAM_HEIGHT * 2]; static volatile uint8_t dvp_finish_flag = 0; static void dvp_finish_cb(void) { dvp_finish_flag = 1; } static void io_mux_init(void) { // LCD引脚复用 fpioa_set_function(36, FUNC_CMOS_D0); fpioa_set_function(37, FUNC_CMOS_D1); fpioa_set_function(38, FUNC_CMOS_D2); fpioa_set_function(39, FUNC_CMOS_D3); fpioa_set_function(40, FUNC_CMOS_D4); fpioa_set_function(41, FUNC_CMOS_D5); fpioa_set_function(42, FUNC_CMOS_D6); fpioa_set_function(43, FUNC_CMOS_D7); fpioa_set_function(44, FUNC_CMOS_XCLK); fpioa_set_function(45, FUNC_CMOS_PCLK); fpioa_set_function(46, FUNC_CMOS_HREF); fpioa_set_function(47, FUNC_CMOS_VSYNC); fpioa_set_function(52, FUNC_SPI0_SCLK); fpioa_set_function(53, FUNC_SPI0_MOSI); fpioa_set_function(54, FUNC_SPI0_MISO); fpioa_set_function(55, FUNC_SPI0_CS0); fpioa_set_function(56, FUNC_SPI0_CS1); // UART引脚复用: IO10=TX, IO11=RX fpioa_set_function(10, FUNC_UART1_TXD); fpioa_set_function(11, FUNC_UART1_RXD); } static void uart_init_custom(void) { uart_init(UART_DEVICE_1); uart_configure(UART_DEVICE_1, 115200, 8, UART_STOP_1, UART_PARITY_NONE); }初始化顺序上,官方例程的要求是:先初始化FPIOA引脚复用,然后初始化SPI和摄像头,再初始化LCD,最后挂中断和启动持续采集。顺序不能乱,特别要注意先dvp_init再lcd_init,因为LCD初始化会读取屏幕ID,而SPI总线的引脚和DVP是有复用关系的,顺序反了容易导致摄像头输出异常。
3.2 KPU加载人脸检测模型
K210的KPU加载模型有两种方式:从Flash地址直接加载,或者从SD卡通过文件系统读取后加载。Flash方式速度快、稳定性高,适合产品;SD卡方式灵活,适合开发阶段反复换模型。我的调试版本采用的是SD卡方式,方便平时烧录验证。
核心代码段:
static kpu_model_context_t face_detect_ctx; static int load_model_from_sd(const char *filename) { FIL file; FRESULT res; uint32_t file_size; uint8_t *model_buf; res = f_open(&file, filename, FA_READ); if (res != FR_OK) { printf("Open model file failed: %d\n", res); return -1; } file_size = f_size(&file); model_buf = (uint8_t *)malloc(file_size); if (!model_buf) { printf("Malloc model buffer failed\n"); f_close(&file); return -2; } UINT br; res = f_read(&file, model_buf, file_size, &br); if (res != FR_OK || br != file_size) { printf("Read model file failed: %d, read=%d/%d\n", res, br, file_size); free(model_buf); f_close(&file); return -3; } f_close(&file); if (kpu_load_kmodel(&face_detect_ctx, model_buf) != 0) { printf("KPU load model failed\n"); free(model_buf); return -4; } free(model_buf); return 0; }有个细节必须提醒:kpu_load_kmodel这个函数,不少人的认知是加载完模型后模型数据还存在模型缓冲区里,KPU推理时可以直接访问,实际上它会拷贝模型数据到KPU内部的模型内存。所以在kpu_load_kmodel成功后,model_buf就可以释放了。但如果模型特别大,超过了KPU内部可用内存,会加载失败,这种情况需要检查kpu_model_context_t的返回值或者打印KPU版本和内存状态来排查。
人脸检测模型跑起来之后,kpu_run_kmodel会输出一大段浮点数据,需要经过box_anchor解析才能转成具体的人脸框坐标。这段解析代码几乎是所有K210人脸识别项目的“必踩点”,网上流传版本也很多,但核心逻辑一样,都是把KPU输出的特征图解码成人脸候选框,再用NMS(非极大值抑制)去掉重叠框,最终得到每个人脸的位置。这部分我用的是官方参考实现里的box_proposals加NMS,具体代码比较长,这里贴出主干逻辑:
static void handle_kpu_output(kpu_model_context_t *ctx, uint8_t *cam_buf, uint8_t *dis_buf) { float *output = (float *)ctx->output; size_t output_size = kpu_model_output_size(ctx); box_t *boxes = NULL; int box_count = 0; // 解析输出,得到候选框 box_count = get_box_proposals(output, output_size, &boxes); if (box_count <= 0 || boxes == NULL) { return; } // 简单NMS,保留置信度最高的框 int keep[10]; int keep_count = 0; for (int i = 0; i < box_count && keep_count < 10; i++) { int ok = 1; for (int j = 0; j < keep_count; j++) { if (box_iou(&boxes[i], &boxes[keep[j]]) > 0.4f) { if (boxes[i].score > boxes[keep[j]].score) { keep[j] = i; } ok = 0; break; } } if (ok) { keep[keep_count++] = i; } } // 绘制检测框和置信度 for (int i = 0; i < keep_count; i++) { box_t *b = &boxes[keep[i]]; rectangle(dis_buf, b->x, b->y, b->w, b->h, COLOR_GREEN); char score_str[16]; snprintf(score_str, sizeof(score_str), "%.2f", b->score); show_string(dis_buf, b->x, b->y - 20, score_str, COLOR_RED); } if (boxes) { free(boxes); } }3.3 SD卡照片加载与人脸比对思路
把检测到的人脸框从摄像头画面里裁剪出来后,接下来是“识别”环节。K210的KPU支持跑人脸识别模型,比如MobileFaceNet等,但如果你用的是入门级的K210,模型体积和内存都得精打细算,加上要对SD卡里任意一张照片做实时比对,直接跑深度学习特征向量提取模型会非常吃力。
所以我采用了一个工程上足够用的折中方案:将摄像头捕获的人脸区域缩放成32x32灰度图,和SD卡中每张注册照片也缩放成同样尺寸的灰度图,然后计算结构相似度(SSIM)或者归一化相关系数(NCC),设定一个经验阈值来判断是否匹配。这个方法不能说有多先进,但在门禁这种相对固定的场景、光照可控的前提下,识别率实际测试是够用的,而且对K210的算力要求很低,出结果非常快。
SD卡照片加载的核心代码:
#define FACE_IM_SIZE 32 typedef struct { float features[FACE_IM_SIZE * FACE_IM_SIZE]; char name[32]; } reg_face_t; static reg_face_t reg_faces[32]; static int reg_face_count = 0; static int load_registered_faces(void) { DIR dir; FILINFO fno; FRESULT res; FIL file; uint8_t *jpg_buf; uint32_t jpg_size; image_t *img; uint8_t gray_buf[FACE_IM_SIZE * FACE_IM_SIZE]; res = f_opendir(&dir, "0:/faces"); if (res != FR_OK) { return -1; } reg_face_count = 0; for (;;) { res = f_readdir(&dir, &fno); if (res != FR_OK || fno.fname[0] == 0) { break; } if (fno.fattrib & AM_DIR) { continue; } char *dot = strrchr(fno.fname, '.'); if (dot == NULL || strcasecmp(dot + 1, "jpg") != 0) { continue; } char path[64]; snprintf(path, sizeof(path), "0:/faces/%s", fno.fname); res = f_open(&file, path, FA_READ); if (res != FR_OK) { continue; } jpg_size = f_size(&file); jpg_buf = (uint8_t *)malloc(jpg_size); UINT br; res = f_read(&file, jpg_buf, jpg_size, &br); f_close(&file); if (res != FR_OK || br != jpg_size) { free(jpg_buf); continue; } img = image_init(jpg_buf, jpg_size); if (img == NULL) { free(jpg_buf); continue; } // 裁剪为正方形并缩放 int side = img->w < img->h ? img->w : img->h; image_resize_area(img, gray_buf, FACE_IM_SIZE, FACE_IM_SIZE, 0, 0, side, side); memcpy(reg_faces[reg_face_count].features, gray_buf, sizeof(gray_buf)); strncpy(reg_faces[reg_face_count].name, fno.fname, 31); reg_faces[reg_face_count].name[31] = 0; reg_face_count++; image_destroy(img); free(jpg_buf); } f_closedir(&dir); printf("Registered faces count: %d\n", reg_face_count); return reg_face_count; }image_resize_area这个函数是K210 SDK里自带的一个图像处理接口,可以直接把原始图像缩放到指定尺寸并转成灰度。它内部用的是双线性插值,质量比简单抽点好不少,人脸特征保真度更高,比对结果也更稳定。注意这里image_init的输入是JPEG数据流,K210 SDK自带JPEG解码器,性能虽然比硬解差点,但逐张处理注册图片完全够用。
比对时我用的是结构相似度,具体实现不贴全部代码了,只说阈值。SSIM的值范围是0到1,同一个人不同角度、不同光照下大概能到0.65~0.85;不同人的脸基本在0.4以下。所以我的判断逻辑是:大于0.75直接判定为同一个人,0.6~0.75的区间标记为“待确认”,需要连续两帧比对都通过才放行,低于0.6直接拒绝。这个策略在实测中把误识率压得比较低,代价是识别速度稍有下降,但对门禁场景来说稳定性优先。
3.4 UART通信协议实现
K210和STM32之间靠串口通信,通信协议一定要事先约定好,不然两边代码写完了一联调就到处是问题。我这里设计的是最简单的帧格式,但包含了帧头、长度、命令、数据和校验,足够应对这个项目的需求:
typedef enum { CMD_NONE = 0x00, CMD_HELLO = 0x01, CMD_FACE_MATCH = 0x10, CMD_FACE_NO_MATCH = 0x11, CMD_FACE_UNKNOWN = 0x12, CMD_REGISTER_USER = 0x20, CMD_DELETE_USER = 0x21, CMD_GET_LOG = 0x30, } protocol_cmd_t; #define FRAME_HEADER1 0xAA #define FRAME_HEADER2 0x55 #define FRAME_MAX_LEN 128 typedef struct { uint8_t header1; uint8_t header2; uint8_t cmd; uint8_t data_len; uint8_t data[FRAME_MAX_LEN]; uint8_t checksum; } protocol_frame_t;发送一帧数据的实现:
static void send_frame(uint8_t cmd, const uint8_t *data, uint8_t data_len) { uint8_t buf[FRAME_MAX_LEN + 6]; uint8_t idx = 0; uint8_t checksum = 0; buf[idx++] = FRAME_HEADER1; buf[idx++] = FRAME_HEADER2; buf[idx++] = cmd; buf[idx++] = data_len; for (int i = 0; i < data_len; i++) { buf[idx++] = data[i]; checksum += data[i]; } checksum += cmd + data_len; buf[idx++] = checksum; buf[idx++] = 0x0D; // 帧尾回车符,便于调试 uart_send_data(UART_DEVICE_1, buf, idx); }STM32侧收到0xAA 0x55后开始解析,校验通过后按cmd执行对应操作。识别通过时K210发送的data部分包含人员ID和相似度评分,STM32收到后拉高继电器3秒执行开门动作,同时蜂鸣器滴一声提示;识别不通过时发送FACE_NO_MATCH,STM32保持门锁状态,并可通过串口回传“验证失败,请重试”给上位机。这样一来,K210和STM32各管一摊,通信内容非常克制,两侧代码都不复杂。
4. STM32侧的配合要点与通信保障
4.1 门禁业务逻辑放在STM32上的优势
STM32在这个系统里负责三件核心事:接收K210的串口识别结果并做决策、控制继电器和指示灯等外设、维护系统运行状态的超时守护。简单说,K210只回答一个问题——“这是不是已注册的人”,至于这个人能不能进、什么时候能进、共进来了多少次,都由STM32说了算。
这套分工方式的好处在于,后续如果你想扩展指纹识别模块或者密码键盘,完全不需要动K210的代码,只在STM32侧增加对应驱动和判断逻辑即可。K210依然是那个专注视觉的“眼睛”,而STM32升级成更强大的“大脑”。工程上模块边界越清晰,后期维护成本越低。
STM32侧的串口中断解析代码,需要保证接收不丢字节,建议使用DMA加空闲中断的方式。轮询方式虽然代码简单,但如果你还开着LCD刷新、按键扫描、继电器控制这些逻辑,很容易出现串口数据覆盖丢失。我贴一段DMA接收的基础配置思路供参考:
// 初始化UART1_RX的DMA接收 void uart1_dma_init(uint8_t *rx_buf, uint16_t len) { // 使能USART1接收DMA USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); // 配置DMA通道 DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = len; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel5, &DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); }DMA接收要注意K210发送的数据帧长度不固定,所以不能依赖固定长度DMA完成中断来判断收到完整帧,必须结合串口空闲中断来做。STM32的USART_IT_IDLE触发时,说明一帧数据已经传输完毕,此时从DMA缓冲区里取出数据解析,然后重新配置DMA接收下一次数据。我在代码里预处理了帧头和帧尾,解析失败时直接丢弃整个缓冲区,避免半帧数据导致的错位污染下一帧。
4.2 通信异常兜底与看门狗联动
串口通信再稳定也难免有极端情况,K210程序如果发生硬件异常或者死循环,STM32可能一直收不到有效识别帧。所以我在STM32侧加了一个超时看门狗逻辑:每次收到合法数据帧就清零计数,如果连续超过5秒没有任何合法帧,视为K210异常,STM32控制蜂鸣器周期性报警,并点亮系统故障指示灯。这个设计在长时间运行测试中确实多次派上用场,有一次K210因为SD卡松动卡死,STM32这边立即报了警,不用拿着万用表去找故障点。
还有一个经常被新手忽略的细节:K210和STM32之间的电平参考地一定要连好。如果只接TX、RX两根线而没有共地,串口数据在收发两端会出现随机错位、乱码,甚至出现偶发的正确帧而后又频繁错误的“灵异现象”。这种问题有时候很难排查,因为万用表量TX和RX引脚的电压都是正常的,但通信就是不稳定,接上共地线后立即恢复正常。别问我是怎么知道的。
5. 常见问题与排查实录
5.1 SD卡挂载失败问题
这是我调试阶段遇到最多的问题,也是新手最容易栽的坑。主要现象是f_mount返回FR_NOT_READY或FR_DISK_ERR,原因基本有三种:
第一,SD卡初始化时序问题。K210的SD卡驱动遇到有些低速卡或者老旧卡,SPI波特率太快会导致初始化失败。解决办法是在sd_init里把SPI时钟先降到2MHz以下,初始化成功了再把速率提高。这个调整直接在SD卡驱动层的spi_set_speed里改即可。
第二,卡格式不对。K210的SDK中FATFS虽然支持多重分配单元大小,但最常见的还是FAT32格式。用Windows格式化时选择默认的FAT32就行,不要选择exFAT,也不要选NTFS。有次我从旧手机里拆了一张64GB的卡,格式化时Windows默认给了exFAT,结果K210怎么都挂载不上,换回FAT32立刻正常。
第三,卡槽接触不良。Maix Bit板载的Micro SD卡槽做工一般,卡片插入后其实没有真正卡到位,稍微晃动一下接触就松了。排查办法很简单,插卡后用手轻轻压一下卡面,然后在代码里反复打印f_mount的返回值看是否稳定。建议买一张质量好一点的Class 10高速卡,能减少很多供电和时序上的诡异问题,比如读取中断、数据损坏等。
另外,FATFS的f_mount不是每次挂载都成功,建议在初始化时做一个重试机制:
int mount_retry(void) { for (int i = 0; i < 3; i++) { FRESULT res = f_mount(&fs, "0:", 1); if (res == FR_OK) { return 0; } printf("Mount retry %d failed, res=%d\n", i + 1, res); sleep(500); } return -1; }实测在劣质卡片上这个重试机制能明显提高上电成功率。但根本上还是建议换卡,一张跑得稳的卡能省掉你后面所有调试时间。
5.2 摄像头黑屏或花屏
这个问题需要分阶段判断。如果LCD上完全黑屏,先检查摄像头排线是否插紧、方向是否正确。如果LCD有画面但花屏,多半是DVP的引脚复用没配对,或者是SPI时钟频率过高导致LCD刷新异常。
我用官方例程初始化摄像头时,默认的DVP时钟为500kHz,但屏幕初始化和画面刷新对SPI频率有要求。如果LCD画面出现颜色错乱或者横纹,尝试在配置SPI/LCD频率时把spi_set_clk_rate调整为20MHz左右再试。不要一味追求高刷,K210的CPU还在跑KPU,总线带宽有限,频率越高越容易出现花屏。
还有一个很隐蔽的点:K210同时进行JPEG解码和LCD显示时,内存带宽会非常紧张。如果你在代码里开启了AES、SHA等硬件加速且占用了DMA通道,可能导致DVP采集DMA和LCD刷新DMA冲突,表现形式是摄像头图像间歇性花屏或者画面卡住。处理方法就是这类外设不要在推理循环中同时开启,或者调整DMA优先级,给DVP采集设置最高优先级。
5.3 人脸识别率偏低或漏检
识别率这个指标,一看算法,二看图像质量,三看使用场景。K210内置的YOLOv2人脸检测模型对近距离、正面人脸非常敏感,但在强逆光、侧脸、遮挡物超过30%的情况下,漏检和误检率会明显上升。
实际调试时,最有效提升识别率的方法是先把摄像头焦距调好。OV2640镜头出厂焦距适合50cm到无穷远,但门禁场景人脸距离大约在30~60cm之间,需要手动旋转镜头座微调焦距,调到LCD画面中人脸边缘锐利为止。这步调好了,识别率能提升30%以上,属于零成本优化,效果立竿见影。
另外,环境光照的影响比很多人想象得大。同一张已注册照片,在早上自然光和晚上室内灯下,SSIM值可能相差0.2以上。我实际测试中,注册照片最好在自然光照均匀的正脸条件下采集,并且采集后截图检查一下人脸区域是否曝光过度或欠曝,不达标的重新采集。如果场景光照变化剧烈,考虑在摄像头前加装红外补光灯板或者通过软件做直方图均衡化再做比对。
5.4 STM32收到的识别结果乱码或反复无规律变化
最后一个高频问题是K210和STM32之间的串口通信不稳定。除了上面提到的共地问题之外,还有一个坑就是波特率匹配。K210的UART外设使用FPIOA引脚复用,理论上任意GMPU引脚都能映射到UART功能,但不同引脚内部的走线长度、驱动能力不同,实测波特率上限差异不小。我在IO10、IO11上跑115200稳定,但换到IO15、IO16之后,115200就频繁丢帧,降到57600才恢复,所以如果换了引脚出现乱码,先不要怀疑代码,试着降低波特率或者换回默认引脚。
另外,不要在K210的UART发送函数里直接调用printf调试,尤其是高频循环里。printf的格式化输出会占用大量CPU时间,可能阻塞KPU推理主循环,同时两个发送源同时往USART的DMA写数据,数据会交错,STM32那边收到的帧校验自然绝大部分不过。正确做法是调试信息单独用一组IO口通过LED或者LCD显示,或者加一个开关量只在调试模式下才开启printf。
STM32侧解析时还有一个细节,K210的UART发送是高位在前,STM32解析时也要按同样的顺序。我自己在写STM32接收解析时,一开始按习惯判定帧尾回车符0x0D,结果K210那边发送完成后刚好紧跟着下一帧的帧头0xAA,导致解析错位。后来改成“找到帧头后计数,帧内长度字节约束,最后校验和一致才采纳”,问题就再也没出现过了。推荐你也直接按协议校验的方式来解析,不要把简单的回车符当成帧结束标志。
6. 完整实现方案串联与效果展示
硬件上电后,整个系统的工作流程是这样的:
系统启动,K210先初始化摄像头、LCD、UART、SD卡,挂载SD卡,读取faces目录下的注册照片并构建比对库,加载KP模型,屏幕显示“System Ready”。此时STM32也已初始化完成,通过串口向K210发送CMD_HELLO握手请求,K210收到后返回确认包,双方建立通信。
循环主逻辑中,K210持续采集摄像头画面,LCD实时预览,KPU每帧推理一次人脸检测。当检测到人脸时,裁剪人脸区域图像并统一缩放到32x32灰度图,与SD卡注册库里每一张照片的灰度特征做SSIM比对,取分数最高者。如果最高分超过0.75,判断为已注册用户,发送CMD_FACE_MATCH并附上用户ID和分数,LCD上画绿框并显示用户名;如果分数在0.6到0.75之间,发CMD_FACE_UNKNOWN通知STM32进入二次确认模式;低于0.6则发CMD_FACE_NO_MATCH,LCD上画红框显示“Unknown”。
STM32收到匹配成功指令后,拉高继电器驱动电磁锁或门舵机,开门3秒后自动复位,同时蜂鸣器发出短促提示音;匹配失败则点亮红色提示灯,并在LCD副屏(如果接了)显示失败原因。整个识别到开门的流程,实测在320x240分辨率下从KPU推理到串口指令发出,大约耗时300~500ms,属于可以接受的实时性范围。
日志方面,每次事件都会追加写入SD卡的logs/access.log,内容包括用户ID、比对分数、当前帧时间戳。开发测试跑了一整天,日志文件累计几百行,文件系统读写稳定,没有出现数据错乱或文件损坏的问题。这个排障与审计能力,是纯K210方案很难做到的。
后续扩展方向我目前已经在尝试两个点:一个是换用更强的K510或者K230,用更深的模型做端到端人脸特征提取,替换当前灰度相关比对方案,识别精度会再次提升;另一个是在STM32侧加一个ESP8266模块,把出入记录通过MQTT上报到局域网服务器,这样手机端随时可以查看门禁记录,产品化程度就又上了一个台阶。
最后分享一点个人心得。这个项目真正做下来,我最大的收获不是“K210会跑人脸识别了”,而是理解了为什么产品设计里都讲究“模块职责单一”。K210专心做视觉,STM32专心做控制,SD卡只用文件系统做存储,三者通过极其简单的串口协议协作,整个系统的稳定性和可调试性都高得惊人。调试过程中出问题的点几乎都被隔离在单一模块内,不太会出现这边改了一行,那边莫名崩溃的连锁反应。
如果你正在做类似的边缘视觉加MCU控制的毕业设计或者产品原型,我很推荐按照这个思路来搭框架。不要为了显得高大上而强行在单板上堆所有功能,硬件资源够不够还是其次,调试和迭代的效率才是最影响项目成败的隐形指标。代码细节上,SD卡用FATFS文件系统做注册库管理、K210和STM32之间用带校验的帧协议通信、KPU推理结果用SSIM做二次判定,这三个设计是我反复验证过稳定性最靠谱的组合,可以直接照着抄。