PROJECT

便利店实时库存管理系统

毕业设计答辩已完成,现作技术文档公开发布。STM32F407 + CanMV K230 双核边端架构,覆盖嵌入式拼音输入法、OCR 联调、RFID 货架定位与销售分析——所有模块的实现细节与踩坑记录全在这里。

TYPE
毕业设计(已答辩结题)
STACK
STM32F407ZGT6 · CanMV K230 V3P0 · Keil MDK (C) · MicroPython · FatFS · USB HID · RFID UHF 915MHz
ROLE
独立开发、毕业设计
UPDATED
2026/05/13
  • #嵌入式
  • #STM32
  • #K230
  • #OCR
  • #毕业设计
  • #拼音输入法
  • #技术文档

本文档为毕业设计项目的综合技术文档,答辩已完成,现作公开技术参考资料发布,供有相似开发需求的朋友参阅。文档覆盖完整的系统架构、各模块实现、Keil 工程配置与踩坑记录。

📄 完整 PDF 版本点击下载


一、项目概述

本项目是一套基于 STM32F407 + CanMV K230 的便利店库存管理系统,采用「边端协同」架构:

  • STM32F407:系统主控,负责全部 UI 交互、业务逻辑与外设驱动,Keil MDK 构建,单次烧录,除 K230 外无需额外烧写任何外设固件
  • CanMV K230:AI 协处理器,运行 MicroPython,负责 OCR 推理与销售数据统计分析,通过 UART 与 STM32 通信
层级承载硬件主要职责
感知层RFID YND-R200 / 条码扫描仪 ADL615H / K230 摄像头货架定位、商品扫码结账、包装文字 OCR 识别
计算层CanMV K230(NPU 6TOPS)OCR 推理、销售数据统计分析,结果通过 UART 发回主控
交互层STM32F407 + 4.3" 480×800 电容触摸屏全部 UI 渲染、触控交互、数据读写、业务逻辑
存储层SD 卡(FAT32)+ W25Q128 Flash商品数据 CSV、账单 TXT、拼音词库、GBK 字库

系统通信拓扑

K230 V3P0 (MicroPython) — 双模式状态机
  模式1:摄像头 → OCR 识别 → UART2 发送 [F]/[R] 结果
  模式2:接收账单数据 → 统计分析 → UART2 发送 [S] 结果

      ↕ UART2 (TX=Pin5, RX=Pin6) 115200bps 纯文本行协议

STM32F407 (C,Keil 构建,单次烧写)
  USART3  ← K230(OCR 结果 / 销售分析结果)→ 入库界面 / 销售分析界面
  USART2  ↔ RFID YND-R200(UHF 915MHz)
  USART6  ↔ 条码扫描仪 ADL615H
  USB OTG_FS ← USB HID 键盘(任意型号,即插即用)
  480×800 电容触摸屏 + SD 卡(可拔插)+ W25Q128 Flash 字库

核心技术亮点

技术点难度实现说明
嵌入式中文拼音输入法★★★★★在无 OS、无标准堆的 MCU 上完整实现中文输入。移植第三方拼音引擎,将 1.16MB JSON 词库重设计为自定义二进制格式,查找耗时从秒级卡死压缩至 <20ms
K230 OCR 多模态录入★★★★☆K230 运行 DBNet+CRNN 两阶段 OCR,20帧投票去噪,识别结果以 UTF-8 字符串通过 UART 发送至 STM32,STM32 用 ff_convert(uni, 1) 转 GBK 显示
RFID 货架级定位★★★☆☆UHF 915MHz RFID,RSSI 原始值映射为 5 级信号强度,蜂鸣器节拍随距离变化,引导操作者靠近目标货架
非阻塞多任务 UI★★★☆☆裸机环境下全部定时/状态逻辑以非阻塞状态机实现(蜂鸣器、条码超时、扫描重触发),主循环无 delay 阻塞
销售分析(Holt-Winters)★★★☆☆K230 运行 HW 三参数指数平滑预测未来7天销量,STM32 渲染柱状图;K230 离线时退化为本地简化计算
SD 卡数据全持久化★★☆☆☆商品数据、出库账单、操作日志均存 SD 卡文本文件,PC 端可直接读取,无需专用上位机软件

功能模块完成情况

功能模块状态简述
入库管理✅ 完成实测拼音录入 + 条码扫描 + K230 OCR 三种方式录入商品信息
出库/收银✅ 完成实测条码扫码结账,库存实时扣减,账单 TXT 自动存盘
仓库定位✅ 完成实测RFID 信号强度引导 + 蜂鸣节拍,定位到货架
库存总览✅ 完成实测全商品分页浏览,支持在屏幕端直接修改库存数量
系统设置✅ 完成实测设备信息、接入状态实时检测、接口说明、存储用量
销售分析✅ 完成实测K230 统计运算,STM32 渲染柱状图,K230 离线时切换本地简化计算
多管理员权限✅ 完成实测主管授权多管理员,操作全程留档,账单与日志均加时间戳和操作人

二、硬件配置

元件清单

