便利店实时库存管理系统
毕业设计答辩已完成,现作技术文档公开发布。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
本文档为毕业设计项目的综合技术文档,答辩已完成,现作公开技术参考资料发布,供有相似开发需求的朋友参阅。文档覆盖完整的系统架构、各模块实现、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 915MHz | USART2 PA2/PA3 115200bps | 仓库货架箱级定位,实测读距 >4m |
| ④ | AI 协处理器 | CanMV K230 V3P0 NPU 6TOPS 1GB LPDDR4 | USART3 PB10/PB11 115200bps | OCR 文字识别 + 销售分析,结果通过 UART 发回 |
| ⑤ | 条码扫描仪 | ADL615H(滴滴解码 T165) | USART6 PC6/PC7 9600bps | 出库结账扫码,支持命令触发与模式切换 |
| ⑥ | Flash 存储 | W25Q128 128Mbit SPI NOR Flash | SPI1 PB3/PB4/PB5 | GBK 中文字库,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 | 调试用,不接外设不影响运行 |
| USART2 | RFID YND-R200 | PA2(TX) / PA3(RX) | 115200bps 8N1 | 中断接收,RFID_DataReceive() |
| USART3 | K230 OCR 模块 | PB10(TX) / PB11(RX) | 115200bps 8N1 | 中断接收,OCR_UART_IRQHandler();同时处理 OCR 及销售分析回传 |
| USART6 | 条码扫描仪 ADL615H | PC6(RX) / PC7(TX) | 9600bps 8N1 | 指令控制扫描触发/模式切换 |
| SDIO | SD 卡(4-bit) | PC8-12 / PD2 | SDIO 4-bit | FAT32,挂载为 "0:" |
| SPI1 | W25Q128 Flash | PB3(SCK)/PB4(MISO)/PB5(MOSI) | SPI 模式0 | FAT32,挂载为 "1:" |
| USB OTG_FS | USB HID 键盘 | PA11(D-) / PA12(D+) | USB 2.0 FS | 任意 HID 键盘,热插拔 |
| I2C / CTIIC | 触摸控制器 FT5206 | PB6(SCL) / PB7(SDA) | I2C | 支持 GT9147 / OTT2001A |
| FSMC | LCD SSD1963 | PD/PE,NE4 片选,A6=RS | 16-bit 并行 | 480×800,60Hz |
| 软件 I2C | DS3231 RTC | PC0(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 卡步骤
- 将 SD 卡格式化为 FAT32(不能是 exFAT 或 NTFS)
- 手动建好文件夹结构(系统不会自动创建目录):
BILLS/、IMG/、LOGS/ - 将
items.csv、zh_pinyin.bin、zh_word_dict.bin复制到 SD 卡根目录 - 插入开发板,上电即可使用
五、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)。
⚠️ 不要同时加 IRAM2(
Start: 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.c | s_rows[40] 等打包进 SettCtx_t,动态分配 | ~2.9 KB |
| ui_stock_overview.c | view_idx[128]、zone_list 等打包进 StockCtx_t | ~165 B |
| ui_inbound_item.c | field_buf[8][48]、IME_t ime 等打包进 ItemCtx_t | ~0.8 KB+ |
| ui_checkout.c | scan_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 MB | 784 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。三种方案的踩坑路径:
- 让 K230 直接发 GBK → 固件不支持,encode 异常后 except 分支再次崩溃,实际发出 UTF-8 或空帧
- 在中断上下文里做转换 →
ff_convert()在 ISR 里不稳定,FATFS 函数不应在中断里执行 - 最终方案 ✅:K230 始终发 UTF-8,STM32 在主循环
OCR_GetResult()中调用ff_convert(uni, 1)完成 Unicode→GBK 转换
⚠️
ff_convert()方向参数:dir=1是 Unicode→GBK,dir=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 原始值 | 响时长 | 静音时长 | 节奏 |
|---|---|---|---|
≥ 0xDE | 20ms | 20ms | 极快(已贴近) |
≥ 0xD3 | 50ms | 50ms | 快 |
≥ 0xC8 | 80ms | 170ms | 中速 |
≥ 0xC0 | 80ms | 420ms | 慢 |
≥ 0xB8 | 80ms | 920ms | 极慢(远距离) |
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,防止引擎超时停止。
结账执行顺序(顺序不可乱)
- 扣减
sd_items[ci].qty SD_Items_SaveAll()写 CSVSave_Bill_TXT()写账单 TXT(0:/BILLS/BILLxxxx.TXT)- 显示完成界面
账单格式示例:
============================== 便利店出库账单 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_SECTION | 34px | 深色节标题行 |
RT_INFO | 46px | 左标签 + 右值 |
RT_STATUS | 46px | 左标签 + 彩色状态点 + 右值(绿=OK / 橙=WARN / 红=ERROR) |
RT_TEXT | 82px | 长文本行 |
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_7、s_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 | 引脚 | 状态 |
|---|---|---|
| UART0 | Pin38/39 | 被小核占用,不可用 |
| UART3 | Pin50/51 | 被大核占用,不可用 |
| UART2 | Pin5(TX)/Pin6(RX) | 当前使用 ✅ |
| UART1 | Pin3/Pin4 | 可用 |
| UART4 | Pin36/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 默认 C89 | Keil → 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() 返回 None | VIRT 模式下无显示尺寸 | 改回 "lcd" 模式 |
| K230 不自启 | 文件直接拷到 TF 卡不生效 | 用 IDE 菜单「工具 → 保存当前脚本为 main.py 到 CanMV Cam」 |
| 待机时摄像头仍亮 | Pipeline 初始化后摄像头就启动 | 收到 OCR\n 再初始化,收到 STOP\n 后 destroy |
OCR 中文编码与转换
| 问题 | 原因 | 解决方法 |
|---|---|---|
| STM32 收到中文显示乱码 | K230 发 UTF-8,Show_Str() 只认 GBK | ff_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 无法拉低 SDA | SDA 改为开漏+内部上拉(OD + PuPd_UP),直接读 IDR |
销售分析模块
| 问题 | 原因 | 解决方法 |
|---|---|---|
f_gets 永久挂死 | 文件读取期间调用 Show_Str(含字库 SD 读取)打断 SDIO DMA | 文件读取循环内禁止所有 LCD 操作,动画刷新移至安全窗口 |
sscanf %f 解析金额返回 0 | ARMCC MicroLIB 下 sscanf %f 不工作 | 改用 atof() 手动解析;价格乘 100 以整数传输,K230 收到后 /100.0 还原 |
[S]PRED 超行长导致 [S]END 被覆盖 | [S]PRED 最长 386B,远超 RING_LINE_SZ=160 | K230 拆为 [S]PRED1: 每商品一行,单行控制在 50B 内 |
| USART3 ORE 后中断永久失效 | STM32F4 USART 经典坑:ORE 标志不清除时后续 RXNE 永不置位 | OCR_UART_IRQHandler 开头检测 USART_FLAG_ORE,置位时读一次 DR 清标志后 return |
| 7天/30天过滤结果与全部完全相同 | 以 DS3231 当前时间为基准,历史账单距今均超限,被全部过滤 | 改以数据集最大日期(账单文件名最晚日期)为基准 |
通用问题
| 问题 | 原因 | 解决方法 |
|---|---|---|
L6915E: __use_no_semihosting | usart.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 驱动 | ✅ 完成实测 |


邮箱身份验证
检测到你使用了博主邮箱评论。请先验证邮箱所有权,验证一次 30 天内同邮箱无需重复。