开篇先交代一句:我最早碰FSR薄膜压力传感器,是为了做一个廉价的座椅占用检测模块。当时手里正好有一块吃灰的STC89C52开发板和一块STM32F103C8T6最小系统板,于是就把两颗芯片都跑了同一套FSR方案。做完后最大的感受是:FSR这东西,原理一句话能说完,但真把它接到单片机上读出稳定、可用的数据,中间有不少坑。这篇我就把完整的过程、代码和踩坑记录都摊开讲,从传感器原理到两套MCU的接线与代码,再到标定和滤波,争取让新手照着做也能一次跑通。
1. 先把FSR的脾气摸透:阻值变化才是核心
1.1 FSR到底怎么工作,为何读数会飘
FSR的全称是Force Sensing Resistor,中文一般叫力敏电阻或薄膜压力传感器。它本质是个压敏电阻:膜片表面涂有特殊的压敏材料,没受力时内部导电粒子间距大,电阻非常高;一旦施加压力,导电粒子被压得越来越近,形成更多导电通路,电阻值会迅速下降。
我手里这块是常见的圆形FSR,直径大概17mm,实测数据大致是这样:
| 状态 | 阻值范围 |
|---|---|
| 无压力(空载) | 大于100kΩ,甚至接近1MΩ |
| 轻轻按压(约0.5N) | 30kΩ ~ 80kΩ |
| 中等按压(约2N) | 5kΩ ~ 15kΩ |
| 用力按压(约10N) | 300Ω ~ 2kΩ |
注意几个关键特征:
第一,FSR不是线性元件,阻值随压力的变化曲线是对数关系,低压段特别灵敏,高压段逐渐饱和。所以你想用FSR做精确称重,从硬件原理上就走不通,它的定位是“力感”而不是“秤”。
第二,FSR有一个起始阈值力。并不是手一碰就有阻值变化,通常需要施加至少20g左右的力才开始导通。表面看这是个缺点,但反过来在一些需要防误触的场景里反而是优点。
第三,FSR的重复性和温漂都比较一般,同一压力下多次测量,阻值可能有10%到20%的偏差,温度变化也会让读数缓慢漂移。这不是你接线的问题,是这个传感器本身的性质决定的。
1.2 分压电路:让“电阻”变成“电压”的唯一正确姿势
单片机只能读电压,读不了电阻,所以必须搭一个分压电路,把FSR的阻值变化转化为电压变化。
标准接法是:电源正极接FSR一端,FSR另一端接一个固定电阻,固定电阻另一端接地。FSR和固定电阻的中间节点,引到单片机的ADC引脚。
假设电源电压为VCC,固定电阻为R,FSR当前阻值为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引脚 |
|---|---|
| SCL | P2.0 |
| SDA | P2.1 |
| A0/A1/A2 | GND |
| VCC | +5V |
| GND | GND |
| AIN0 | FSR分压节点 |
注意: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代表约20mV(5V/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电路 | STM32F103C8T6 |
|---|---|
| FSR上方接VCC | 3.3V |
| 固定电阻R下端 | GND |
| 分压节点 | PA1(ADC1_IN1) |
| 可选串联1kΩ保护电阻 | PA1前 |
我通常会串一个1kΩ的限流电阻在PA1和分压节点之间,万一软件配置错误导致引脚被配置成输出高电平,也能限制灌电流。这是多年养成的小习惯,新手可以照抄。
3.2 用CubeMX配置ADC1_IN1和UART
STM32开发走的是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位ADC,STM32的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典型值(R=10kΩ,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 “这数值怎么一直涨?”——温度与蠕变
我早期调试时发现一个诡异现象:用手持续按压FSR,ADC值一开始上升很快,但随着时间推移还在慢慢往上爬,爬到几十秒都停不下来。最开始我以为是分压电阻温漂,排查了半天电阻温漂系数很小,根本达不到这个程度。后来查资料才确认,这是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独立分压,然后通过多路ADC(PCF8591最多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就能从“能读数”变成“可用的压力检测模块”。后面如果要做蓝牙无线传输,只要把串口数据包转发给蓝牙模块就行,软件架构基本不用动。