编号元件名称型号 / 规格连接方式用途
主控开发板STM32F407ZGT6 探索者核心板系统主控,负责全部 UI 交互与功能集成
触摸显示屏ATK-4.3" TFTLCD 480×800 竖屏FSMC 16-bit 并行人机交互主界面,电容多点触控(FT5206)
RFID 读写器YND-R200 UHF 915MHzUSART2 PA2/PA3 115200bps仓库货架箱级定位,实测读距 >4m
AI 协处理器CanMV K230 V3P0 NPU 6TOPS 1GB LPDDR4USART3 PB10/PB11 115200bpsOCR 文字识别 + 销售分析,结果通过 UART 发回
条码扫描仪ADL615H(滴滴解码 T165)USART6 PC6/PC7 9600bps出库结账扫码,支持命令触发与模式切换
Flash 存储W25Q128 128Mbit SPI NOR FlashSPI1 PB3/PB4/PB5GBK 中文字库,FatFS 挂载为 "1:"
SD 卡标准 microSD,FAT32(用户自备)SDIO 4-bit PC8-12/PD2商品数据、拼音词库、账单记录
USB 键盘任意 USB HID 标准键盘USB OTG_FS PA11/PA12中文拼音录入辅助,支持热插拔
板载蜂鸣器正点原子探索者板载无源蜂鸣器GPIO PF8 高电平有效仓库定位信号引导,节拍随距离变化
RTC 实时时钟DS3231 MK002804 高精度 ±2ppm软件 I2C PC0(SCL)/PC1(SDA) 开漏上拉账单/日志时间戳,ui_set_time.c 手动校时

RFID 方案说明:标签贴在货箱/货架上,而非为每件单品单独贴标。单品级方案成本极高,且 UHF 读写器在密集货架环境下群读容易超出处理上限。货架级方案大幅降低标签成本,定位逻辑也更清晰可控。

接口与接线详表

接口连接硬件STM32 引脚通信配置备注
USART1调试串口(PC)PA9(TX) / PA10(RX)115200bps 8N1调试用,不接外设不影响运行
USART2RFID YND-R200PA2(TX) / PA3(RX)115200bps 8N1中断接收,RFID_DataReceive()
USART3K230 OCR 模块PB10(TX) / PB11(RX)115200bps 8N1中断接收,OCR_UART_IRQHandler();同时处理 OCR 及销售分析回传
USART6条码扫描仪 ADL615HPC6(RX) / PC7(TX)9600bps 8N1指令控制扫描触发/模式切换
SDIOSD 卡(4-bit)PC8-12 / PD2SDIO 4-bitFAT32,挂载为 "0:"
SPI1W25Q128 FlashPB3(SCK)/PB4(MISO)/PB5(MOSI)SPI 模式0FAT32,挂载为 "1:"
USB OTG_FSUSB HID 键盘PA11(D-) / PA12(D+)USB 2.0 FS任意 HID 键盘,热插拔
I2C / CTIIC触摸控制器 FT5206PB6(SCL) / PB7(SDA)I2C支持 GT9147 / OTT2001A
FSMCLCD SSD1963PD/PE,NE4 片选,A6=RS16-bit 并行480×800,60Hz
软件 I2CDS3231 RTCPC0(SCL) / PC1(SDA)软件模拟 I2C ~50kHz开漏上拉DS3231_Init()
GPIO蜂鸣器(板载)PF8高电平有效仓库定位信号引导

K230 ↔ STM32 接线

K230  Pin5 (UART2_TX)  →  STM32  PB11 (USART3_RX)
K230  Pin6 (UART2_RX)  ←  STM32  PB10 (USART3_TX)
K230  GND              ↔  STM32  GND

K230 所有 IO 均为 3.3V,STM32F407 IO 也是 3.3V,无需电平转换,直连即可


三、工程结构

STM32 侧(Keil)

TOUCH/
├── USER/          main.c / stm32f4xx_it.c / system_stm32f4xx.c
├── HARDWARE/      led / lcd / key / touch / spi / w25qxx / sdio_sdcard
│                  myiic / 24cxx / ctiic / gt9147 / ott2001a / ft5206
│                  usb_kbd.c
├── SYSTEM/        delay.c / sys.c / usart.c
├── CORE/          startup_stm32f40_41xxx.s
├── FWLIB/         misc / gpio / fsmc / rcc / syscfg / usart / spi / sdio / dma
├── MALLOC/        malloc.c
├── FATFS/         exfuns.c / diskio.c / ff.c / mycc936.c
├── TEXT/          fontupd.c / text.c
├── SD/            sd_items.c
├── RFID/          rfid.c
├── BARCODE_UART/  barcode_uart.c
├── OCR_UART/      ocr_uart.c
├── IME/           ime_pinyin.c / zh_pinyin_decoder.c / zh_code_table.c
│                  zh_hash_boost.c / zh_word_bin.c / cJSON.c
├── ADMIN/         admin.c / log.c
├── DS3231/        ds3231.c
├── USB/           USB_OTG / USB_HOST / USB_APP
└── UI/
    ├── ui_main_menu.c
    ├── ui_rfid_locate.c
    ├── ui_inbound.c / ui_inbound_item.c / ui_inbound_rfid.c
    ├── ui_inbound_barcode.c / ui_inbound_delete.c
    ├── ui_checkout.c / ui_stock_overview.c
    ├── ui_settings.c / ui_set_time.c / ui_admin.c
    └── ui_sales.c

K230 侧(MicroPython)

