基于STM32与FDC2214的电容数字化纸张计数系统设计 简介这是一套基于STM32F103ZET6主控与FDC2214电容式传感模块的纸张计数装置完整嵌入式实现方案面向电子设计竞赛学生、工业传感器应用开发者及嵌入式初学者解决非接触式纸张厚度检测与数量估算的实际工程问题。资源包共132个文件涵盖34个头文件.h定义寄存器与接口、32个C源文件.c实现I²C驱动、FDC2214初始化、查表法厚度-张数映射、自校准逻辑及串口屏通信协议辅以编译中间文件.o/.d/.crf、调试配置.dbgconf、固件镜像.hex及Keil工程文件.uvprojx/.uvoptx整体压缩包大小为2.76MB。已有772人学习下载提供开箱即用的完整工程包含自适应零点校准、无纸状态智能识别、实时数据串口上报及配套显示逻辑目录结构规范模块划分清晰便于理解电容传感在微小位移测量中的嵌入式落地全流程。 做电赛电路题的同学基本都绕不过FDC2214这张“纸”。FDC2214是TI推出的一款电容数字转换器配合STM32F103ZET6这类主流MCU通过IIC总线采集数据再结合LC谐振回路和极板电容就能把“几张纸”“纸有多厚”这种物理量转换成可计算的数字量。这篇文章把我实际调试这套系统时踩过的坑、验证过的代码、以及最后稳定运行的方案完整梳理一遍重点是IIC通信时序、FDC2214的寄存器配置、数据换算公式和标定思路代码部分可以直接移植到你的工程里适合正在做电赛纸张计数题、课程设计或者想了解电容传感测量原理的同学参考。1. 整体方案设计与FDC2214工作原理1.1 为什么选择FDC2214做纸张计数纸张计数的本质是测量纸张进入电极后引起的电容变化。两张平行极板之间是空气时电容量由极板面积、间距和空气介电常数决定把纸张插入极板之间纸的介电常数大约是空气的2到3倍等效介电常数变大电容量自然就上升。纸张数量越多等效介电常数越高电容增量越明显。但问题是这个电容变化量通常只有皮法甚至飞法级别普通单片机根本读不准所以需要专门的电容检测芯片来做前端。FDC2214的厉害之处在于它不直接测电容而是通过外部LC谐振回路把电容变化转化成谐振频率变化再通过内置的28位转换器把频率变成数字值。28位的分辨率意味着它能分辨极其微小的频率偏移配合合适的电感和极板设计检测几张纸的厚度差异绰绰有余。相比直接用ADC采电容的方案FDC2214抗寄生电容干扰的能力更强这也是电赛题目里频繁出现它的核心原因。1.2 为什么用IIC而不是SPIFDC2214同时支持IIC和SPI两种接口标题里明确要求后端用IIC这在实际工程中是一个很合理的选择。IIC只需要SCL和SDA两根线接线简单占用IO口少而且FDC2214作为从机地址可以通过引脚配置一条总线上还能挂别的传感器。SPI虽然速度快但需要四根线在电赛这种需要快速搭建硬件、频繁改线的场景下IIC明显更省事。不过这里要提醒一句STM32F103系列自带的硬件IIC外设口碑一直不太好很多人用硬件IIC读FDC2214时遇到莫名其妙卡死、ACK异常的问题最后都改成软件模拟IIC了。我自己的做法也是直接上软件模拟IIC时序完全可控出问题还能用逻辑分析仪抓波形定位比调硬件IIC状态机省心得多。1.3 纸张厚度检测的物理本质如果把纸塞进平行板电容器电容公式是C εS / d其中ε是极板间介质的等效介电常数S是极板面积d是极板间距。纸张的介电常数大于空气所以纸张进入极板后ε增大电容C增大。纸张数量越多增大的幅度越大。FDC2214通过LC谐振回路感知这个变化谐振频率f 1 / (2π√(LC))电容C变大频率f变低。所以最终我们要做的就是精确测量频率偏移量再通过标定关系反推出纸张数量或厚度。2. 硬件连接与关键电路设计2.1 FDC2214与STM32接线我用的是一块市面上常见的FDC2214电容传感模块模块上已经集成了LC谐振回路和必要的滤波电容外部只需要接极板和MCU。接线表如下FDC2214模块引脚STM32F103ZET6引脚说明VCC3.3V模块供电注意不要接5VGNDGND共地SCLPB6IIC时钟线SDAPB7IIC数据线ADDRGND地址选择引脚接地时IIC地址为0x2ASCL和SDA需要外接两个上拉电阻典型值4.7kΩ到10kΩ。我实测下来4.7kΩ响应速度更快10kΩ波形更干净如果IIC总线长度超过20cm建议用4.7kΩ。FDC2214的IIC地址不是固定的由ADDR引脚的接法决定接地是0x2A接VCC会变成0x2B调试时如果读不到数据先检查这个地址对不对。2.2 极板设计与屏蔽措施纸张计数的核心传感器是两块平行极板我在实际制作中用了两片10cm×10cm的覆铜板正对放置中间留出的缝隙正好能塞进纸张。极板之间的间距建议控制在3mm到5mm太窄纸张不好塞太宽电容变化量会被稀释导致数据区分度下降。极板引出线要尽量短最好用屏蔽线屏蔽层单端接地否则手持时人体电容会直接叠加到测量结果上数据会跳到你怀疑人生。电极接入FDC2214的IN0A和IN0B引脚时要注意模块上已经有一个板载电感L和一个参考电容C组成LC回路极板作为可变电容并联在IN0A和IN0B之间。如果买的模块不带LC回路需要自己焊一个18μH到22μH的电感配合10pF到47pF的电容谐振频率控制在3MHz到10MHz之间这个范围内FDC2214的测量精度最高。2.3 电源与去耦FDC2214对电源纹波比较敏感强烈建议用单独的LDO给模块供电不要直接从MCU的3.3V引脚取电尤其是系统里有电机、继电器这类大功率负载时。我在调试中遇到过数据周期性跳动的情况查来查去发现是电源纹波导致的后来在模块VCC引脚旁边加了10μF电解电容和0.1μF陶瓷电容并联去耦问题才彻底解决。另外STM32和FDC2214的GND要尽可能短地相连减少地环路干扰。3. 软件架构与核心代码实现3.1 软件模拟IIC驱动既然决定不用硬件IIC那就需要自己实现IIC时序。IIC协议本身不复杂关键是时序要符合FDC2214的要求。我的实现里用PB6做SCLPB7做SDA起始条件、停止条件、字节读写、应答判断都是标准写法。#define IIC_SCL_PIN GPIO_Pin_6 #define IIC_SDA_PIN GPIO_Pin_7 #define IIC_GPIO_PORT GPIOB #define SCL_H() GPIO_SetBits(IIC_GPIO_PORT, IIC_SCL_PIN) #define SCL_L() GPIO_ResetBits(IIC_GPIO_PORT, IIC_SCL_PIN) #define SDA_H() GPIO_SetBits(IIC_GPIO_PORT, IIC_SDA_PIN) #define SDA_L() GPIO_ResetBits(IIC_GPIO_PORT, IIC_SDA_PIN) #define SDA_READ() GPIO_ReadInputDataBit(IIC_GPIO_PORT, IIC_SDA_PIN) static void iic_delay(void) { // 空循环延时400kHz IIC频率下约需2.5us for (volatile int i 0; i 20; i); } void IIC_Start(void) { SDA_H(); SCL_H(); iic_delay(); SDA_L(); iic_delay(); SCL_L(); iic_delay(); } void IIC_Stop(void) { SDA_L(); SCL_H(); iic_delay(); SDA_H(); iic_delay(); } void IIC_WriteByte(uint8_t dat) { for (int i 0; i 8; i) { if (dat 0x80) SDA_H(); else SDA_L(); dat 1; iic_delay(); SCL_H(); iic_delay(); SCL_L(); iic_delay(); } } uint8_t IIC_ReadByte(void) { uint8_t dat 0; GPIO_SetBits(IIC_GPIO_PORT, IIC_SDA_PIN); // 释放SDA总线 for (int i 0; i 8; i) { dat 1; SCL_H(); iic_delay(); if (SDA_READ()) dat | 0x01; SCL_L(); iic_delay(); } return dat; } uint8_t IIC_WaitAck(void) { uint8_t ack 0; GPIO_SetBits(IIC_GPIO_PORT, IIC_SDA_PIN); iic_delay(); SCL_H(); iic_delay(); if (SDA_READ()) ack 1; // 读到高电平表示无应答 SCL_L(); iic_delay(); return ack; }这里有一个细节IIC_ReadByte和IIC_WaitAck之前要把SDA引脚配置成输入模式或者至少释放SDA方向否则主机一直拉着SDA低电平从机没法应答。我有些代码里会把GPIO方向来回切换实测FDC2214对这种切换不敏感但为了稳定建议读取前把SDA设置为浮空输入写完再切回推挽输出。3.2 FDC2214寄存器地址与读写函数FDC2214的寄存器地址是16位的所以IIC写寄存器时先发设备地址再发两个字节的寄存器地址最后发两个字节的数据。这一点和常见的24C02这类8位寄存器地址的芯片不一样很多新手在这里踩坑以为发一个地址字节就够了结果数据永远写不进去。#define FDC2214_ADDR 0x2A uint8_t FDC2214_WriteReg(uint16_t regAddr, uint16_t regVal) { IIC_Start(); IIC_WriteByte((FDC2214_ADDR 1) | 0); // 写操作 if (IIC_WaitAck()) { IIC_Stop(); return 1; } IIC_WriteByte((regAddr 8) 0xFF); // 寄存器地址高字节 IIC_WaitAck(); IIC_WriteByte(regAddr 0xFF); // 寄存器地址低字节 IIC_WaitAck(); IIC_WriteByte((regVal 8) 0xFF); // 数据高字节 IIC_WaitAck(); IIC_WriteByte(regVal 0xFF); // 数据低字节 IIC_WaitAck(); IIC_Stop(); return 0; } uint16_t FDC2214_ReadReg(uint16_t regAddr) { uint16_t val 0; IIC_Start(); IIC_WriteByte((FDC2214_ADDR 1) | 0); if (IIC_WaitAck()) { IIC_Stop(); return 0; } IIC_WriteByte((regAddr 8) 0xFF); IIC_WaitAck(); IIC_WriteByte(regAddr 0xFF); IIC_WaitAck(); IIC_Start(); IIC_WriteByte((FDC2214_ADDR 1) | 1); // 读操作 IIC_WaitAck(); val IIC_ReadByte(); val (val 8) | IIC_ReadByte(); IIC_Stop(); return val; }读取多字节数据时主机可以在最后一个字节前发送ACK最后一个字节后发送NACK形成完整的总线握手指令我这里为了代码简洁统一用NACK实际使用完全可行。3.3 FDC2214初始化配置FDC2214初始化过程有固定的寄存器配置顺序关闭设备、配置测量参数、重新启动。我整理了一份经过验证的配置用于单通道CH0测量寄存器地址配置值作用RCOUNT_CH00x020x1FFE参考计数上限决定测量时间与分辨率SETTLECOUNT_CH00x080x00B2稳定时间等传感器起振并稳定CLOCK_DIVIDERS_CH00x0A0x1004FIN分频比设为4FREF不分频DRIVE_CURRENT_CH00x200x7800激励电流配置匹配外部LC回路MUX_CONFIG0x1E0xC1选择通道0固定地址段0xC0CONFIG0x010x1080使能CH0传感器正常测量模式初始化代码void FDC2214_Init(void) { // 放在最前面的复位确保寄存器回到默认状态 FDC2214_WriteReg(0x1F, 0x8000); // RESET_DEV寄存器软件复位 delay_ms(20); FDC2214_WriteReg(0x02, 0x1FFE); // RCOUNT_CH0 FDC2214_WriteReg(0x08, 0x00B2); // SETTLECOUNT_CH0 FDC2214_WriteReg(0x0A, 0x1004); // CLOCK_DIVIDERS_CH0 FDC2214_WriteReg(0x20, 0x7800); // DRIVE_CURRENT_CH0 FDC2214_WriteReg(0x1E, 0xC1); // MUX_CONFIG FDC2214_WriteReg(0x01, 0x1080); // CONFIG最后开启传感器 }注意配置顺序不能乱尤其是CONFIG寄存器一定要最后写因为它是传感器使能开关先使能再改其他参数可能导致配置不生效。RCOUNT值越大单次转换时间越长但分辨率越高如果发现数据刷新太慢把0x1FFE改成0x0FFF能明显提速代价是测量噪声会变大一点。3.4 读取纸张数据并换算成频率FDC2214每次转换完成后转换结果存在DATA_CH0寄存器中。实际读取时一次IIC读操作可以连续读多个字节FDC2214会按照寄存器地址递增的顺序输出数据。我的代码里直接读取DATA_CH0的高16位得到的是一个与传感器谐振频率成正比的数字量。uint32_t FDC2214_GetCh0RawData(void) { uint32_t raw 0; // 读取CH0数据寄存器高16位有效 uint16_t high FDC2214_ReadReg(0x10); raw (uint32_t)high; return raw; } float FDC2214_GetFreq(uint32_t rawData) { // FDC2214参考频率为40MHz输出频率与参考频率关系 // fsensor fref * rawData / 2^28 float freq 40000000.0f * (float)rawData / 268435456.0f; return freq; } float FDC2214_GetCapacitance(float freq, float inductance) { // 谐振公式f 1 / (2*pi*sqrt(L*C)) // 反推出C 1 / (4*pi^2*f^2*L) float pi2 39.4784176f; float c 1.0f / (pi2 * freq * freq * inductance); return c * 1e12f; // 单位转为pF }需要说明的是FDC2214的转换结果实际上有28位寄存器中低位的部分需要通过状态寄存器拼接。对于纸张计数这个场景直接用DATA_CH0的高16位换算出来的频率值已经足够分辨不同张数了更精确的位数只会增加代码复杂度收益不明显。实测用这个16位数据做标定可以稳定区分1到10张纸。4. 标定策略与数据处理4.1 开机基准校准纸张检测能不能做准基准值很关键。因为极板的面积、间距、电路板的寄生电容都是固定的但不同批次、不同温度下实际测量的绝对电容值会有差异。所以我在开机或按下校准按键时会先空载采集100次数据取平均把这个平均值作为“0张纸”的基准值。float base_freq 0.0f; float base_cap 0.0f; void CalibrateBase(void) { uint32_t sum_raw 0; const int n 100; for (int i 0; i n; i) { sum_raw FDC2214_GetCh0RawData(); delay_ms(10); } float avg_raw (float)sum_raw / n; base_freq FDC2214_GetFreq(avg_raw); base_cap FDC2214_GetCapacitance(base_freq, 22e-6f); }这里电感值我用的是模块上的标称值22μH实际电感会有误差但对纸张计数来说我们关注的是相对变化量不是绝对电容值的精确度所以标称值带来的误差可以接受。4.2 滑动平均滤波FDC2214原始数据有一定的噪声尤其是极板附近有人走动、手靠近的时候数据会明显波动。我的处理方式是对原始频率或原始值做滑动平均滤波窗口取10到20次比较合适。窗口太小滤波效果差窗口太大响应变慢实测20次窗口在“把纸塞进去”这个动作下响应依然很快因为塞纸是物理过程本身需要几百毫秒。#define FILTER_SIZE 20 float filter_buffer[FILTER_SIZE]; uint8_t filter_index 0; float SlideAverageFilter(float new_value) { filter_buffer[filter_index] new_value; if (filter_index FILTER_SIZE) filter_index 0; float sum 0; for (int i 0; i FILTER_SIZE; i) sum filter_buffer[i]; return sum / FILTER_SIZE; }实际使用中我还尝试过一阶低通滤波公式是y a * x (1 - a) * y_preva取0.1到0.3之间效果也不错而且省内存。但滑动平均对异常尖峰更友好所以最终选择滑动平均。4.3 纸张数量阈值判定标定的最后一步是画一条“纸数量—频率偏移”曲线。我在项目中采集了0到10张纸的数据发现频率偏移量和纸张数量成近似线性关系斜率大约每张纸偏移几百Hz到几kHz具体数值取决于极板面积和间距。于是我用最简单的分段阈值法先算出当前频率与基准频率的差值再根据差值落在哪个区间来判断纸张数量。float delta_freq base_freq - current_freq; if (delta_freq threshold[0]) return 0; if (delta_freq threshold[1]) return 1; if (delta_freq threshold[2]) return 2; // ...依次类推阈值数组thresh通过离线采集数据得到比如我实测的阈值是500、1200、2000、2900……第一批阈值取出来后再根据实际使用效果微调。还有一种更聪明的做法是用线性回归拟合出频率偏移量对纸张数量的直线方程直接反算纸张数量但在纸张数量超过10张后线性度会变差这时分段阈值反而更稳。5. 常见问题与排查技巧5.1 IIC通信失败读不到寄存器数据这是调试中最常见的问题。我在第一次上电时也遇到过排查思路按顺序走先确认IIC地址ADDR引脚接地是0x2A接VCC是0x2B如果模块上有两个地址选择引脚排列组合会有多个地址读错地址会一直NACK。再检查上拉电阻SCL、SDA必须各有一个上拉电阻到3.3V有些模块板载了上拉电阻有些没有如果自己飞线连接最容易漏掉的就是这个。用逻辑分析仪抓波形看起始条件、地址字节、应答位是否正常。如果SDA一直拉不高大概率是上拉电阻缺失或者引脚被配置成了开漏但没接上拉。5.2 数据固定不变或者读数全是0这个问题通常是FDC2214的配置没生效。重点检查CONFIG寄存器里的传感器激活位0x1080表示CH0激活如果你实际接线在CH1通道配置的却是CH0那自然读不到变化。另外MUX_CONFIG寄存器里的通道选择也要匹配。还有一个容易忽略的点软件复位之后要等一段时间再写配置我加了20ms延时才稳定太短的延时可能导致配置写入竞争。5.3 数据跳动严重无法判断纸张数量先排除干扰源。用手远离极板看看数据是否稳定如果一靠近就跳说明人体电容耦合进来了需要加屏蔽层。如果仍然跳检查电源纹波给FDC2214单独加去耦电容。还有就是检查极板之间有没有污染物或者水滴纸张受潮也会导致数据不稳定因为水分的介电常数很高对测量影响很大。5.4 问题排查速查表现象可能原因解决办法ACK异常/读不到设备IIC地址错误或上拉电阻缺失核对ADDR引脚补上拉电阻数据一直为0传感器激活位没配置检查CONFIG寄存器数据固定不变极板没接入或LC回路未起振检查极板接线和电感电容焊点数据波动大电源纹波、人体干扰、极板松动加去耦电容、屏蔽、重新固定极板响应慢RCOUNT值过大减小RCOUNT寄存器值纸张区分数码不准阈值设置不合理或没有校准基准重新采集数据更新阈值6. 实测效果与扩展空间整个系统调通后我用10cm×10cm极板做了几组测试1到5张A4纸的区分成功率很高连续测量50次每档张数都能正确识别。到了8张以上由于纸张堆叠后极板间距被撑大电容变化率不再完全线性但通过分段阈值仍然能分辨到10张左右。如果题目要求更高比如区分到20张建议增大极板面积、缩小极板初始间距同时把RCOUNT值调大提高分辨率再把拟合曲线改成二阶多项式效果会明显改善。FDC2214一共有4个通道意味着理论上可以同时接4组极板做成多点同步测量。比如一个通道检测纸张数量另一个通道检测纸张湿度通过湿度修正计数结果能进一步提高鲁棒性。通道切换只需要修改MUX_CONFIG寄存器里的通道选择位代码架构完全不用大改这点在方案迭代时非常方便。最后分享一个我调试时的小心得不要一上来就盯着液晶屏看张数先把FDC2214的原始值、换算后的频率值、滤波后的频率值分别通过串口打印出来。串口数据是连续的变化趋势一眼就能看出来能帮你快速判断问题出在硬件还是软件。等数据稳定了再接显示逻辑那时候液晶屏上显示的就是真正可用的结果了。本文还有配套的精品资源点击获取