玩嵌入式这些年,FSR薄膜压力传感器算是我在项目里复用率最高的传感器之一。它便宜、轻薄、耐折腾,STC89C52和STM32这两款玩家里最常见的单片机,都能通过它感知“轻重缓急”——按压力度、触碰有无、坐姿偏移、足底步态,全靠这一层薄薄的压敏膜撑起来。
不过这东西有个特点:原理简单,用起来坑不少。同一个FSR,在51单片机上没ADC,你得想办法扩展采集电路;换到STM32,内置12位ADC直接读,可不同采样时间、不同分压电阻,读出来的数据又是另一副面孔。网上相关代码满天飞,但大多只贴一段,不说为什么这么接、为什么这么配,你抄过来往往不是读数飘就是标定不准。
这篇文章就把我从选型、电路、代码到标定的完整踩坑过程写出来,两种平台都给出可直接复用的代码和参数选择思路,适合正在做毕设、DIY或者从51过渡到32的工程师参考。
1. FSR传感器原理与选型要点
1.1 压阻效应:先把“电阻变压力”这件事搞明白
FSR全称Force Sensing Resistor,中文叫力敏电阻,本质上是一个阻值会随施加压力变化的可变电阻,利用的是压阻效应。你可以把它理解成一个“被压得越狠电阻越小”的特殊电阻:手指轻轻搭上去,阻值可能还有几十千欧;用力按压到满量程,阻值能掉到几百欧甚至更低。关键参数是未受力时的阻值,一般规格书会写“大于1MΩ”。
这个特性决定了它无法直接给单片机读。MCU读不了电阻,只能读电压,所以必须先把电阻变化转换成电压变化。最基础也最常用的办法就是电阻分压,这个后面单独讲。另外FSR不是精密器件,它更适合做“有无压力”“压力大致大小”“压力趋势变化”这类场景,比如触碰检测、防夹手检测、座椅占位检测、步态分析、机器人足底反馈,这些场景对绝对精度要求不高,但对响应速度和重复性有一定要求。
选型时最容易忽略的一点是迟滞。FSR在加载和卸载过程中,即使压力相同,读数也会有差异,典型迟滞在10%以内。也就是说,你缓慢加大压力到500g读到一个值,再从500g慢慢减到0,同样在500g时刻读到的值会有偏差,这在做阈值判断时尤其要留意,后面我专门说解决办法。
1.2 常见型号和参数选择:量程、面积、封装哪个最重要
市面上最常用的是Interlink的FSR 400系列,比如FSR402和FSR406。FSR402的激活区直径是12.7mm,适合做单点按压检测;FSR406的激活区直径是38.1mm,面积更大,适合坐垫、床垫这类需要更大感应面的场景。两者的量程类似,大致在100g到10kg之间,响应时间在1到3毫秒级别,对绝大多数嵌入式项目够用。
高端一些的还有FlexiForce系列,比如A401,同样叫薄膜压力传感器,但它内部做了信号调理和标定,精度比普通FSR好不少,当然价格也贵一个数量级。我的建议是:做验证原型或者课程设计,直接用FSR402,二十块以内能搞定;如果做产品或者需要相对精确的力反馈,再看FlexiForce。
选择时还要关注三个点。第一是量程,FSR在不同力区间内电阻变化是非线性的,轻压区灵敏度高、重压区容易饱和,所以先确定你关心的压力范围,再选对应量程的型号。第二是激活区面积,点按用小的,大面检测用大的,面积和灵敏度有直接关系。第三是出线方式,有焊脚、有带连接器的,批量做样机时带连接器的省事得多,手工焊FSR的银浆导线很容易脱落,这点要有心理准备。
1.3 分压电路:接法方向决定初始输出是高还是低
把FSR接入MCU之前,必须设计好分压电路。我常用的接法是FSR接在电源和采样点之间,固定电阻接在采样点和地之间,电路结构是这样的:
VCC(5V或3.3V) ---- FSR ----+---- R_fix(固定电阻) ---- GND | ADC采样点此时采样点电压Vout = VCC * R_fix / (R_fsr + R_fix)。没按压力的时候,R_fsr非常大,分母大,Vout接近0V;按下后R_fsr变小,Vout升高。这个接法的好处是“无压力时输出接近0”,软件上天然安全,不会出现悬空误触发。
固定电阻R_fix的取值是整个电路设计里最关键的一步。原则很简单:在你最关心的那个压力点附近,让R_fix和FSR的阻值处于同一数量级,这样分压中点附近的电压变化斜率最大,灵敏度最高。举个例子,如果项目重点监测0到1kg范围的按压,实测FSR在1kg时阻值大概在几kΩ到十几kΩ,R_fix取10kΩ就很合适,既能覆盖轻压区间,又不会在重压时太快饱和。如果只做轻触检测,R_fix可以取20kΩ甚至47kΩ;如果做的是重压报警,R_fix取1kΩ到4.7kΩ更合理。这个参数强烈建议在原型阶段用可调电阻或者多准备几个阻值实测对比,别只看规格书硬算,FSR个体差异比较大。
2. 双平台硬件方案对比:STC89C52差个ADC,STM32直接吃
2.1 硬件选型与总体电路设计
两款单片机的核心差异就在ADC上。STM32F103C8T6内置12位ADC,直接采集分压点就完事;STC89C52完全没有ADC模块,必须外挂采集芯片,这是整个项目最容易被新手卡住的地方。
外挂ADC的方案有几种:ADC0832是8位串行ADC,两路输入,接口简单、代码量小,新手友好,缺点是分辨率一般,5V参考下量化步长约19.5mV;PCF8591是I2C接口的8位ADC,胜在I2C可以挂多个设备,但读时序稍麻烦;ADC0804是8位并行ADC,精度一样但占IO太多,不推荐在51上折腾。如果预算和供货允许,也可以直接换成带ADC的国产51单片机,比如STC12C5A60S2,内置10位ADC,省掉外挂芯片,但前提是你能接受换MCU平台。
从我的实测经验看,STC89C52加ADC0832这个组合最稳定,时序简单,代码几十行就能写完,不会引入I2C那种时序调试成本,所以下面的51部分我主要讲这个方案。STM32这边就省心很多,F103的ADC1通道0接到PA0,再配合DMA或者简单轮询都可以,接线一共就三根:电源、地、信号线。
整体电路还需要注意一点:滤波电容。在ADC采样点和GND之间并联一个0.1uF的陶瓷电容,能明显抑制FSR表面接触噪声和高频干扰。这个电容离单片机ADC引脚越近越好,我用过加和不加两种板子,不加的时候串口打印的数值肉眼可见地抖,加了之后稳定很多。另外FSR的信号线不要跟电机驱动线、PWM线绑在一起走,干扰会很严重。
2.2 STC89C52方案:外挂ADC0832的完整接法
ADC0832引脚很少,一共8个脚。和STC89C52连接时,CS接P1^0,CLK接P1^1,DIO接P1^2,VCC和VREF都接5V。分压电路的VCC也统一用5V,这样ADC参考电压就是5V,后期换算电压时直接用。
一个容易踩的细节:STC89C52的P1口是准双向IO,读外部信号之前必须先往引脚写1,否则读进来的电平可能不对。ADC0832的DIO是双向数据线,在读取数据阶段会从推挽输出切换成高阻输入,51侧必须先把DIO拉高再采样。不少抄来的代码没有这个细节,导致读到的数据要么全0要么乱跳。
我建议在ADC0832的VCC和GND之间也加一个0.1uF退耦电容,这芯片虽然便宜,但供电不稳会直接影响转换精度。如果项目对读数稳定性要求高,还可以在DIO线上串联一个1kΩ电阻再做输入保护,防止插拔瞬间的毛刺损坏IO。
2.3 STM32方案:内置12位ADC的接线与初始化
STM32这边接线更简单。以STM32F103C8T6为例,ADC1的通道0固定在PA0引脚。分压电路VCC一定接3.3V,因为STM32的ADC输入范围是0到3.3V,接5V分压会直接烧引脚。这是我在帮人排查问题时见过最多的事故,切记。
初始化时有个关键参数很多人忽略:采样时间。FSR轻压时阻值很大,信号源等效阻抗高,如果ADC采样时间太短,内部采样电容还没充好电就开始转换,结果就是读数偏低而且非线性。我建议把采样时间设到最长,也就是239.5个ADC时钟周期。具体到代码里,就是ADC_SAMPLETIME_239CYCLES_5这个宏。实测用1.5周期采样和239.5周期采样,同一块FSR在轻压区读数能差20%以上,这个坑非常隐蔽。
另外F103的ADC最大时钟不能超过14MHz,一般主频72MHz下选6分频得到12MHz,稳定可靠。HAL库配置里对应ADC_CLOCK_SYNC_PCLK_DIV6。如果你用的是F407或者H7系列,ADC时钟树略有差别,但原则一样:先查数据手册确认ADC输入时钟上限,别默认配置就完事。
3. 完整代码:从裸读ADC到输出压力值
3.1 STC89C52 + ADC0832读取FSR代码
下面这段代码是在Keil C51环境下写的,晶振11.0592MHz,串口9600波特率。代码里包含ADC0832时序驱动、串口打印数字的函数,以及一个简单的多次采样取平均,实测在STC89C52RC上稳定运行。
/************************************************ * STC89C52 + ADC0832 读取FSR薄膜压力传感器 * 分压电路:VCC(5V) -> FSR -> 采样点 -> R_fix(10k) -> GND * 采样点接ADC0832的CH0 * 串口输出:ADC原始值(0~255) 和 估算电压(mV) ************************************************/ #include <reg52.h> #include <intrins.h> typedef unsigned char u8; typedef unsigned int u16; sbit ADC_CS = P1^0; // 片选,低有效 sbit ADC_CLK = P1^1; // 时钟 sbit ADC_DIO = P1^2; // 数据线,双向 void Delay_Ms(u16 ms) { u16 i, j; for(i = 0; i < ms; i++) for(j = 0; j < 123; j++); } void Uart_Init(void) { SCON = 0x50; // 模式1,允许接收 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1,模式2 TH1 = 0xFD; // 9600bps @ 11.0592MHz TL1 = 0xFD; TR1 = 1; } void Uart_SendByte(u8 dat) { SBUF = dat; while(TI == 0); TI = 0; } void Uart_SendString(char *s) { while(*s) Uart_SendByte(*s++); } void Uart_SendInt(u16 num) { u8 i; u8 buf[5]; for(i = 0; i < 5; i++) { buf[i] = num % 10 + '0'; num /= 10; } for(i = 5; i > 0; i--) Uart_SendByte(buf[i - 1]); } /************************************************ * ADC0832读取通道0,8位分辨率 * 读两次校验:高8位和低8位相等则说明读取成功 ************************************************/ u8 ADC0832_GetValue(void) { u8 i; u8 dat_high = 0; u8 dat_low = 0; ADC_CS = 1; ADC_CLK = 0; ADC_DIO = 1; ADC_CS = 0; // 片选拉低,启动转换 ADC_CLK = 1; _nop_(); ADC_CLK = 0; _nop_(); // 起始位 ADC_DIO = 1; // SGL/DIF = 1,单端输入 ADC_CLK = 1; _nop_(); ADC_CLK = 0; _nop_(); ADC_DIO = 0; // ODD/EVEN = 0,选择CH0 ADC_CLK = 1; _nop_(); ADC_CLK = 0; _nop_(); ADC_DIO = 1; // 释放DIO,准备接收数据 // 读8位高位数据,MSB在前 for(i = 0; i < 8; i++) { ADC_CLK = 1; _nop_(); ADC_CLK = 0; _nop_(); dat_high = (dat_high << 1) | (u8)ADC_DIO; } // 读8位低位数据,LSB在前,用于校验 for(i = 0; i < 8; i++) { dat_low = dat_low | ((u8)ADC_DIO << i); ADC_CLK = 1; _nop_(); ADC_CLK = 0; _nop_(); } ADC_CS = 1; // 释放片选 if(dat_high == dat_low) return dat_high; return 0; } /************************************************ * 连续采样num次取平均,抑制FSR抖动 ************************************************/ u16 ADC0832_GetAverage(u8 num) { u16 sum = 0; u8 i; for(i = 0; i < num; i++) sum += ADC0832_GetValue(); return sum / num; } void main() { u16 adc_val; u16 voltage_mv; Uart_Init(); while(1) { // 连续采8次取平均,效果比单次采样稳很多 adc_val = ADC0832_GetAverage(8); // 5V参考电压,8位ADC,单位电压 = 5000mV / 255 voltage_mv = (u16)adc_val * 5000UL / 255UL; Uart_SendString("ADC="); Uart_SendInt(adc_val); Uart_SendString(" Voltage="); Uart_SendInt(voltage_mv); Uart_SendString("mV\r\n"); Delay_Ms(50); } }这段代码直接烧进开发板就能用。串口打印的数字中,ADC=后面是0到255的原始值,Voltage=是估算电压。我这里用8次采样取平均,实测能把跳变量控制在5个LSB以内。如果还是觉得抖,可以加到16次或32次,代价是响应变慢,取舍一下。
有一个细节值得说:ADC0832_GetValue里读取低8位是为了校验,如果高8位和低8位不一致,说明时序有问题或者DIO线没释放干净,这时候返回0而不是返回错误码,是为了防止读数乱跳影响上层逻辑。实际调试中,如果一直返回0,优先检查DIO方向切换是否成功。
3.2 STM32 HAL库读取FSR完整代码
STM32这边我用HAL库,基于F103C8T6。核心逻辑是配置ADC1通道0,轮询方式读取,增加多次采样平均函数,再封装一个电压换算函数。这段代码可以直接移植到CubMX生成的工程里,把系统时钟和串口初始化好就能跑。
/* 核心驱动代码,适配 STM32F103C8T6 + HAL库 * 硬件连接:FSR分压采样点 -> PA0 (ADC1_IN0) * 分压电路VCC接3.3V */ #include "main.h" ADC_HandleTypeDef hadc1; static void MX_ADC1_Init(void) { __HAL_RCC_ADC1_CLK_ENABLE(); hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; // 关键1:F103 ADC时钟不能超过14MHz,72MHz主频用6分频 hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV6; HAL_ADC_Init(&hadc1); ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; // 关键2:FSR轻压时源阻抗大,必须用最长采样时间 sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); } static void MX_GPIO_Init(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } /* 单次读取ADC原始值 */ uint16_t Read_FSR_ADC(void) { uint16_t adc_val = 0; if(HAL_ADC_Start(&hadc1) != HAL_OK) return 0; if(HAL_ADC_PollForConversion(&hadc1, 10) != HAL_OK) return 0; adc_val = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); return adc_val; } /* 多次采样取平均 */ uint16_t Read_FSR_Filtered(uint8_t times) { uint32_t sum = 0; uint8_t i; for(i = 0; i < times; i++) sum += Read_FSR_ADC(); return (uint16_t)(sum / times); } /* ADC原始值转电压,12位分辨率,参考电压3.3V */ float ADC_To_Voltage(uint16_t adc) { return (float)adc * 3.3f / 4095.0f; }使用时在主循环里调用Read_FSR_Filtered(16)取平均,再用ADC_To_Voltage转成电压。这里我没有写DMA方式,是因为轮询在大多数FSR应用里完全够用,采样率几十赫兹到几百赫兹没问题。如果你要同时采集多路或者做高频步态分析,再考虑ADC+DMA,逻辑会复杂一些。
有一个容易忽略的点:PA0引脚必须配置成模拟模式,如果保持默认的浮空输入,ADC采样结果会莫名其妙地偏低或者非线性。CubMX里在GPIO设置里直接选Analog即可。
3.3 数据处理三板斧:滤波、标定、阈值
读完ADC只是第一步。FSR毕竟是个电阻型器件,原始数据噪声大、个体差异大,不处理直接拿去判断压力很容易误动作。我通常做三件事:滤波、标定、阈值滞回。
滤波最简单,就是多次采样取平均,或者用滑动窗口。如果采样点数多、噪声又比较随机,可以先做一次排序,去掉最大最小值再平均,也就是中值平均滤波,对付FSR这种偶发尖峰很有效。51上算力有限,用8到16次平均就够了;STM32上资源宽裕,可以上个简单的滑动窗口数组,效果更平滑。
标定的目的是把ADC值换算成有物理意义的重量。FSR不是线性元件,直接乘系数误差大,我建议用多点分段线性插值。操作方法是:在FSR上依次放标准砝码,记录每个重量对应的ADC均值,形成一个映射表,然后查表插值。砝码不需要太多,5个点左右就能覆盖常用量程。
/* 多点线性插值标定代码,适配两种平台 */ typedef struct { uint16_t adc; uint16_t weight_gram; } CalPoint; /* 实测标定表,务必根据自己的FSR和分压电阻重新标定 */ const CalPoint cal_table[] = { {50, 0}, {200, 100}, {500, 500}, {700, 1000}, {850, 2000}, }; #define CAL_NUM (sizeof(cal_table) / sizeof(cal_table[0])) uint16_t Cal_ADC_To_Gram(uint16_t adc) { uint8_t i; if(adc <= cal_table[0].adc) return cal_table[0].weight_gram; for(i = 0; i < CAL_NUM - 1; i++) { if(adc >= cal_table[i].adc && adc < cal_table[i + 1].adc) { float ratio = (float)(adc - cal_table[i].adc) / (float)(cal_table[i + 1].adc - cal_table[i].adc); return cal_table[i].weight_gram + (uint16_t)(ratio * (cal_table[i + 1].weight_gram - cal_table[i].weight_gram)); } } return cal_table[CAL_NUM - 1].weight_gram; }阈值判断这里我特别推荐加“滞回”处理。很多新手只设一个阈值,读数在阈值附近抖动时,按键状态就会反复跳变。解决办法是设两个阈值:一个按下阈值,一个松开阈值,松开阈值明显低于按下阈值。比如按下设80g,松开设40g,这样读数在40到80g之间不会改变状态,天然带防抖效果。
/* 滞回阈值判断,防临界抖动 */ #define PRESS_ON_THRESHOLD 80 // 按下判定,单位:克 #define PRESS_OFF_THRESHOLD 40 // 松开判定,单位:克 uint8_t press_state = 0; // 0=松开,1=按下 void Update_Press_State(uint16_t gram) { if(press_state == 0 && gram > PRESS_ON_THRESHOLD) press_state = 1; else if(press_state == 1 && gram < PRESS_OFF_THRESHOLD) press_state = 0; }Update_Press_State每20ms调用一次,按键状态就非常稳定了。这个思路在机械按键消抖里很常见,但用在FSR这种模拟量判断上效果同样好。
4. 实测踩坑记录与排查速查表
4.1 读数跳变到亲妈都不认识
这是我第一次用FSR时遇到的最头疼的问题。未做任何处理时,ADC读数在5秒内能跳几十个LSB,尤其在轻压区,手指稍微抖动一下,数值就像心电图一样跳。原因有两个:一是FSR本身接触面上的导电粒子在压力变化时有微动,输出本身就带噪声;二是引线过长或者和电源线靠太近,引入工频干扰。
解决办法我按优先级排一下:先加0.1uF硬件滤波电容,再在软件里做8到16次采样平均,最后注意把FSR信号线跟电源线、PWM线分开走。做完这三步,读数基本稳定。如果还不够,可以试试中值滤波,把采样窗口里的最大最小值去掉再平均,对付偶发尖峰非常有效。
4.2 手没碰就有输出
这是一个非常容易被忽略的问题。理论上FSR未受力时电阻大于1MΩ,分压点电压接近0,但实际测量会发现ADC输出不为0,甚至能到几十个LSB。
我遇到过的原因有三个:一是固定电阻R_fix选得太大,比如用了100kΩ,FSR未受力时虽然也有1MΩ,但分压比是100/(100+1000),约9%,5V参考下能到0.45V,这就不小了;二是PCB板面受潮或者助焊剂没洗干净,采样点漏电;三是FSR表面沾了导电污渍,等于并联了一个电阻。排查时先测FSR两端实际阻值,再量采样点电压,能很快定位问题。
4.3 标定好了换块板子又漂了
同一个FSR,同一套代码,从5V供电的STC89C52板子换到3.3V供电的STM32板子,标定表全部作废。这不是代码问题,是ADC参考电压变了。FSR分压输出的是相对电压,ADC看到的是以参考电压为基准的比值,参考电压不同,ADC值自然不同。
解决办法有两个:一是重新标定,这最靠谱;二是在代码里用实际测量的VCC算绝对电压,再通过查表映射压力。第二种方法可以减小跨平台差异,但FSR个体差异依然存在,所以换FSR或者换板子后,我都建议至少做一次两点标定。
4.4 快速踩压和缓慢按压读数差很多
FSR有迟滞特性和一定的动态响应限制,快速冲击和缓慢施压即使最终压力相同,读数也会有差异。早期我做一个踢踏舞节奏检测,快速踩踏时读数明显偏低,慢踩时读数又正常,一度以为是采样率不够。
后来确认是迟滞和接触时间的问题,FSR的导通特性决定了它更适合低频或者静态压力检测。如果项目必须检测动态力,建议把采样率提到100Hz以上,配合短窗口的平均,动态效果会好一些。另外,分压电阻越小,RC时间常数越小,响应越快,这也是R_fix不能盲目取大的原因之一。
4.5 FSR引脚读数为0或者一直满量程
这类问题先别怀疑代码,先用万用表量电压。我把排查流程整理成了一个表,方便直接对照:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 读数一直为0 | FSR断路或分压点被拉到地 | 量FSR两端阻值,检查接线 |
| 读数一直满量程 | 分压点被拉到VCC | 检查FSR和R_fix是否接反 |
| 读数跳变剧烈 | 接触噪声或电磁干扰 | 加0.1uF电容,软件多次采样 |
| 无压力时读数偏高 | R_fix取值过大或板面漏电 | 换小阻值R_fix,清洗PCB |
| 换平台后读数全变 | ADC参考电压不同 | 用实际VCC换算电压,重新标定 |
| STM32读数偏低 | 采样时间设置太短 | 改成239.5周期采样 |
| 读数对压力不敏感 | 分压电阻与量程不匹配 | 按目标压力范围重选R_fix |
另外有个小技巧,调试时如果读数异常,先把FSR拆下来,用一个10kΩ普通电阻替代接到采样点,看ADC能不能读到稳定值。能读到正常值,问题大概率在FSR和焊接;读不到,问题就在分压电路或者ADC配置。这个方法能帮你快速缩小排查范围,比盲改代码高效得多。
最后分享一点我的个人习惯
FSR这东西,本质上是个“活电阻”,别拿它当精密称重传感器用。温度、湿度、按压面积、按压速度都会影响读数,我做项目时最常用的定位是“可靠的定性检测加粗略的定量测量”。量程匹配选对、做一次多点标定、再把阈值滞回做好,稳定性完全够用。
如果后续想扩展,可以试试在同一片FSR上加载和卸载的差异做疲劳检测,或者用多片FSR组成阵列做压力分布判断,还能接蓝牙通过App看实时曲线。玩熟了之后,你会发现这个不到二十块的传感器,能做的项目比想象中多得多。