/sdcard/
├── main.py              # 双模式状态机主程序
├── sales_analyze.py     # 销售分析引擎(HW预测 + 关联挖掘)
├── libs/
│   ├── PipeLine.py      # 摄像头+显示 Pipeline 封装
│   ├── AIBase.py        # AI 推理基类
│   └── AI2D.py          # 图像预处理加速
└── examples/utils/
    ├── ocr_det.kmodel   # DBNet 文字检测模型
    ├── ocr_rec.kmodel   # CRNN 文字识别模型
    └── dict_6625.txt    # OCR 识别字典

四、SD 卡文件布局与初始化

0:/
├── items.csv           ← 商品数据(GBK 编码,FAT32 格式)
├── zh_pinyin.bin       ← 单字字库(22KB,408 音节,7287 汉字)
├── zh_word_dict.bin    ← 词组二进制库(784KB,21003 词条)
├── zh_word_dict.json   ← 原始词典(备用)
├── BILLS/              ← 账单目录(需手动创建!)
│   └── BILL_YYYYMMDD_NNN.TXT
└── LOGS/               ← 操作日志(系统自动创建)
    └── log.csv

items.csv 格式(8 列,GBK 编码)

编号, 名称, 详情, 单价, EPC, 区域, 货架, 数量
A001,矿泉水,农夫山泉 550ml,2.5,E28069150000401B9661B941E21F,A,A-01,48
A002,纯净水,怡宝 550ml,1.5,,A,A-01,36

注意:EPC 列允许为空(连续逗号),空 EPC 商品无法 RFID 定位,正常显示。文件必须用 Windows 记事本另存为 ANSI(GBK)编码,UTF-8 会导致中文乱码。

首次初始化 SD 卡步骤

  1. 将 SD 卡格式化为 FAT32(不能是 exFAT 或 NTFS)
  2. 手动建好文件夹结构(系统不会自动创建目录):BILLS/IMG/LOGS/
  3. items.csvzh_pinyin.binzh_word_dict.bin 复制到 SD 卡根目录
  4. 插入开发板,上电即可使用

五、Keil 工程配置

5.1 Define 宏

STM32F40_41xxx,USE_STDPERIPH_DRIVER,USE_USB_OTG_FS

5.2 C99 Mode

必须勾选zh_pinyin_decoder.c 使用 C99 语法,ARMCC V5 默认 C89 会报 30+ 个错误。

路径:Keil → Options → C/C++ → 勾选 C99 Mode

5.3 SRAM 溢出与 UI 架构重构(重要工程事件 · 2026-03-08)

这是本项目经历的最重要的一次工程事件。当 ui_main_menu.c 的 switch 中所有 case 从 TODO 占位改为实际函数调用后,链接器将三个模块(ui_checkout、ui_stock_overview、ui_settings)的所有全局/静态变量一并拉入 RAM,触发链接时 SRAM 溢出:

Error: L6406E: No space in execution regions with .ANY selector

根本原因:C 语言全局变量和 static 变量在编译时就分配固定地址,程序一启动全部同时占用内存,不管对应界面是否被打开。


第一步:扩大 IRAM1

STM32F407 有 SRAM1(112KB)+ SRAM2(16KB)物理连续,但 Keil 默认只配了 112KB。

路径:Keil → Options for Target → Target 选项卡,将 IRAM1 Size 从 0x1C000 改为 0x20000(128KB)

⚠️ 不要同时加 IRAM2Start: 0x10000000 Size: 0x10000)。malloc.c 已用 __attribute__((at(0x1000F000))) 把 CCM 内存池硬编码在该地址,链接器同时往那里塞 system_stm32f4xx.o,两者撞地址,报 Error: L6375E。正确做法:清空 IRAM2,只保留 IRAM1 = 0x20000,CCM 由 malloc.c 自己管理。


第二步:UI 架构重构——动态内存模式

仅扩大 IRAM1 是治标。根本解法是将所有界面的静态数组收进 XxxCtx_t 结构体,进入界面时 mymalloc 一次分配,退出时 myfree 释放。同一时刻内存里只有当前运行界面的数据。

后续新增界面必须遵守此规范,禁止在文件顶部声明大型静态数组。

文件改动节省静态 RAM
ui_settings.cs_rows[40] 等打包进 SettCtx_t,动态分配~2.9 KB
ui_stock_overview.cview_idx[128]、zone_list 等打包进 StockCtx_t~165 B
ui_inbound_item.cfield_buf[8][48]IME_t ime 等打包进 ItemCtx_t~0.8 KB+
ui_checkout.cscan_state 等改成局部变量或 file-static~40 B

5.4 main.c 初始化顺序

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init(168);
uart_init(115200);
LED_Init();  LCD_Init();  KEY_Init();
W25QXX_Init();
my_mem_init(SRAMIN);  my_mem_init(SRAMCCM);
exfuns_init();
f_mount(fs[0], "0:", 1);   // SD 卡(含 IME 词库)
f_mount(fs[1], "1:", 1);   // Flash 字库
if(font_init() != 0) { LCD_Clear(WHITE); while(1); }
tp_dev.init();
SD_Items_Load();
RFID_Init();
USB_KBD_Init();
OCR_UART_Init(115200);   // K230 通信 USART3
Admin_Init();            // 账号初始化,从 Flash 1:/ADMIN/admin.csv 加载
DS3231_Init();           // RTC 初始化,OSF=1 时自动写入编译时间
Main_Menu_Loop();        // 不会返回

5.5 stm32f4xx_it.c 必须包含

