资讯详情 STM32与AS608指纹门禁系统设计:串口协议与状态机实战
📅 2026/10/26 12:52:38
简介围绕STM32C8T6与AS608指纹识别模块的二次开发资料包面向具备一定C语言与单片机基础的嵌入式学习者和项目开发者可用于指纹采集、录入、比对及串口通信等实验场景。包内共211个文件以C源文件、头文件为主辅以Keil工程配置、HEX烧录文件及编译列表、映射等中间产物压缩包约7.04MB工程结构基本完整便于直接打开编译、烧录与按模块检索。已有1914人学习下载说明其在同类指纹模块开发资料中具有一定参考价值。资料围绕AS608的供电、串口收发、触摸感应与状态读取等引脚定义展开结合芯片控制协议和STM32外设驱动可帮助读者理解指纹模块的初始化、指令交互、指纹录入与比对流程并借助现有工程验证通信、定位常见配置与编译问题。整体适合用作课程设计、毕业设计或小型指纹识别项目的起步模板也便于在现有代码上扩展门禁、考勤等应用。1. 指纹门禁的选型判断为什么用 STM32 加 AS608 而不是自己跑算法STM32 加 AS608 的指纹识别组合几乎是每届课设里出现频率最高的方案之一但真正能做到稳定演示的并不多。常见误区一上来就在 STM32F103C8T6 上自己实现指纹特征提取结果被图像质量和算法复杂度劝退。我一般建议反过来考虑识别算法固化在 AS608 模块内部的那颗 DSP 里主控只负责流程调度、模板管理和锁具驱动代码量能压到两三千行以内出问题也有明确的排查切分点。这套结构适合指纹门禁、考勤终端、储物柜锁这类课程设计与毕业设计也适合小规模的门禁改造场景。2. 总体架构与模块划分一颗 M3 主控带起的五条支路整套方案的核心判断是“算法下沉、流程上浮”。AS608 负责采图、特征提取、特征合并和模板比对STM32 负责什么时候让它干活、结果怎么解释、模板 ID 怎么分配、锁开多久。把这条线划清楚后面所有的代码结构、调试手段、验收指标都是从它推出来的。2.1 硬件最小系统与外围拓扑主控选 STM32F103C8T672MHz 主频、64KB Flash、20KB SRAM、3 个 USART、2 个 I2C跑这套业务绰绰有余。最小系统按常规配8MHz 无源晶振经 PLL 九倍频到 72MHz复位电路用 10k 上拉加 100nF 电容SWD 四线3.3V、GND、SWDIO、SWCLK引出来做下载和调试。供电部分是最容易被低估的地方。AS608 采图瞬间电流能到 60mA 左右OLED 屏背光二三十毫安加上 MCU 本身3.3V 这一路 LDO 至少要留 300mA 的余量并且指纹模块的电源脚旁边要就近放一颗 100μF 电解加 100nF 陶瓷。电磁锁、继电器这类执行机构走独立的 5V 或 12V 回路MCU 只通过三极管或光耦给控制信号绝不允许从板载 3.3V 上取电。通信连接上AS608 的 TX 接 MCU 的 RXRX 接 MCU 的 TX交叉连接两边必须共地。共地缺失是后面排查章节里第一条要讲的问题很多“模块不应答”的现场故障都源于杜邦线只接了三根。2.2 五个功能模块的职责与接口定义把整机拆成五块每块只有一个明确的输入输出接口一旦定下来就不要在中途改否则后面联调会互相拉扯。模块接口方式引脚示例关键参数AS608 指纹模块USART2PA2 / PA357600 8N1默认地址 0xFFFFFFFF0.96 寸 OLEDI2C1PB6 / PB7从机地址 0x3CSSD1306 驱动三键键盘GPIO 输入PB0 / PB1 / PB10内部上拉低电平有效软件消抖 20ms继电器与电磁锁GPIO 输出PA4高电平吸合三极管基极串 1k 电阻线圈反并续流二极管蜂鸣器与状态灯GPIO 输出PA5 / PC13有源蜂鸣器短鸣 80ms 表示成功指纹模块负责“生物特征到 ID”的转换键盘负责管理动作录入、删除、清空OLED 负责状态提示继电器负责物理执行蜂鸣器和灯负责人机反馈。每一块的边界都卡死比如 OLED 驱动里不出现任何指纹相关的判断业务层里不出现任何 I2C 时序操作。2.3 软件分层与时间预算软件分四层驱动层uart、i2c、gpio、flash、协议层as608 指令封装、业务层状态机、调度层main 里的时间片。协议层对外只暴露一个函数原型业务层调用它的时候不关心串口是怎么收发字节的这个抽象在后面换主控或者换模块型号时能省下大量返工。时间预算要在动手写代码之前算一遍。一次 GenImg 采图 100 到 300msImg2Tz 生成特征 50ms 左右RegModel 合并 200msStore 写模块内部 Flash 100msSearch 搜索在 300 枚模板满载时也在 300ms 以内。这意味着一次完整识别从按压到开锁端到端能控制在 1 秒以内用户体感是可接受的。主循环用 SysTick 打 10ms 节拍所有超时判断都基于这个节拍累加不用 HAL_Delay 死等。独立看门狗喂狗周期设 2 秒喂狗动作放在主循环末尾任何一层阻塞超过 2 秒都会复位这在答辩现场是保命设计。提示采图指令在传感器上没有手指时会立刻返回确认码 0x02不会阻塞等待。所以“等人按手指”这件事必须放在主循环里轮询而不能指望一条指令卡在那里等。3. USART 协议落地AS608 指令包的组包、收发与确认码处理协议这一层写扎实后面的业务代码就是排列组合。AS608 的串口协议是定长头加变长体的结构包头固定两字节地址四字节包标识一字节包长度两字节然后是指令码、参数、校验和。3.1 指令包结构与校验和的算法先把包格式记住这是所有通信的基础。字段长度说明包头2 字节固定 0xEF 0x01模块地址4 字节出厂默认 0xFFFFFFFF单模块不用改包标识1 字节0x01 命令包0x02 数据包0x08 结束包包长度2 字节指令码 参数 校验和的字节数不含包标识指令码1 字节如 0x01 采图、0x02 生成特征、0x04 搜索参数N 字节随指令不同而变可为空校验和2 字节从包标识累加到最后一个参数字节取低 16 位组包函数是整个协议层的基础写法如下/* as608_proto.c —— 组包包长度不含包标识校验和从包标识开始累加 */ #define AS608_MAX_PARAM 64 #define AS608_MAX_PACK (12 AS608_MAX_PARAM) static uint16_t as608_build(uint8_t *out, uint8_t pid, uint8_t cmd, const uint8_t *param, uint16_t plen) { uint16_t i, sum 0; out[0] 0xEF; out[1] 0x01; /* 固定包头 */ out[2] 0xFF; out[3] 0xFF; /* 模块地址出厂默认全 F */ out[4] 0xFF; out[5] 0xFF; out[6] pid; /* 0x01 命令包 */ out[7] (uint8_t)((plen 3) 8); /* 包长度 指令码 参数 校验和 */ out[8] (uint8_t)((plen 3) 0xFF); out[9] cmd; for (i 0; i plen; i) { out[10 i] param[i]; } for (i 6; i 10 plen; i) { /* 从包标识累加到最后一个参数 */ sum out[i]; } out[10 plen] (uint8_t)(sum 8); out[11 plen] (uint8_t)(sum 0xFF); return 12 plen; /* 返回整包字节数 */ }plen是参数个数比如采图指令参数为空传 0搜索指令参数是“缓冲区号 起始 ID 模板数”共 5 字节。pid在发命令时固定传 0x01。函数返回整包长度发送时直接按这个长度调HAL_UART_Transmit不要用sizeof(buf)那样会把缓冲区尾部的脏数据一起发出去模块会直接判成收包错误。3.2 接收侧DMA 加空闲中断的整包接收接收最简单可靠的做法是 DMA 加串口空闲中断。DMA 负责把字节搬进缓冲区空闲中断负责告诉我们“这一帧结束了”主循环里再解析。这样做的好处是 CPU 完全不参与搬运也不会因为主循环忙别的事情丢字节。/* stm32f1xx_it.c —— 串口空闲中断判定一帧结束 */ #define AS608_RX_SIZE 128 volatile uint8_t as608_rx_buf[AS608_RX_SIZE]; volatile uint16_t as608_rx_len 0; volatile uint8_t as608_frame_ready 0; void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart2); HAL_UART_DMAStop(huart2); /* 缓冲区大小减去 DMA 剩余计数就是本次收到的字节数 */ as608_rx_len AS608_RX_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart2_rx); as608_frame_ready 1; /* 立刻重新挂上 DMA避免下一帧溢出 */ HAL_UART_Receive_DMA(huart2, (uint8_t *)as608_rx_buf, AS608_RX_SIZE); } HAL_UART_IRQHandler(huart2); }解析时先校验buf[0]0xEF buf[1]0x01再从buf[7]8 | buf[8]取包长度算出整包应为9 len字节然后比对校验和。校验不对直接丢弃并重试一次不要带着怀疑的数据往下走否则后面会出现“明明采到指纹却比对失败”这种玄学。应答包的确认码在buf[9]0x00 表示成功其余都是错误。这一步封装成统一的as608_cmd()返回确认码业务层只看返回值。3.3 五个核心指令的调用序列与确认码业务层真正会用到的指令其实只有五六个把这几个的调用顺序和确认码吃透就够了。指令码名称典型参数用途0x01GenImg无采集指纹图像0x02Img2Tz1 或 2生成特征放入缓冲区 1 或 20x05RegModel无合并两个缓冲区的特征0x06Store缓冲区号 模板 ID存模板到模块内部 Flash0x04Search缓冲区号 起始 ID 数量在模板库里搜索0x0CDeleteChar模板 ID 数量删除指定模板0x0DEmpty无清空整个模板库确认码里最常见的几个含义0x00 成功0x02 传感器上无手指0x06 图像太乱0x07 特征点过少0x08 指纹不匹配0x09 没搜索到0x0A 特征合并失败0x0B 模板 ID 越界0x10 删除失败0x11 清空失败。这里有一个容易翻车的点RepeatGenImg 到 Img2Tz 之间缓冲区号必须和后面 RegModel 的约定一致第一次录的特征放缓冲区 1第二次放缓冲区 2两个都放 1 会让 RegModel 返回 0x0A。这种错误看代码看不出来只能靠确认码定位。/* 统一指令入口超时时间按指令类型给采图类给长一些 */ uint8_t as608_cmd(uint8_t cmd, const uint8_t *param, uint16_t plen, uint32_t timeout_ms) { uint8_t pack[AS608_MAX_PACK]; uint16_t n as608_build(pack, 0x01, cmd, param, plen); uint32_t start HAL_GetTick(); HAL_UART_Transmit(huart2, pack, n, 100); while (!as608_frame_ready) { if (HAL_GetTick() - start timeout_ms) return 0xFF; /* 超时 */ } as608_frame_ready 0; if (!as608_check_sum((uint8_t *)as608_rx_buf, as608_rx_len)) return 0xFE; return as608_rx_buf[9]; /* 返回确认码 */ }timeout_ms是个需要按指令分别给的参数。GenImg 在无人按压时会立刻返回 0x02超时给 100ms 就够Search、RegModel、Store 这几条模块内部要干活给 500ms 到 1000ms 比较稳清空指纹库在某些固件上会比较慢给 2 秒。4. 业务状态机与模板库管理录入、比对、删除的闭环协议层跑通以后剩下的就是“什么时候发哪条指令”。这部分用状态机写比一堆 if-else 嵌套清晰得多也方便答辩的时候画出来讲。4.1 状态划分与迁移条件状态直接对应人机交互的阶段一共七个待机轮询、录入第一次采图完成、等待抬指、第二次采图、搜索比对、开锁计时、管理态。加上一个失败锁定态防止连续试探。/* fp_app.h —— 业务状态枚举 */ typedef enum { ST_IDLE 0, /* 待机周期性发 GenImg 等待按手指 */ ST_ENROLL_1, /* 第一次特征已入缓冲区 1等用户抬起手指 */ ST_ENROLL_2, /* 第二次特征已入缓冲区 2准备合并 */ ST_SEARCH, /* 搜索比对 */ ST_OPEN, /* 开锁计时 3 秒后自动回落 */ ST_ADMIN, /* 管理态删除单枚、清空全部 */ ST_LOCKED /* 连续失败 5 次后锁定 60 秒 */ } fp_state_t;迁移条件写死不要留模糊地带。待机态每 300ms 发一次 GenImg返回 0x00 就进录入或搜索分支返回 0x02 继续待在待机态。录入第一次采图成功后进 ENROLL_1该状态下持续发 GenImg直到返回 0x02 才确认手指已经离开再进 ENROLL_2这个“等抬指”的判断如果用固定延时替代用户手慢一点就会污染第二次采图。搜索态拿到 Search 的返回参数里会带回匹配到的模板 ID 和匹配分数分数低于阈值按失败处理。开锁态保持 3 秒期间不再响应指纹避免连续触发。锁定态是产品化思维课设里可以不做但答辩时讲出来是加分项。4.2 模板 ID 分配与 Flash 掉电保存AS608 内部可以存 300 枚模板模板 ID 从 1 到 3000 号通常留给比对用的临时缓冲区。如果只在 SC 里维护一个自增变量掉电重启就归零第二次录入会覆盖别人。所以必须把 ID 分配表放到 STM32 的 Flash 里。STM32F103C8T6 的 Flash 页大小是 1KB64KB 空间里最后一页起始地址是 0x0800FC00正好拿来放参数区。/* fp_store.h —— 参数区结构放在 Flash 最后一页 */ #define FP_STORE_ADDR 0x0800FC00u #define FP_STORE_MAGIC 0x5A5AA5A5u #define FP_MAX_TEMPLATE 300 typedef struct { uint32_t magic; /* 魔数判断该页是否已初始化 */ uint16_t next_id; /* 下一个可分配的模板 ID1 ~ 300 */ uint16_t used_cnt; /* 已占用数量 */ uint8_t bitmap[(FP_MAX_TEMPLATE 7) / 8]; /* 38 字节位图标记 ID 占用 */ uint16_t crc; /* 前 46 字节的累加校验 */ } fp_store_t;magic用固定值判断首次上电如果读到 0xFFFFFFFF 说明这页从没写过直接初始化成next_id1。位图用 38 字节覆盖 300 个 ID每一位代表一枚模板是否被占用比存一个 300 字节的数组省 262 字节虽然 C8T6 的 64KB 也不算紧张但习惯是好的。写入流程必须是“擦除整页 → 写入整页”。F1 系列的 Flash 只能按页擦不能字节覆盖所以每次保存都要先把整页 1KB 擦掉再写。擦写期间要关中断因为 Flash 操作会暂停取指此时如果有中断进来会触发总线异常。擦写一次大概几毫秒对业务没有影响。/* 分配一个空闲模板 ID满了返回 0xFFFF */ uint16_t fp_id_alloc(void) { fp_store_t st; uint16_t id; fp_store_load(st); for (id 1; id FP_MAX_TEMPLATE; id) { if (!(st.bitmap[(id - 1) 3] (1 ((id - 1) 0x07)))) { st.bitmap[(id - 1) 3] | (1 ((id - 1) 0x07)); st.used_cnt; st.next_id id 1; fp_store_save(st); /* 立刻落盘防止分配后掉电丢记录 */ return id; } } return 0xFFFF; /* 模板库已满 */ }分配后立刻落盘这一步不能省。有些实现为了省擦写次数攒够十条再存一次结果掉电后模块里有模板、MCU 里没记录出现“能开门但删不掉”的脏数据。4.3 录入流程的实现与两次采图的时序录入是最容易出问题的环节因为它涉及两次采图和一次合并中间还夹着“等用户抬手指”这种非确定性等待。用状态机的分步写法每一步只做一件事。/* fp_app.c —— 分步录入每次主循环调用一次不阻塞 */ static uint8_t s_step 0; fp_result_t fp_enroll_step(uint16_t *out_id) { uint8_t resp, buf; switch (s_step) { case 0: /* 第一次采图特征放缓冲区 1 */ if (as608_cmd(0x01, NULL, 0, 100) ! 0x00) return FP_WAIT; if (as608_cmd(0x02, (uint8_t){1}, 1, 500) ! 0x00) { s_step 0; return FP_BAD_IMAGE; /* 图像太乱重来 */ } s_step 1; return FP_BUSY; /* 提示请抬起手指 */ case 1: /* 等抬指GenImg 返回 0x02 才说明手已离开 */ if (as608_cmd(0x01, NULL, 0, 100) 0x02) s_step 2; return FP_BUSY; case 2: /* 第二次采图特征放缓冲区 2 */ if (as608_cmd(0x01, NULL, 0, 3000) ! 0x00) return FP_BUSY; if (as608_cmd(0x02, (uint8_t){2}, 1, 500) ! 0x00) { s_step 0; return FP_BAD_IMAGE; } s_step 3; return FP_BUSY; case 3: /* 合并两个缓冲区的特征 */ if (as608_cmd(0x05, NULL, 0, 500) ! 0x00) { s_step 0; return FP_MERGE_FAIL; } buf 1; /* 合并结果仍在缓冲区 1 */ *out_id fp_id_alloc(); if (*out_id 0xFFFF) { s_step 0; return FP_FULL; } /* Store 参数缓冲区号 模板 ID 高低字节 */ uint8_t p[3] { buf, (uint8_t)(*out_id 8), (uint8_t)(*out_id 0xFF) }; if (as608_cmd(0x06, p, 3, 1000) ! 0x00) { fp_id_free(*out_id); /* 存失败要把 ID 还回去 */ s_step 0; return FP_STORE_FAIL; } s_step 0; return FP_OK; } return FP_BUSY; }几个细节值得单独说。第一as608_cmd的第二个参数用了复合字面量(uint8_t){1}在 C99 下合法如果编译器是 C89 就老老实实定义临时变量。第二Store失败时必须调用fp_id_free把刚分配的 ID 释放掉否则位图里留着一个没有任何模板对应的“幽灵 ID”时间长了模板库明明没满却分配不出新 ID。第三s_step在每次失败后都归零重来不要试图从中间状态续跑指纹录入不是断点续传。搜索流程简单得多GenImg → Img2Tz(1) → Search(1, 1, 300)返回参数里第一个字节是匹配到的模板 ID 高字节第二个是低字节第三个是匹配分数。分数阈值默认取 50 左右具体值可以在调试时用同一根手指多按几十次取一个稳定区间再往上抬 10 到 15 作为判定线。5. 常见问题排查五个最容易翻车的点这一节里的每一条都是我或者身边的同学真金白银踩过的。5.1 模块完全不应答重上电偶尔又能用现象是主控发指令没有任何返回用逻辑分析仪抓串口线也没有波形或者波形正常但一直超时。原因大概率在供电和共地上。AS608 在采图瞬间电流冲到 60mA如果 LDO 只有 100mA 的余量或者模块的电源是从主控板 3.3V 排针用 20cm 杜邦线引出去的线阻造成的压降会让模块在最关键的采图阶段掉电复位。另一个常见原因是只接了 TX、RX 和电源三根线两边没有共地串口电平没有参考点通信自然不通。解决方式是给指纹模块单独走一条粗走线电源脚旁就近放 100μF 加 100nF 去耦杜邦线剪短到 10cm 以内并且一定把 GND 接上。如果实在找不到原因用示波器看模块 VCC 引脚在采图瞬间有没有超过 200mV 的跌落这是最快的定位手段。5.2 一直返回 0x02无手指但手指明明按着现象是 GenImg 持续返回无手指换了人按也不行偶尔按得特别重才能成功。原因分两类。硬件上光学指纹头表面的保护膜没撕干净那层膜会让图像对比度严重下降特征提取直接失败或者模块表面玻璃被指纹油污糊住采到的是一片模糊。软件上有些版本的模块需要用 ReadSysPara 确认当前处于正常模式如果之前误发了 SetSysPara 改了参数又没保存模块会一直停在异常状态。解决上先撕膜、用无水酒精擦干净指纹头。然后写一段最小的测试程序只发 GenImg 并打印确认码把业务逻辑全部摘掉如果这样能返回 0x00说明问题在业务层如果还是 0x02就换一个模块交叉验证。手指特别干的季节让用户对着手指哈一口气再按成功率会明显上升这不是玄学是光学采集的物理限制。5.3 录入时提示成功搜索却永远返回 0x09现象是录入流程走完OLED 上显示“录入成功”但同一个手指再按Search 一直返回 0x09没搜索到。原因通常是把特征合并成功了却忘了 Store。RegModel 只是把两个缓冲区合并成一个特征这个特征还躺在缓冲区里掉电就没了必须再发一条 Store 指令把它写进模块内部的模板库。另一种情况是 Store 的参数写错了模板 ID 传成了 0 或者超过 300模块会返回 0x0B但有些实现没检查这个返回一路当成成功继续往下走。解决方式是给录入流程加一个“回读校验”Store 成功以后立刻用 Store 时的 ID 发一次 LoadChar确认模块里真的存在这个模板再让用户按一次手指走 Search只有搜索能命中同一个 ID 才算真正录入完成。这套校验会多花 200ms但能把“假成功”彻底堵死。5.4 连续运行十几分钟后死机或自动重启现象是刚开始一切正常跑一段时间后 OLED 花屏、蜂鸣器长鸣或者干脆重启。原因之一是栈溢出。C8T6 只有 20KB RAM默认的启动文件给栈分配 1KB如果协议层在栈上开了 128 字节的接收缓冲、业务层又有一层嵌套的局部数组再加上串口中断的栈帧很容易压到临界。原因之二是 Flash 擦写时没有关中断擦除那一瞬间 CPU 取指被暂停如果此时有串口中断进来会直接触发 HardFault。解决方式是把大缓冲区全部改成静态全局变量启动文件里把栈从 1KB 调到 2KB堆设成 0x200 就够这套代码不用 malloc。Flash 擦写函数里用__disable_irq()和__enable_irq()包住擦完再开。另外一定要开独立看门狗周期给 2 秒哪怕真死机了也能自动恢复答辩现场不至于僵在那里。5.5 别人也能开锁或者同一个人经常被拒现象是误识率明显偏高换个手指也能开或者本人按十次有三次没反应。原因是安全等级没调。AS608 的比对等级默认是 3范围 1 到 5数值越高越严格误识率降下来但拒真率会上升。课设现场人手汗、灰尘、按压角度都不一样默认值 3 往往偏松。另一类原因是同一个手指在不同角度重复录了多枚模板本来就相似匹配分数都在阈值上下浮动。解决方式是把安全等级调到 4用 ReadSysPara 读出来确认写进去了。模板方面一个人只录一枚靠按压姿势的规范来提升一致率OLED 上给一个“请将手指平放、覆盖整个采集窗”的提示比多录三枚更有效。测试阶段统计 50 次按压的成功率低于 90% 就回来调阈值不要凭感觉拍板。6. 验收与答辩用一张测试表把方案讲清楚课设和毕设的评分点从来不是“功能能不能跑”而是“你怎么证明它稳定”。所以验收环节要提前设计把测试项做成表格现场逐条打勾比口头说“我测过了”有说服力得多。下面这张表是我一般会准备的验收清单八项覆盖功能、可靠性和安全性序号测试项操作步骤判定标准1首次上电初始化断电后重新上电OLED 显示待机界面3 秒内进入轮询2指纹录入同一手指按压两次中间抬起提示录入成功分配 ID 从 1 递增3正确指纹开锁已录入手指按压一次1 秒内继电器吸合 3 秒蜂鸣器短鸣4未录入指纹拒绝陌生手指按压提示失败不开锁失败计数加一5模板掉电保持断电 1 分钟后重新上电再按压仍能识别并开锁6单枚删除管理态删除指定 ID 后按压该手指提示无匹配其余指纹不受影响7连续稳定性连续按压 50 次成功率不低于 90%无死机重启8失败锁定连续按错 5 次进入 60 秒锁定期间不响应按压答辩演示的顺序也有讲究。先讲架构图把“算法在模块里、主控做流程”这条主线说清楚评委一听就知道你理解了分工而不是把模块当黑盒乱调。然后现场走一遍录入、识别、删除的完整链路中间故意留一次失败演示——比如用一根没录入的手指按一下展示错误处理和蜂鸣器提示这比全程顺利更能体现工程完成度。最后把测试表投出来说清楚每一项的实测结果评委的问题基本会集中在你主动暴露的那几个点上反而好答。代码讲解控制在三分钟。我的习惯是只讲三个函数as608_build讲协议理解fp_enroll_step讲状态机设计fp_id_alloc讲掉电保存的考虑。这三个讲透了剩下的都是重复劳动不用一句句念。从那以后我每次带人做这类项目都强制先写验收表再写代码表格里填不出来的项说明需求还没想清楚那就别急着敲键盘。希望帮到你。本文还有配套的精品资源点击获取