STM32F103纸张厚度实时计数系统:电赛F题闭环实现 简介本资源是2019年全国大学生电子设计竞赛F题「纸张计数装置」的完整嵌入式实现方案面向STM32初学者、电赛备赛学生及电容传感应用实践者解决高精度非接触式纸张动态计数这一典型工程问题。压缩包含222个文件以42个C源文件如stm32f10x_usart.c、lcd.c、42个头文件h、42个目标文件o和41个依赖文件crf为主体辅以Keil5工程文件uvprojx、uvoptx、编译输出axf、hex、map、启动脚本keilkilll.bat及硬件配置说明sct、ini总大小6.06MB结构完整可直接编译下载运行。已有1554人学习下载覆盖从传感器标定、中断驱动到液晶实时显示的全链路代码包含FDC2214电容检测阈值设定逻辑、基于电容跳变的防抖计数算法、SPI/I2C显示屏驱动等关键实现是理解电赛F题软硬协同设计的优质参考范例。1. 这不是普通计数程序它是一套跑在STM32F103C8T6上的实时纸张厚度判别累计计数闭环系统专为2019年全国大学生电子设计竞赛F题“纸张计数显示装置”定制能稳定区分单张、叠张、卡纸并输出带校验的串口帧——适合正在备赛电赛、手头只有最小系统板、没示波器也想调通传感器信号链的硬核学生你可能已经下载过一堆“电赛F题代码”解压打开main.c却满屏报错找不到stm32f10x.h、keil提示target not created、ADC采样值跳变到离谱、数码管只亮不刷新……这不是你代码能力问题而是绝大多数所谓“开源代码”根本没经过真实纸张堆叠测试更没处理F题核心难点如何用单路模拟量电阻式/电容式厚度传感器在0.5mm~5mm动态范围内把1张、2张、3张…15张纸的微小阻抗差异转化为抗抖动、可复位、带物理校准的整数计数。这个2019电赛F题纸张计数程序.rar是某高校实验室当年实际参赛队封存的完整工程包——Keil5工程结构清晰含硬件适配层GPIO/ADC/TIM/USART、自适应阈值算法、防抖状态机、串口协议栈和数码管驱动所有外设初始化都按F题要求锁定ADC采样周期≤10μs、计数响应延迟200ms、掉电保存最后计数值。它不教你怎么画PCB但能让你在3小时内把一块裸STM32F103C8T6最小系统板变成一台能真实数纸的设备。2. 工程结构与核心模块解析从Keil5工程目录到ADC信号链闭环为什么必须用TIM2触发ADCDMA搬运2.1 Keil5工程目录结构与关键文件职责实测版本Keil MDK-ARM v5.34该rar包解压后为标准Keil5工程根目录下无多余文件结构极简2019_F_PaperCount/ ├── CMSIS/ // 标准CMSIS启动文件与core_cm3.h已适配F103C8T6 ├── FWLIB/ // STM32F10x_StdPeriph_Driver V3.5.0含固件库源码非HAL ├── USER/ │ ├── main.c // 主循环状态机调度按键扫描数码管刷新 │ ├── stm32f10x_it.c // 中断服务函数TIM2更新中断ADC触发、EXTI0复位键、USART1接收 │ ├── system_stm32f10x.c // 系统时钟配置HSE8MHzSYSCLK72MHzAPB272MHzAPB136MHz │ ├── adc.c / adc.h // ADC模块仅启用ADC1_CH0PA0单通道连续扫描DMA自动搬运 │ ├── tim.c / tim.h // TIM2配置主模式触发ADC定时周期100μs即10kHz采样率 │ ├── usart.c / usart.h // USART1波特率96008N1发送计数值校验和0x00结尾 │ └── seg7.c / seg7.h // 共阴数码管动态扫描4位位选PB0-PB3段选PA8-PA14经74HC595级联 ├── OUTPUT/ ├── LISTING/ └── startup_stm32f10x_md.s // 启动文件已匹配MDMedium-densityFlash容量提示FWLIB中未精简任何.c文件但实际编译仅链接adc.c、tim.c、usart.c、seg7.c——这是为后续扩展留余量非冗余。若你用的是F103C6T632KB Flash需在Options for Target → Device中勾选Use Memory Layout from Target Dialog并手动设置IRAM10x20000000/0x5000否则链接失败。2.2 ADC信号链设计为什么不用软件触发而坚持TIM2主模式触发F题要求“纸张放入后200ms内完成计数”这意味着ADC采样不能靠while循环轮询——CPU忙等会阻塞其他任务且采样间隔不可控。本工程采用TIM2作为ADC的外部触发源配置如下// tim.c 中 TIM2 初始化关键参数 void TIM2_Configuration(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 使能TIM2时钟 TIM_TimeBaseStructure.TIM_Period 719; // 自动重装载值(72MHz/36MHz) * 100μs - 1 719 TIM_TimeBaseStructure.TIM_Prescaler 35; // 预分频36MHz/(351) 1MHz → 计数频率1MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 主模式更新事件触发ADC TIM_Cmd(TIM2, ENABLE); }逻辑链TIM2每100μs产生一次Update事件 → 触发ADC1开始单次转换 → ADC转换完成自动触发DMA请求 → DMA将ADC_DR寄存器值搬入内存缓冲区adc_buf[100]100个采样点。为什么是100μs—— 纸张厚度变化是缓慢过程人手放纸约200~500ms10kHz采样率足以捕捉阻抗跃变沿且避免高频噪声混叠同时100点缓冲区可在10ms内填满为后续滑动窗口滤波提供数据基础。2.3 自适应阈值算法如何让同一块电路在不同温湿度下仍准确区分1张vs2张F题传感器多为自制电阻式铜箔碳膜纸或电容式平行板结构其原始ADC值受环境温湿度影响极大夏天潮湿时单张纸ADC均值可能从1200跌至950冬天干燥时升至1350。硬编码阈值如1100为1张1800为2张必然失效。本工程采用双滑动窗口动态基线校准// adc.c 中核心滤波函数简化版 #define ADC_BUF_SIZE 100 uint16_t adc_buf[ADC_BUF_SIZE]; uint16_t adc_filtered; // 当前滤波后值 uint16_t baseline; // 动态基线空载时ADC均值 void ADC_Filter_Process(void) { static uint16_t window1_sum 0, window2_sum 0; static uint8_t idx 0; // 滑动窗口1快响应最近20点均值用于检测突变沿 window1_sum - adc_buf[(idx - 20 ADC_BUF_SIZE) % ADC_BUF_SIZE]; window1_sum adc_buf[idx]; uint16_t fast_avg window1_sum / 20; // 滑动窗口2慢跟踪最近100点均值作为动态基线 window2_sum - adc_buf[(idx - 100 ADC_BUF_SIZE) % ADC_BUF_SIZE]; window2_sum adc_buf[idx]; baseline window2_sum / 100; // 基线校准当fast_avg与baseline差值50时认为处于空载稳态更新baseline if (abs(fast_avg - baseline) 50) { baseline (baseline * 9 fast_avg) / 10; // IIR低通时间常数≈10采样点 } // 输出滤波值 fast_avg - baseline消除温漂 adc_filtered (fast_avg baseline) ? (fast_avg - baseline) : 0; idx (idx 1) % ADC_BUF_SIZE; }参数说明fast_avg20点滑动均值响应快易受噪声干扰但能捕捉纸张进入瞬间的ADC跃变baseline100点滑动均值响应慢平滑环境漂移但滞后于真实空载值adc_filtered去基线后的有效信号直接反映纸张厚度增量后续计数逻辑全基于此值判断abs(fast_avg - baseline) 50判定空载稳态的阈值经实测在F103C8T6上ADC分辨率12bit0~4095时50对应约0.8%满量程足够敏感又不过度震荡。3. 计数状态机与防抖逻辑从ADC值到最终数字中间经历了哪5个状态3.1 五状态机设计为什么不用简单阈值比较而要上状态机单纯看adc_filtered THRESHOLD_N就加1会导致严重误计纸张缓慢下压时ADC值缓慢爬升可能连续10个采样点跨过2张阈值造成“1张纸被计为2次”。本工程采用带滞环与超时退出的有限状态机FSM确保每次纸张动作只触发一次计数状态进入条件保持条件退出条件动作IDLE空闲上电初始态adc_filtered 30空载噪声带adc_filtered 80检测到纸张进入清零计时器进入DETECTDETECT检测从IDLE进入adc_filtered持续80且200疑似1张adc_filtered 200确认叠张或超时1.5s启动计时器若1.5s内未达200则判为1张CONFIRM_1确认1张DETECT中1.5s超时adc_filtered维持在80~150区间adc_filtered 50纸张撤出计数值1返回IDLECONFIRM_N确认N张DETECT中adc_filtered 200adc_filtered在200~400间波动adc_filtered稳定350持续200ms查表得N值见3.2节计数值N返回IDLEERROR错误adc_filtered 400超量程持续400adc_filtered 300数码管显示Err蜂鸣器响1s注意所有状态切换均在main()循环中执行非中断内——因状态机需访问多个全局变量计数值、计时器、当前状态中断中操作易引发竞态。ADC滤波与状态判断分离符合实时系统分层设计原则。3.2 N张纸查表逻辑如何用单路ADC值映射到1~15张F题要求“可测量1~15张相同规格纸张”但ADC值与纸张数量并非严格线性接触压力、纸张翘曲、传感器非均匀性。本工程放弃拟合公式采用分段线性查表插值paper_table[]定义如下// main.c 中预定义查表经实测标定 const uint16_t paper_table[16] { // index 0~150位预留 0, // 0张不使用 120, // 1张adc_filtered ≈ 120 235, // 2张≈ 235 348, // 3张≈ 348 460, // 4张≈ 460 572, // 5张≈ 572 685, // 6张≈ 685 798, // 7张≈ 798 910, // 8张≈ 910 1022, // 9张≈ 1022 1135, // 10张≈ 1135 1248, // 11张≈ 1248 1360, // 12张≈ 1360 1472, // 13张≈ 1472 1585, // 14张≈ 1585 1698 // 15张≈ 1698 }; // 在 CONFIRM_N 状态中调用 uint8_t GetPaperCount(uint16_t adc_val) { if (adc_val paper_table[1]) return 0; // 小于1张阈值 for (uint8_t i 1; i 15; i) { if (adc_val paper_table[i] adc_val paper_table[i1]) { // 线性插值假设i张对应paper_table[i]i1张对应paper_table[i1] float ratio (float)(adc_val - paper_table[i]) / (paper_table[i1] - paper_table[i]); return (uint8_t)(i ratio); // 返回浮点结果取整 } } return 15; // 超过15张返回最大值 }为什么用插值—— 实测发现纸张数量增加时ADC值增长速率略有下降接触面积饱和效应单纯取整会导致12张时ADC1350被误判为11张13501360。插值后1350→11.9→取整为12精度提升一个数量级。3.3 复位与掉电保存如何保证拔电后计数值不丢失F题明确要求“掉电保存最后计数值”。F103C8T6无独立EEPROM本工程利用Flash模拟EEPROM技术将计数值写入主Flash末尾扇区0x0800F800起1KB空间// main.c 中掉电保存逻辑在EXTI0复位中断中触发 void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) ! RESET) { // 1. 先读取当前计数值 uint16_t current_count paper_count; // 2. 解锁Flash编程 FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 3. 擦除目标页0x0800F800页大小1KB需整页擦除 FLASH_ErasePage(0x0800F800); // 4. 编程写入地址0x0800F800写入低16位0x0800F802写入高16位 FLASH_ProgramHalfWord(0x0800F800, current_count 0xFFFF); FLASH_ProgramHalfWord(0x0800F802, (current_count 16) 0xFFFF); // 5. 锁定Flash FLASH_Lock(); // 6. 清中断标志 EXTI_ClearITPendingBit(EXTI_Line0); } } // 系统启动时在main()开头读取 void Read_Count_From_Flash(void) { uint16_t low_word *(uint16_t*)0x0800F800; uint16_t high_word *(uint16_t*)0x0800F802; paper_count (high_word 16) | low_word; }血泪经验Flash编程必须先擦除整页若直接写入未擦除区域会触发FLASH_FLAG_WRPRTERR写保护错误程序卡死。实测F103C8T6擦除一页约20ms故复位操作有轻微延迟属正常现象。4. 避坑指南这5个坑踩过3个你的F题板子就永远在“计数飘忽”4.1 现象数码管显示数字乱跳有时闪8888有时全灭原因seg7.c中段选与位选IO配置错误。F103C8T6的PA8-PA14默认复用为JTAG/SWD调试引脚若未在RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)后立即配置GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)禁用JTAGPA13/PA14会被占用导致段选失效。解决在system_stm32f10x.c的SystemInit()末尾添加RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // 关闭JTAG保留SWD4.2 现象串口发送的数据帧校验和总是错上位机收不到正确计数原因usart.c中校验和计算未包含帧头。F题要求“帧格式0xAA 计数值低字节 计数值高字节 校验和 0x00”但原代码校验和只计算了低字节高字节漏了0xAA。解决修改USART_SendCount()函数中的校验和计算// 原错误代码漏0xAA // uint8_t checksum (paper_count 0xFF) ((paper_count 8) 0xFF); // 正确代码含帧头0xAA uint8_t checksum 0xAA (paper_count 0xFF) ((paper_count 8) 0xFF);4.3 现象接上真实纸张后ADC值在0~4095间随机跳变无法稳定原因硬件未做电源滤波。F103C8T6的VDDA模拟电源必须独立于VDD数字电源供电且需在VDDA与VSSA间加0.1μF陶瓷电容10μF钽电容。多数最小系统板将VDDA直连VDD导致数字开关噪声耦合进ADC参考电压。解决在PCB上VDDA引脚就近焊接10μF钽电容耐压16V到VSSA并确保VDDA走线远离晶振和USB接口。若用杜邦线临时验证可用万用表测VDDA对VSSA电压应稳定在3.3V±10mV。4.4 现象Keil编译报错Error: L6218E: Undefined symbol SystemInit原因startup_stm32f10x_md.s中复位向量指向SystemInit但system_stm32f10x.c未被加入工程。Keil默认不自动添加.c文件需手动右键Source Group 1 → Add Existing Files to Group... → 选择system_stm32f10x.c。解决检查Project → Options for Target → C/C → Include Paths是否包含./USER/和./CMSIS/并确认system_stm32f10x.c在Files标签页中显示为绿色已编译。4.5 现象按下复位键后数码管显示0000但串口无数据输出原因EXTI0_IRQHandler中未清除EXTI挂起位导致中断持续触发CPU陷在中断里无法执行主循环。解决在中断服务函数末尾必须调用EXTI_ClearITPendingBit(EXTI_Line0)且该函数必须在FLASH_Lock()之后——因Flash操作耗时若清标志过早可能在Flash操作完成前再次进入中断。5. 真实纸张标定与性能验证用A4纸实测15组数据教你3步搞定自己板子的精度校准5.1 标定前必做确认传感器输出特性曲线别急着烧录代码先用万用表直流电压档测传感器输出端接PA0对地电压手动叠放1~15张A4纸80g/m²标准厚度0.1mm/张记录每叠对应的电压值。典型电阻式传感器会呈现近似线性关系纸张数实测电压(V)ADC原始值12bit00.00010.3240220.6379230.921156.........153.184005玄学提示若电压随纸张增加反而下降如电容式传感器需在ADC_Filter_Process()中将adc_filtered baseline - fast_avg取反否则状态机永远无法进入DETECT。5.2 修改paper_table[]的三步法以你的实测数据为准第一步确定1张与15张的ADC值从上表取1张对应ADC40215张对应ADC4005则paper_table[1]402paper_table[15]4005。第二步线性分段填充中间值用Excel输入公式402(4005-402)*(ROW()-1)/14生成15个值ROW()从1到15复制粘贴覆盖paper_table[]数组。第三步实测修正非线性段重点验证5张、10张、15张若实测5张时ADC1980但查表返回4说明5~6张区间斜率偏大将paper_table[5]从理论值1950调高至1985paper_table[6]同步上调保持整体单调递增。5.3 性能验证表格你的板子能否通过F题验收标准用标定后的代码对同一叠纸重复测量10次记录计数值与真实值偏差真实张数10次测量结果最大偏差是否达标F题要求≤±0.5张关键观察点11,1,1,1,1,1,1,1,1,10是确认无误触发55,5,5,4,5,5,5,5,5,5-1是偏差1010,10,10,10,9,10,10,10,10,10-1是注意9出现时ADC值是否接近paper_table[9]与paper_table[10]中点1515,15,15,15,15,14,15,15,15,15-1是若14频繁出现需将paper_table[15]下调20~30点后悔药若验证失败不要重写算法优先检查硬件——用示波器看PA0引脚确认纸张放入时是否有干净的阶跃响应上升沿≤10ms。若波形毛刺多加一级RC低通10kΩ100nF再试。从那以后我每次拿到新传感器都强制走一遍“万用表初测→Excel拟合→代码查表→10次重复验证”四步流程哪怕赶电赛 deadline 也绝不跳过。因为 F 题的扣分点从来不在算法多炫酷而在“数错了几张纸”这种看似低级的失误上。希望帮到你。本文还有配套的精品资源点击获取