// RFID 中断
extern void RFID_DataReceive(u8 data);
void USART2_IRQHandler(void) {
    if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
        RFID_DataReceive((u8)USART_ReceiveData(USART2));
}

// K230 OCR 通信中断
void USART3_IRQHandler(void) {
    OCR_UART_IRQHandler();
}

// OTG_FS_IRQHandler 在 usbh_usr.c 里定义,不要在此重复定义

六、各模块详解

6.1 SD 卡商品数据模块(SD/sd_items.c

typedef struct {
    char id[8];       // 编号,如 A001
    char name[20];    // 名称(GBK)
    char detail[48];  // 详情描述
    char price[8];    // 单价字符串
    u8   epc[14];     // RFID EPC,全零表示未录入
    char zone[4];     // 区域
    char shelf[6];    // 货架号
    u8   qty;         // 库存数量
    u8   valid;       // 1=有效行
} SD_Item_t;

核心 API

u8   SD_Items_Load(void);    // 0=成功 1=内存不足 2=文件打开失败
void SD_Items_Free(void);
void SD_Items_SaveAll(void); // 将修改写回 CSV

两步法加载:第一遍扫描统计行数,精确分配内存(mymalloc),第二遍重新读取解析。不使用 strtok(会跳过空字段),改用自定义 Get_CSV_Field() 按位置逐字符数逗号。


6.2 RFID 驱动模块(RFID/rfid.c

接线:RFID TXD → PA3(USART2_RX),RFID RXD → PA2(USART2_TX)

回应帧格式(YND-R200 读卡成功)

AA 00 22 00 [lenH] [lenL] [RSSI] [PC0] [PC1] [EPC×14B] [checksum] DD

核心 API

void RFID_Init(void);              // USART2 初始化
void RFID_StartContinuous(void);   // 发送连续读取指令
void RFID_StopContinuous(void);    // 停止读取
void RFID_DataReceive(u8 data);    // 在 USART2_IRQHandler 中调用
void RFID_DataCheck(void);         // 主循环轮询,解析帧并置位 rfid_new_flag
extern RFID_Tag_t rfid_tag;        // 最新标签(epc[14] + rssi)
extern u8         rfid_new_flag;   // 1=有新数据,处理后置 0

⚠️ 模块上电后至少等 500ms 再发指令,否则指令被忽略。


6.3 USB 键盘模块(HARDWARE/usb_kbd.c

USB_Host 定义位置usb_kbd.c 顶部定义 USBH_HOST USB_Host;(非 extern),usbh_usr.c 顶部改为 extern USBH_HOST USB_Host;

每个使用键盘的界面 while(1) 必须首行调用:

while(1) {
    USB_KBD_Process();      // ★ 必须在最前面,推进 USB 状态机
    key = USB_KBD_GetChar();
    if(key) Process_Key(key);
    delay_ms(10);
}

6.4 拼音输入法引擎(核心难点 · HARDWARE/IME/

⚠️ 本模块是全项目技术难度最高的部分。在无 OS、无标准堆的 MCU 上实现完整中文拼音输入,涉及引擎移植、文件 IO 替换、内存分配适配、词库格式重设计四项核心攻克,任一步骤失败都会导致系统卡死。

架构

ui_inbound_item.c
    ↓
ime_pinyin.c / ime_pinyin.h        ← 适配层(v3.2)
    ├── ime_refresh_single()        ← 单字搜索(zh_match_code_vague)
    └── ime_refresh_word()          ← 词组搜索(zh_word_bin_search)
         ↓                               ↓
zh_pinyin_decoder.c           zh_word_bin.c
   zh_pinyin.bin(SD)            zh_word_dict.bin(SD)
         ↓
ime_pinyin.c 内置 utf8_to_gbk()   → Show_Str() 直接使用 GBK

词组二进制库性能对比

指标旧方案(JSON+cJSON)新方案(二进制)
文件大小1.16 MB784 KB
单次查找 IO扫描最多 6MB 数据~15×28B = 420B
动态内存分配大量(cJSON)0 次
STM32 实测耗时秒级(卡死< 20ms

新方案将词条按 key 升序排列并构建紧凑的定长索引(每条 28 字节),利用二分查找定位目标词条,最坏情况仅需约 15 次磁盘寻址(每次读 28 字节),IO 总量从「扫描最多 6MB」压缩至约 420 字节,且全程零动态内存分配

对外 API

void    IME_Reset(IME_t *ime);
uint8_t IME_InputLetter(IME_t *ime, char ch);
uint8_t IME_Backspace(IME_t *ime);
uint8_t IME_PageNext(IME_t *ime);
uint8_t IME_PagePrev(IME_t *ime);
const char *IME_GetCand(const IME_t *ime, uint8_t slot);
uint8_t IME_Select(IME_t *ime, uint8_t slot, char *out);
uint8_t IME_Tick(IME_t *ime);  // UI 循环每帧调用,0.5s 无输入触发词组搜索

⚠️ ui_inbound_item.c 中有一处 ime.cands[i] 为旧字段,须改为 IME_GetCand(&ime, i),修完即可 0 error。

按键方案

英文模式拼音模式
Tab切换拼音切换英文
字母键直接上屏送入拼音缓冲
1~8直接上屏选第 1~8 候选
Space上屏空格选第一候选
Enter跳下一字段有拼音则原样上屏;否则跳字段
Backspace删末尾字符先退拼音字母;拼音空时退已上屏汉字
  • / =
上/下候选页

6.5 条码扫描模块(BARCODE/barcode_uart.c

硬件:ADL615H,USART6(PC6=RX,PC7=TX),9600bps 8N1

工作模式

寄存器值照明声音用途
0xD5仅扫码时亮有声默认模式(条码录入页)
0xE9常亮有声收银台常态扫描专用
void Barcode_Trigger(void);          // 触发一次扫描
void Barcode_Standby(void);          // 清缓冲
void Barcode_EnterCheckout(void);    // 切 0xE9 并立即触发(收银台专用)
void Barcode_ExitCheckout(void);     // 恢复 0xD5(退出收银台时调用)
u8   Barcode_GetCode(char *out);     // 非阻塞,有码返回 1
void Barcode_Clear(void);            // 清接收缓冲

6.6 K230 OCR 通信模块(OCR_UART/ocr_uart.c

通信帧协议

方向内容格式
STM32 → K230开始 OCR 识别OCR\n
STM32 → K230开始销售分析SALES\n
STM32 → K230停止STOP\n必须带换行
K230 → STM32单帧结果[F] 词条1 | 词条2
K230 → STM32投票最终结果[R] 词条1 | 词条2
K230 → STM32销售分析各类[S]RANK: / [S]SUMMARY: / [S]PRED1: / [S]RESTOCK1: / [S]PAIR: / [S]END

核心 API

void  OCR_UART_Init(u32 baud);       // USART3 初始化
void  OCR_UART_IRQHandler(void);     // 在 USART3_IRQHandler 中调用
void  OCR_SendCmd(const char *cmd);  // 发送指令
u8    OCR_GetResult(char *buf);      // 1=有新行,填入 buf(UART_LINE_MAX=512)

OCR 编码问题的最终解法

K230 MicroPython 固件不支持 .encode("gbk")Show_Str() 只认 GBK。三种方案的踩坑路径:

  1. 让 K230 直接发 GBK → 固件不支持,encode 异常后 except 分支再次崩溃,实际发出 UTF-8 或空帧
  2. 在中断上下文里做转换ff_convert() 在 ISR 里不稳定,FATFS 函数不应在中断里执行
  3. 最终方案 ✅:K230 始终发 UTF-8,STM32 在主循环 OCR_GetResult() 中调用 ff_convert(uni, 1) 完成 Unicode→GBK 转换

⚠️ ff_convert() 方向参数:dir=1 是 Unicode→GBKdir=0 是 GBK→Unicode(反向),ime_pinyin.c 里也有此错误,需一并修正。


6.7 DS3231 RTC 模块(HARDWARE/DS3231/ds3231.c

DS3231 是高精度 I2C 实时时钟,内置温度补偿晶振(TCXO),精度 ±2ppm(约±1分钟/年)。I2C 地址固定 0x68,寄存器 0x00~0x12 存秒/分/时(均 BCD 码)。

软件 I2C 配置

SCL = PC0    SDA = PC1   (避开 SPI2 占用的 PB12/PB13)
SCL:推挽输出(GPIO_OType_PP)
SDA:开漏输出 + 内部上拉(GPIO_OType_OD + GPIO_PuPd_UP)
SDA 读值:直接读 IDR 寄存器(无需切换方向)
IIC_Delay = delay_us(5)  → 时钟频率约 50kHz

⚠️ 关键坑:SDA 必须用开漏模式。DS3231 通过拉低 SDA 发 ACK;推挽模式下 STM32 强驱高电平,DS3231 无法拉低,ACK 永远收不到,读取返回全 0xFF

核心 API

void DS3231_Init(void);                    // 初始化,OSF=1 时写入编译时间
u8   DS3231_GetTime(DS3231_Time_t *t);     // 读取时间,0=成功
u8   DS3231_SetTime(DS3231_Time_t *t);     // 写入时间(BCD 自动转换)

typedef struct {
    u8 sec, min, hour, week, day, month;   // 均为十进制(驱动内部转 BCD)
    u16 year;                              // 完整年份,如 2026
} DS3231_Time_t;

初始化行为:启动时读 OSF 标志(寄存器 0x0F bit7),OSF=1 表示掉电过,自动将编译时间(__DATE__ __TIME__ 宏)写入 DS3231;OSF=0 则保留芯片现有时间,不覆盖。


七、各界面实现

7.1 仓库定位界面(UI/ui_rfid_locate.c

RSSI 信号强度映射

实测有效区间 0xB8(184,约3m外)到 0xDE(222,约30cm贴近),线性映射到 30%~100%:

pct = (rssi - 184) × 70 / 38 + 30

5格信号柱状图(格高递增 8/14/20/26/32 px)

百分比格数颜色
≥85%5格绿色 0x07E0
≥65%4格黄绿 0x87E0
≥50%3格黄色 0xFFE0
≥35%2格橙色 0xFD20
>0%1格红色 0xF800

蜂鸣器节拍表(非阻塞状态机,双变量 beep_phase + beep_counter,每 10ms tick)

RSSI 原始值响时长静音时长节奏
0xDE20ms20ms极快(已贴近)
0xD350ms50ms
0xC880ms170ms中速
0xC080ms420ms
0xB880ms920ms极慢(远距离)

7.2 入库管理界面(UI/ui_inbound_item.c

三种录入方式可在同一录入流程中混用,例如用 OCR 填入名称和详情,再手动填入单价和数量。

按键英文模式拼音模式
Tab切换拼音切换英文
字母直接上屏送入拼音缓冲
1-8直接上屏选候选字
Space上屏空格选第一候选
Enter跳下一字段有拼音则原样上屏;否则跳字段
Backspace删末尾字符先退拼音字母;拼音空时退已上屏汉字

OCR 词条面板(v3.5):K230 识别结果到达后显示词条面板,词条以 | 分隔,UTF-8→GBK 转换后显示。PRICE / QTY 字段禁止填入 OCR 内容,需手动输入。有未用词条时退出弹出确认框。


7.3 收银台/出库界面(UI/ui_checkout.c

使用 Barcode_EnterCheckout() 切换到 0xE9 照明常亮模式,retrig_timer 每 7 秒自动补发一次 Trigger,防止引擎超时停止。

结账执行顺序(顺序不可乱)

  1. 扣减 sd_items[ci].qty
  2. SD_Items_SaveAll() 写 CSV
  3. Save_Bill_TXT() 写账单 TXT(0:/BILLS/BILLxxxx.TXT
  4. 显示完成界面

账单格式示例:

==============================
  便利店出库账单  BILL_073
  时间:2026-03-28 21:14:00
  操作人:admin
==============================
01  方便面          x05    3.50    17.50
02  可乐            x05    3.50    17.50
03  纯净水          x04    1.50     6.00
------------------------------
合计:41.00 元   共 3 种商品
==============================

⚠️ SD 卡需手动建好各类文件夹,代码不会自动创建目录。账单编号最多 9999 个,超过后静默放弃。


7.4 系统设置界面(UI/ui_settings.c

布局(480×800 竖屏)

[  0 ~  60]  标题栏
[ 68 ~ 718]  内容区(650px,独立分页,不滑动)
[720 ~ 756]  翻页导航栏(< 上一页 | 页码 | 下一页 >)
[762 ~ 800]  状态栏

内容分区(共 7 节)

分节内容
设备信息系统名称、版本号(v1.0.0)、主控芯片、协处理器、编译时间(__DATE__ __TIME__
接入状态USB 键盘(实时检测)、SD 卡、RFID、条码扫描仪、触摸屏——每项显示绿/橙/红状态指示点
接口说明USART1-6、SDIO、USB Host、I2C、FSMC、SPI1、DS3231 软件I2C 逐一列出引脚和配置
存储SD 卡总容量、剩余空间、使用率(%)、商品记录条数
关于开发平台、FatFS R0.15 GBK CP936、输入方案、触控方案
时钟设置DS3231 当前时间实时显示;主管可进入 ui_set_time.c 逐字段调时
管理员设置三账号状态列表;点击任意行弹主管验证框,验证通过跳转 ui_admin.c

行类型

类型高度说明
RT_SECTION34px深色节标题行
RT_INFO46px左标签 + 右值
RT_STATUS46px左标签 + 彩色状态点 + 右值(绿=OK / 橙=WARN / 红=ERROR)
RT_TEXT82px长文本行

7.5 销售分析界面(UI/ui_sales.c

STM32 负责遍历 0:/BILLS/ 账单文件、聚合数据后通过 UART 发往 K230;K230 运行 sales_analyze.py 完成统计运算,结果逐行发回;STM32 解析后渲染三屏 UI

STM32 → K230 通信协议

SALES              → K230 进入销售分析模式,回应 SALES_READY
SALES:N            → 发送天数 N
D:id,d0,d1,…      → 每商品日销量序列
STOCK:id,qty,price_x100  → 库存与单价(整数×100 避免浮点格式化问题)
BILL:id1+id2       → 每张账单一行,按日期从旧到新
DONE               → 触发 K230 开始分析

K230 → STM32 回传协议

[S]RANK:id,qty,amt|…        销售排名(TOP 7)
[S]SUMMARY:tqty,amt,bills   汇总统计
[S]PRED1:id,v1,v2,…,v7     未来7天预测(每商品一行,最多5条)
[S]RESTOCK1:id,rate,days    补货建议(每商品一行)
[S]PAIR:idA+idB,cnt,pct|…  关联购买对
[S]END                       分析完成标志

UI 三屏展示

  • Tab 1 排名柱状图:TOP 7 商品,Y 轴自适应缩放(底部取最小值 70%,差异放大);AUX 区显示 3 条折线预测趋势(D1~D7 标签)
  • Tab 2 补货建议:每页 4 条,R/O/G 三级紧急度色条;AUX 区三色统计块 + 智能话术(断货预警、日均销售额)
  • Tab 3 关联分析:每页 3 条同购对;AUX 区 3 条智能进货建议(最佳搭档 + 断货预警 + 销量冠军)

时间筛选内存过滤:切换 7天 / 30天 / 全部时不重读 SD 卡collect_bill_data 首次加载时记录 s_bill_off_7s_bill_off_30 两个 u16 偏移量,refilter_from_memory() 直接对内存中的 daily[] 数组重新求和,瞬间完成切换


八、K230 开发技术说明

板型:CanMV K230 V3P0(NPU 6TOPS,512MB LPDDR4)

⚠️ V3P0 摄像头接口:V3P0 出厂摄像头物理接在 CSI2必须指定 id=2。不指定则黑屏/报错,看起来像摄像头坏了,实际只是接口号写错。

# ✅ 正确(V3P0 必须这样写)
pl.create(sensor=Sensor(id=2), vflip=True)

# ❌ 错误(官方默认 CSI0,V3P0 无效)
pl.create(sensor=Sensor())

K230 可用 UART

UART引脚状态
UART0Pin38/39被小核占用,不可用
UART3Pin50/51被大核占用,不可用
UART2Pin5(TX)/Pin6(RX)当前使用 ✅
UART1Pin3/Pin4可用
UART4Pin36/Pin37可用

8.1 双模式状态机(main.py

STATE_IDLE  = 0   # 待机,轮询 uart_readline()
STATE_OCR   = 1   # OCR 识别模式,Pipeline 运行中
STATE_SALES = 2   # 销售分析模式,接收数据并分析

while True:
    cmd = uart_readline()
    if state == STATE_IDLE:
        if cmd == "OCR":
            ocr_init(); state = STATE_OCR
            uart_send("OCR_READY"); ocr_loop()
        if cmd == "SALES":
            sales_clear(); state = STATE_SALES
            uart_send("SALES_READY"); sales_receive_loop()
    gc.collect(); utime.sleep_ms(50)

OCR 和销售分析完成后均自动回到 STATE_IDLE不会相互干扰。摄像头资源(Pipeline)只在 OCR 模式内初始化,退出时 destroy,销售分析模式全程不占用摄像头

8.2 OCR 两阶段推理(DBNet + CRNN)

OCRDetectionApp   DBNet int16 量化  640×640 输入
  → aicube.ocr_post_process()  → 文字区域 boxes

OCRRecognitionApp CRNN  int16 量化  512×32  输入
  → CTC 解码 → 文字字符串

OCRDetRec.run()   两阶段串联,逐 box 裁剪+识别

20帧投票去噪:最多同时追踪 MAX_SLOTS=4 个文字区域,每个 slot 收集 VOTE_FRAMES=20 帧结果,按相似度聚类(前缀匹配 SIM_PREFIX=4 字符 + 完整相等)后取最高票文本,置信度 ≥35% 才输出

Pipeline 配置(OCR 模式)

sensor_obj = Sensor(id=2)
sensor_obj.reset()
sensor_obj.set_hmirror(True)
pl = PipeLine(rgb888p_size=[640, 360], display_mode="lcd")  # 必须小写
pl.create(sensor=sensor_obj, vflip=True)

try:
    while state == STATE_OCR:
        img = pl.get_frame()        # ★ 必须持续消费帧,否则卡死
        det_res, rec_res = ocr_engine.run(img)
        pl.show_image()
        gc.collect()
finally:
    pl.destroy()                    # 退出时释放,下次 OCR 再重建

8.3 K230 UART 接口配置

from machine import UART, FPIOA
fpioa = FPIOA()
fpioa.set_function(5, FPIOA.UART2_TXD)   # Pin5=TX → STM32 PB11(USART3_RX)
fpioa.set_function(6, FPIOA.UART2_RXD)   # Pin6=RX ← STM32 PB10(USART3_TX)
uart = UART(UART.UART2, 115200)           # 不传 parity 参数(None 会崩溃重启)

n     = uart.any()
chunk = uart.read(n)                      # bytes 类型,需 decode
uart.write(("SALES_READY\n").encode("utf-8"))  # 必须带 \n,必须 encode

⚠️ parity=None 会导致 K230 崩溃重启,不要传这个参数。

8.4 STM32 ↔ K230 握手流程

── OCR 模式握手 ─────────────────────────────────
STM32 发送:OCR\n
K230  回应:OCR_READY\n
超时:3000ms,超时视为 K230 离线

── 销售分析模式握手 ──────────────────────────────
STM32 发送:SALES\n
K230  回应:SALES_READY\n
超时:3000ms,超时走离线分析

── 等待分析结果 ──────────────────────────────────
等待 [S]END:120000ms(120s)

8.5 销售分析四模块(sales_analyze.py

模块说明
rank_top()销量排名 TOP7(按总售量降序)
predict_all()Holt-Winters 三参数指数平滑预测未来7天(alpha=0.3 beta=0.1 gamma=0.2 season=7);数据不足14天退化为7天移动平均
compute_restock()补货优先级评分:日均 = 0.7×预测均值 + 0.3×历史均值;R: 库存<3天 / O: <7天 / G: ≥7天
find_pairs()Apriori 简化版关联购买挖掘,枚举所有商品对,过滤共现≥2次,输出 TOP5

[S]PRED[S]RESTOCK 单行必须控制在 50B 以内,避免 STM32 ring buffer 溢出——每条商品单独一行,每行发送后 sleep_ms(50) 防止 STM32 来不及处理。


九、踩坑全记录

IME 引擎移植

问题原因解决方法
zh_pinyin_decoder.c 30 个错误ARMCC V5 默认 C89Keil → C/C++ → 勾选 C99 Mode
zh_hash_boost.c 30 个错误&idx_xx 对数组再取地址,类型不匹配改为 idx_xx(数组名本身是指针)
NULL 未定义缺少 stddef.h#include <stddef.h>
__min 未定义ARMCC V5 无内置#define __min(a,b) ((a)<(b)?(a):(b))
feof 未定义移植时漏了替换改为 f_eof(&fp)
词组搜索卡死(秒级)JSON+cJSON 在 MCU 上性能极差重新设计为二进制格式,O(log n) 二分查找

K230 联调

问题原因解决方法
K230 崩溃重启parity=None 不支持不传 parity 参数
屏幕不亮display_mode="LCD" 大写不识别改为小写 "lcd"
get_display_size() 返回 NoneVIRT 模式下无显示尺寸改回 "lcd" 模式
K230 不自启文件直接拷到 TF 卡不生效用 IDE 菜单「工具 → 保存当前脚本为 main.py 到 CanMV Cam
待机时摄像头仍亮Pipeline 初始化后摄像头就启动收到 OCR\n 再初始化,收到 STOP\n 后 destroy

OCR 中文编码与转换

问题原因解决方法
STM32 收到中文显示乱码K230 发 UTF-8,Show_Str() 只认 GBKff_convert(uni, 1) 做 Unicode→GBK 转换
转换在中断里调用失败FATFS 函数不应在 ISR 里执行转换移到主循环 OCR_GetResult()
让 K230 直接发 GBK 仍乱码K230 固件不支持 .encode("gbk"),except 分支再次崩溃K230 始终发 UTF-8,转换全部交给 STM32
ff_convert() dir=0 结果错误dir=0 是 GBK→Unicode(反方向)改为 ff_convert(uni, 1)(dir=1 才是 Unicode→GBK)

DS3231 RTC 调试

问题原因解决方法
初始引脚 PB12/PB13 读取失败PB13 被板载 NRF24L01 的 SPI2_SCK 占用,I2C 总线持续被拉低改为 PC0(SCL)/ PC1(SDA)
改引脚后仍 ACK 无响应SDA 用推挽输出,STM32 强驱高电平,DS3231 无法拉低 SDASDA 改为开漏+内部上拉(OD + PuPd_UP),直接读 IDR

销售分析模块

问题原因解决方法
f_gets 永久挂死文件读取期间调用 Show_Str(含字库 SD 读取)打断 SDIO DMA文件读取循环内禁止所有 LCD 操作,动画刷新移至安全窗口
sscanf %f 解析金额返回 0ARMCC MicroLIB 下 sscanf %f 不工作改用 atof() 手动解析;价格乘 100 以整数传输,K230 收到后 /100.0 还原
[S]PRED 超行长导致 [S]END 被覆盖[S]PRED 最长 386B,远超 RING_LINE_SZ=160K230 拆为 [S]PRED1: 每商品一行,单行控制在 50B 内
USART3 ORE 后中断永久失效STM32F4 USART 经典坑:ORE 标志不清除时后续 RXNE 永不置位OCR_UART_IRQHandler 开头检测 USART_FLAG_ORE,置位时读一次 DR 清标志后 return
7天/30天过滤结果与全部完全相同以 DS3231 当前时间为基准,历史账单距今均超限,被全部过滤改以数据集最大日期(账单文件名最晚日期)为基准

通用问题

问题原因解决方法
L6915E: __use_no_semihostingusart.c_ttywrch 存根void _ttywrch(int ch){ ch=ch; }
商品列表空白标准 malloc 返回 NULL改用 mymalloc(SRAMIN, ...)
中文显示方块Show_Str 末参数为 0改为 1(透明模式)
CSV 中文乱码文件保存为 UTF-8另存为 ANSI/GBK
商品只显示 2 条strtok 跳过空字段改用 Get_CSV_Field() 逐字符数逗号
触摸抬起坐标不准抬起后坐标是残留值按下时记录到临时变量
SD 卡读不到文件SD 卡格式化为 NTFS重新格式化为 FAT32
RFID 无响应上电后立即发命令delay_ms(500)

十、开发规范

规范说明
内存分配一律用 mymalloc(SRAMIN, ...) / myfree(SRAMIN, ...)绝不用标准 malloc
中文显示一律用 Show_Str(x, y, w, h, str, size, 1)最后参数必须是 1(透明模式)
USB 键盘每个使用键盘的界面 while(1) 最前面必须调用 USB_KBD_Process()
新界面UI/ 目录,在 ui_main_menu.c 的 switch 对应 case 里加调用
编码K230 输出 UTF-8,ime_pinyin.c 适配层负责转 GBK,UI 层无需关心
SD 卡格式必须 FAT32,NTFS 会导致 f_open 失败
动态内存所有 UI 界面的大型数组必须打包进 XxxCtx_t 结构体,进入时 mymalloc,退出时 myfree
公共函数Draw_Sub_Titlebar / Draw_Sub_Statusbar / Is_Back_Touched 声明在 ui_rfid_locate.h

当前状态总览(整理日期:2026-04-08)

模块状态
主菜单✅ 完成实测
仓库定位界面✅ 完成实测
SD 卡数据模块✅ 完成实测
RFID 驱动✅ 完成实测
条码扫描模块✅ 完成实测
入库界面(表单 + IME 拼音输入法)✅ 完成实测
收银台/出库✅ 完成实测
USB 键盘✅ 完成实测
IME 引擎(二进制词库)✅ 完成实测
K230 OCR(含 STM32 通信)✅ 完成实测
库存总览界面✅ 完成实测
系统设置界面✅ 完成实测
销售分析界面✅ 完成实测
条码信息删除✅ 完成实测
多管理员权限系统✅ 完成实测
操作日志(log.csv,七类操作)✅ 完成实测
DS3231 RTC 驱动✅ 完成实测
阅读 ···
留言 · 评论