开篇先交代一句我最早碰FSR薄膜压力传感器是为了做一个廉价的座椅占用检测模块。当时手里正好有一块吃灰的STC89C52开发板和一块STM32F103C8T6最小系统板于是就把两颗芯片都跑了同一套FSR方案。做完后最大的感受是FSR这东西原理一句话能说完但真把它接到单片机上读出稳定、可用的数据中间有不少坑。这篇我就把完整的过程、代码和踩坑记录都摊开讲从传感器原理到两套MCU的接线与代码再到标定和滤波争取让新手照着做也能一次跑通。1. 先把FSR的脾气摸透阻值变化才是核心1.1 FSR到底怎么工作为何读数会飘FSR的全称是Force Sensing Resistor中文一般叫力敏电阻或薄膜压力传感器。它本质是个压敏电阻膜片表面涂有特殊的压敏材料没受力时内部导电粒子间距大电阻非常高一旦施加压力导电粒子被压得越来越近形成更多导电通路电阻值会迅速下降。我手里这块是常见的圆形FSR直径大概17mm实测数据大致是这样状态阻值范围无压力空载大于100kΩ甚至接近1MΩ轻轻按压约0.5N30kΩ ~ 80kΩ中等按压约2N5kΩ ~ 15kΩ用力按压约10N300Ω ~ 2kΩ注意几个关键特征第一FSR不是线性元件阻值随压力的变化曲线是对数关系低压段特别灵敏高压段逐渐饱和。所以你想用FSR做精确称重从硬件原理上就走不通它的定位是“力感”而不是“秤”。第二FSR有一个起始阈值力。并不是手一碰就有阻值变化通常需要施加至少20g左右的力才开始导通。表面看这是个缺点但反过来在一些需要防误触的场景里反而是优点。第三FSR的重复性和温漂都比较一般同一压力下多次测量阻值可能有10%到20%的偏差温度变化也会让读数缓慢漂移。这不是你接线的问题是这个传感器本身的性质决定的。1.2 分压电路让“电阻”变成“电压”的唯一正确姿势单片机只能读电压读不了电阻所以必须搭一个分压电路把FSR的阻值变化转化为电压变化。标准接法是电源正极接FSR一端FSR另一端接一个固定电阻固定电阻另一端接地。FSR和固定电阻的中间节点引到单片机的ADC引脚。假设电源电压为VCC固定电阻为RFSR当前阻值为R_FSR那么ADC引脚上的电压是V_ADC VCC × R / (R_FSR R)这个公式是整套逻辑的核心。也有些人会把FSR和固定电阻的位置对调那是反着来的固定电阻接电源FSR接地。这样电压会随压力增大而增大。两种接法都行但要记住公式不一样。我习惯把FSR放上面、固定电阻放下面对地因为这样压力越大ADC电压越高逻辑上更直观。1.3 怎么选下拉电阻不是随手拿个10k就行固定电阻R的取值直接决定了传感器响应范围和灵敏度这是新手最容易忽略的地方。先说结论FSR空载时阻值很大会让输出电压趋近于0用力按压时阻值可能掉到几百欧会让输出电压趋近于VCC。但整个动态范围并不是均匀变化的如果你选的R不合理很可能出现“轻轻一碰就ADC满量程”或者“用力按到最大才跳几十个数字”的情况。我常用的经验是把R选在FSR常用受力区间阻值的几何中心附近。比如你主要测2N左右的力量这个力下FSR大概是5kΩ到15kΩ取中值大约是10kΩ则R选10kΩ比较合适。如果主要做高灵敏度轻触检测比如做触摸开关可以选20kΩ或47kΩ让低压段变化更明显。还要注意电流问题。FSR本身有最大功率耗散限制一般建议通过FSR的电流不要超过1mA。用5V供电、FSR阻值200Ω时回路电流是5V / (200Ω 10kΩ) ≈ 0.49mA没问题。但如果固定电阻选的太小比如1kΩ相同情况下电流就飙到4.16mA长期工作会让传感器发热、读数漂移甚至损坏。我的推荐配置应用场景固定电阻R备注轻触/接近检测47kΩ灵敏度高一般压力检测10kΩ最常用坐压/踩踏检测4.7kΩ受力偏大2. STC89C52没ADC用PCF8591外扩I2C读压力2.1 先确认你的“STC89C52开发板”到底有没有ADC这是STC89C52玩家最容易踩的坑之一。STC89C52RC这个芯片本身是没有片内ADC模块的P1口是准双向IO口会被一些教程误传成“P1口可以读模拟电压”但实际它只能读高低电平测不了连续电压。很多淘宝开发板上标着“带ADC功能”指的通常是板子额外焊了一颗ADC芯片最常见的是ADC0804或PCF8591。STC89C52只是通过IO口去读取这颗外部ADC的数字结果而已。所以你拿到开发板后先翻原理图确认自己板子上的ADC是哪种方案如果是ADC0804它是并行接口占用8个IO口读取速度一般但编程简单如果是PCF8591它是I2C接口只占用两根IO线SCL/SDA还能顺便多读3路模拟量。我强烈推荐PCF8591因为省IO而且STM32那边以后也可以用。本文以PCF8591为例。2.2 PCF8591硬件接线和I2C地址设置PCF8591是一片8位逐次逼近型ADC支持4路模拟输入通过I2C总线访问。它的地址由A0、A1、A2三个引脚电平决定地址格式是1001 A2 A1 A0 R/W三个地址引脚都接地时器件写地址是0x90读地址是0x91。如果你想挂多片PCF8591可以把A0、A1、A2接到VCC来改地址。接线表PCF8591引脚STC89C52引脚SCLP2.0SDAP2.1A0/A1/A2GNDVCC5VGNDGNDAIN0FSR分压节点注意PCF8591官方标称供电范围是2.5V到6V所以可以直接用STC89C52开发板的5V供电。I2C上拉电阻一般不需要额外接因为很多模块上已经自带上拉如果是裸芯片则需要在SCL和SDA上分别接4.7kΩ到VCC的上拉电阻。FSR分压电路里固定电阻R选10kΩ分压节点接PCF8591的AIN0。2.3 手写I2C时序Keil C51完整读取代码STC89C52没有硬件I2C外设所以必须用软件模拟I2C时序。I2C的基本动作就四个起始信号、停止信号、发送字节、读取字节。只要把这几个函数写稳后面的读ADC就非常顺利。我用的是Keil C51工程里新建三个文件i2c.h、i2c.c、main.c。i2c.c实现#include reg52.h #include intrins.h #define I2C_DELAY() {_nop_();_nop_();_nop_();_nop_();} sbit I2C_SCL P2^0; sbit I2C_SDA P2^1; void I2C_Start(void) { I2C_SDA 1; I2C_SCL 1; I2C_DELAY(); I2C_SDA 0; I2C_DELAY(); I2C_SCL 0; } void I2C_Stop(void) { I2C_SDA 0; I2C_SCL 1; I2C_DELAY(); I2C_SDA 1; } void I2C_SendByte(unsigned char dat) { unsigned char i; for(i 0; i 8; i) { I2C_SDA (dat 0x80) ? 1 : 0; dat 1; I2C_SCL 1; I2C_DELAY(); I2C_SCL 0; } } unsigned char I2C_RecvByte(void) { unsigned char i, dat 0; I2C_SDA 1; for(i 0; i 8; i) { dat 1; I2C_SCL 1; I2C_DELAY(); if(I2C_SDA) dat | 0x01; I2C_SCL 0; } return dat; } unsigned char I2C_WaitAck(void) { unsigned char timeout 200; I2C_SDA 1; I2C_SCL 1; while(I2C_SDA) { if(--timeout 0) return 1; } I2C_SCL 0; return 0; } void I2C_Ack(void) { I2C_SDA 0; I2C_SCL 1; I2C_DELAY(); I2C_SCL 0; I2C_SDA 1; } void I2C_Nack(void) { I2C_SDA 1; I2C_SCL 1; I2C_DELAY(); I2C_SCL 0; }PCF8591读取函数平移到main.c#include reg52.h #include i2c.h #define PCF8591_WRITE 0x90 #define PCF8591_READ 0x91 unsigned char PCF8591_ReadADC(unsigned char ch) { unsigned char dummy, value; I2C_Start(); I2C_SendByte(PCF8591_WRITE); I2C_WaitAck(); I2C_SendByte(0x40 | ch); // 控制字使能ADC选择通道 I2C_WaitAck(); I2C_Start(); I2C_SendByte(PCF8591_READ); I2C_WaitAck(); dummy I2C_RecvByte(); // 第一次读的是上一次转换结果丢弃 I2C_Ack(); value I2C_RecvByte(); // 第二次读才是当前通道有效值 I2C_Nack(); I2C_Stop(); return value; } void UART_Init(void) { SCON 0x50; TMOD 0x20; TH1 0xFD; // 11.0592MHz下设置9600波特率 TL1 0xFD; TR1 1; } void main(void) { unsigned char adc_val; UART_Init(); while(1) { adc_val PCF8591_ReadADC(0); SBUF adc_val; while(!TI); TI 0; } }这段代码里的核心是控制字节写0x40是“使能ADC并选择AIN0”如果你有多路FSR希望切换通道直接把ch改成0、1、2、3即可。2.4 串口打印和读数稳定性处理STC89C52的51核串口发送很简单但直接用串口助手看二进制数不方便。我建议把ADC值转成三位十进制字符串再发这样观察起来更舒服。代码可以这样改void UART_SendString(char *str) { while(*str) { SBUF *str; while(!TI); TI 0; } } void main(void) { unsigned char adc_val; char buf[4]; UART_Init(); while(1) { adc_val PCF8591_ReadADC(0); buf[0] adc_val / 100 0; buf[1] adc_val % 100 / 10 0; buf[2] adc_val % 10 0; buf[3] \0; UART_SendString(buf); UART_SendString(\r\n); } }实测下来PCF8591的8位分辨率在FSR动态范围内已经够用于判断“有没有压力”“压力是大是小”。但1个LSB代表约20mV5V/255在低压段FSR高阻时可能看到明显的数字跳变。可以用一个非常简单的软件滤波连续采样8次去掉最大值和最小值剩下6次取平均。这个办法比单纯求平均抗脉冲干扰能力强很多。3. STM32内置ADC三路滤波一步到位3.1 FSR能不能直接接5V给STM32注意电平适配STM32F103的ADC输入范围是0到3.3V如果你把5V供电的FSR分压电路直接接到PA1脚分压节点电压一旦超过3.3V大概率会烧坏ADC引脚。解决办法就一个STM32这侧的FSR分压电路用3.3V供电。FSR是纯阻性元件供电电压低一点不影响它的阻值变化特性只是输出电压范围变成0到3.3V而已。接线表FSR电路STM32F103C8T6FSR上方接VCC3.3V固定电阻R下端GND分压节点PA1ADC1_IN1可选串联1kΩ保护电阻PA1前我通常会串一个1kΩ的限流电阻在PA1和分压节点之间万一软件配置错误导致引脚被配置成输出高电平也能限制灌电流。这是多年养成的小习惯新手可以照抄。3.2 用CubeMX配置ADC1_IN1和UARTSTM32开发走的是HAL库路线配置用STM32CubeMX生成效率最高。关键配置点PA1配置为模拟输入ADC1开启选择通道1采样时间选239.5个周期分辨率12位开启连续转换模式串口1配置为115200-8-N-1时钟树把ADC时钟分频到12MHz以内过高会影响ADC精度。需要注意一个细节STM32F103的ADC采样时间不是越长越好。采样时间长输入阻抗要求低读数更稳但采样太慢又会拖低整个系统的吞吐率。对于FSR这种信号源阻抗通常在kΩ级别的传感器采样时间选55.5周期或239.5周期都行我直接选了最大值239.5周期稳定性优先。3.3 HAL库读取中值平均滤波代码ADC底层配置由CubeMX自动生成核心代码写在用户区。我的习惯是结合中值滤波和滑动平均两层处理每轮采10个数据去掉头尾极值再求平均。#include main.h extern ADC_HandleTypeDef hadc1; extern UART_HandleTypeDef huart1; #define SAMPLE_NUM 10 static uint16_t adc_sample[SAMPLE_NUM]; uint16_t FSR_GetMedianAverage(void) { uint16_t tmp; uint32_t sum 0; for (int i 0; i SAMPLE_NUM; i) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_sample[i] HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); } // 冒泡排序把极值过滤掉 for (int i 0; i SAMPLE_NUM - 1; i) { for (int j i 1; j SAMPLE_NUM; j) { if (adc_sample[j] adc_sample[i]) { tmp adc_sample[i]; adc_sample[i] adc_sample[j]; adc_sample[j] tmp; } } } // 去掉最小2个和最大2个取中间6个的平均 for (int i 2; i SAMPLE_NUM - 2; i) sum adc_sample[i]; return (uint16_t)(sum / (SAMPLE_NUM - 4)); } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_USART1_UART_Init(); uint16_t fsr_adc; uint8_t msg[16]; while (1) { fsr_adc FSR_GetMedianAverage(); // 格式化输出方便上位机查看 sprintf((char *)msg, ADC:%d\r\n, fsr_adc); HAL_UART_Transmit(huart1, msg, strlen((char *)msg), 100); HAL_Delay(50); } }这套代码我在F103上实测空载时读数稳定在3到5之间跳很少出现大的毛刺用手轻按后读数能迅速上升到300以上响应速度足够快。相比STC89C52那套8位ADCSTM32的12位分辨率在低压段的表现明显更细腻。3.4 把ADC值折算成电压和大致压力STM32的12位ADC满量程是4095对应3.3V所以电压公式是V_ADC 3.3 × adc / 4095反推FSR阻值也简单根据分压公式R_FSR 3.3 × R / V_ADC - R如果你想进一步估算压力可以做一个粗略的线性映射但要注意FSR力-阻值并非线性直接用线性公式在低压段会有很大误差。更实用的办法是取几个特征点做分段插值比如压力等级ADC典型值R10kΩ3.3V供电无压力 10轻触100 ~ 300正常按压800 ~ 1500用力按压2200 ~ 3500我的建议是不要过分依赖精确压力数值FSR更适合做档位判断例如“无压力/轻触/重压”三档。这样误差再大也能保证判断正确。4. 标定与阈值判断让传感器“有手感”4.1 用手头砝码做两点标定拟合线性关系尽管FSR是非线性的但如果你把受力范围控制在一个小区间内例如1N到5N局部可以近似看成线性。做法很简单放一个已知质量的砝码比如200g约2N在传感器中心记录ADC值为A1再放一个500g砝码约5N记录ADC值为A2。然后可以得到一个线性比例系数K (A2 - A1) / (F2 - F1)之后读到的某个ADC值A对应的力大约就是F (A - A1) / K F1注意砝码要放在传感器有效感应区域的正中央FSR的感应区不是整片膜都均匀边缘受力会让读数明显偏低。4.2 滞回比较避免临界值抖动用FSR做阈值判断时最烦人的就是压力正好卡在阈值附近输出会在“有/无”两个状态之间疯狂抖动。解决办法是给阈值加滞回区也就是设两个阈值一个高阈值VH用于判断“确实有压力”一个低阈值VL用于判断“压力消失了”。只有当读数大于VH时才输出有压力之后即使读数略微下降只要不低于VL就继续保持有压力状态。代码逻辑我习惯这么写#define TH_HIGH 600 #define TH_LOW 300 uint8_t fsr_state 0; // 0:无压力 1:有压力 void Update_FsrState(uint16_t adc) { if (adc TH_HIGH) fsr_state 1; else if (adc TH_LOW) fsr_state 0; }这种滞回逻辑在座椅占用检测、扶手抓握检测里特别好用不会被人的微小晃动或者传感器的温漂搞得不稳定。4.3 真实应用座椅占用检测和抓握检测的算法思路座椅占用检测是我当时做FSR的第一需求。把一片FSR平铺在坐垫下方人坐下时传感器受到持续压力ADC值会从个位数跳到几百上千。算法上有个关键点判断的不是绝对ADC值而是变化量。因为垫子本身的材质、环境温度都会让读数有一个基础偏移如果做成“ADC大于500就判定有人”可能空椅子也会误报。所以我在代码里维护一个校准值baseline设备上电后前2秒连续采样作为空载基线之后判断条件改成if (adc - baseline TH_HIGH) { state 1; // 有人 }抓握检测则反过来关注的是“从无到有”的突变和“从有到无”的回落。可以在主循环里记录上一次读数如果发现读数在短时间内跨越了滞回区间就认为完成了一次握持动作。这个思路用到手势控制或智能家居的按压开关上都很顺手。5. 实战排坑我从这块薄膜传感器踩出来的经验5.1 “这数值怎么一直涨”——温度与蠕变我早期调试时发现一个诡异现象用手持续按压FSRADC值一开始上升很快但随着时间推移还在慢慢往上爬爬到几十秒都停不下来。最开始我以为是分压电阻温漂排查了半天电阻温漂系数很小根本达不到这个程度。后来查资料才确认这是FSR的蠕变creep特性在持续压力下内部导电粒子的分布会继续变化导致阻值持续缓慢下降。这个现象带来的启示是FSR不适合做“恒定压力长时间监测”比如称重、压力维持检测如果必须长时间检测建议只取相对变化值并且周期性重新标定基线软件上可以做差分检测只对一定时间窗口内的变化量做响应忽略缓慢漂移。5.2 插头和线材导致的神奇断连FSR模块很多用的是2.54mm排针或FPC软排线。我遇到过好几次“昨天还好好的今天读数全部飘了”的情况用万用表一量发现是排针和杜邦线之间接触电阻变得很大。FSR本身是高阻器件空载时上百kΩ接触电阻多出几十欧姆根本不影响但当你用力按压传感器阻值掉到几百欧姆时接触电阻占比就开始上升读数就会变得混乱。解决办法尽量把FSR引脚焊死不要用杜邦线直插FPC软排线接口要确保锁扣压紧如果需要经常拆装用PH2.0或XH2.54带锁扣端子比杜邦线可靠得多。5.3 软件滤波与硬件电容的取舍软件滤波我在上面已经给了代码但硬件层面也可以在分压节点加一颗小电容。具体做法是在ADC引脚对地并联一个100nF到1uF的陶瓷电容和分压电阻组成低通滤波器能明显抑制高频干扰。不过要注意电容太大会让响应变慢。FSR本身响应速度很快但如果你需要检测快速敲击动作电容超过1uF后波形会明显变钝可能丢了短脉冲。我一般先不加电容跑通逻辑确认真有干扰再加100nF试。5.4 防静电与机械安装薄膜压力传感器表面是高分子材料非常容易积累静电。冬天干燥环境下用手直接触摸FSR引脚可能会瞬间产生几kV的静电直接击穿后端的ADC输入。建议是把FSR放在产品外壳内部不要让用户直接摸到传感器本身如果需要裸露使用可以在ADC引脚对地并联一颗TVS管或者至少串一个1kΩ限流电阻。机械安装上FSR感应区需要贴合受力面但不要用硬胶水把整个感应区粘死因为胶水固化后产生的应力会让传感器一直处于受力状态基线会漂得一塌糊涂。正确做法是用双面胶只贴传感器边缘的引线区感应区保持自由活动。6. 还能怎么玩阵列化、可视化、无线化如果只有一片FSR能玩的花样有限。但当你把多片FSR排成阵列很多有趣的应用就出来了。我用8片FSR排成2x4阵列铺在凳面上可以大致判断坐姿是偏左还是偏右长时间坐姿错误时还能给出提醒。实现思路是给每片FSR独立分压然后通过多路ADCPCF8591最多4路STM32内部ADC通道更多轮询读取。STC89C52方案可以挂两片PCF8591用A0引脚区分地址一共读8路STM32方案直接用PA0到PA7开启ADC1的8个通道循环采集即可。数据可视化方面最简单的是把ADC数据通过串口发给PC用一个简单的Python脚本画实时曲线import serial import matplotlib.pyplot as plt ser serial.Serial(COM3, 115200) data [] while True: line ser.readline().decode().strip() try: val int(line.split(:)[1]) data.append(val) plt.cla() plt.plot(data[-100:]) plt.pause(0.05) except Exception: passSTM32端只要继续保持串口输出格式不变这个脚本就能直接用实现“剪切板级的波形显示”。它在标定过程中特别有用能直观看到按压过程中ADC值的走势判断传感器是否处在最佳工作区间。最后分享一下我个人这段时间做FSR项目的心得FSR的硬件电路非常简单真正的复杂度全在“如何让数据稳定可信”这一步。无论是STC89C52外扩PCF8591还是STM32的内置ADC核心思路都是一样的正确选固定电阻、做软件滤波、加滞回判断、留好校准入口。把这四件事做到位一个FSR就能从“能读数”变成“可用的压力检测模块”。后面如果要做蓝牙无线传输只要把串口数据包转发给蓝牙模块就行软件架构基本不用动。
