买回来的薄膜压力传感器,用万用表一量,电压好像也有变化,但接上STM32的ADC之后,读数要么顶到4095,要么趴在线0附近,中间区域根本没法用。这是很多人在STM32上做薄膜压力传感器数据采集时遇到的第一道坎。其实问题不在单片机,而在传感器本身的特性和信号链路处理方式。这篇文章我把整个项目从原理、硬件到软件完整拆一遍,分别给出标准库和HAL库两套实现,适合正在做触觉检测、机器人足底、坐姿监测这类需要“测力”但不追求称重级精度的场景。
1. 先摸清薄膜压力传感器的脾气:原理与选型
1.1 FSR的物理结构和“电阻-压力”关系
薄膜压力传感器(FSR,Force Sensing Resistor)不是称重传感器,它的核心是两层柔性薄膜之间夹着一层导电聚合物材料。没有压力时,导电粒子之间的接触比较松散,这时候传感器两端的阻抗通常在1MΩ以上;一旦表面受力,导电材料被压缩,内部导电粒子之间的接触路径变多,等效电阻就会明显下降。压力越大,电阻越小,这个趋势在轻载段特别明显,但当压力超过一定范围后,电阻的变化会逐渐趋于饱和。
也就是说,FSR本质上是一个压敏电阻,而不是一个线性的应变片。这就带来一个非常关键的工程结论:你不能指望它输出一个像称重传感器那样“10mV对应多少力”的线性信号。FSR通常给出的指标是“电导随压力近似线性”,也就是1/R与力在一定范围内近似成线性关系,R和力本身是非线性的。所以后面做信号调理、做ADC转换、做软件标定时,都要把这一层非线性考虑进去。
1.2 常用型号怎么选,参数怎么读
市面上最常见的几个型号,我列在下面:
| 型号 | 感应区域 | 大致量程 | 厚度 | 适用场景 |
|---|---|---|---|---|
| FSR402 | 直径14.68mm | 0~10kg | 0.45mm | 按键力度、夹持力检测 |
| FSR406 | 44.5mm x 44.5mm | 0~10kg | 0.45mm | 大面积接触坐垫、床垫 |
| 国产RP-C18.3系列 | 直径18.3mm | 0~10kg / 0~100kg可选 | 0.4mm左右 | 机器人足底、按摩设备 |
| 国产RFP602系列 | 60mm x 20mm | 0~10kg | 0.3mm | 抓握力、鞋垫压力分布 |
选型时有个容易忽略的地方:FSR指标里写的量程通常是指“压强”,而不是“总力”。同样一个10kg量程的传感器,如果用一个尖头去压,可能几百克就报警了;如果用一块平整的大面积压块去压,它能承受的总力才会接近标称值。原因在于真正让导电层产生形变的是单位面积上的压力,而不是总力。所以选型前先想清楚你的机械压头是什么样的,接触面积有多大。
另外精度不要抱有期望。FSR的重复性一般在±5%到±10%之间,批次之间差异也很大。如果你的应用是要精确到克级,FSR不是合适的选择,老老实实去用应变片式称重传感器加HX711;如果你的目标是判断“有没有压力”“压力大概在哪个档位”,或者做连续变化的力度控制,FSR就非常好用,安装薄、结构简单、成本低。
2. 信号调理电路:别把FSR直接怼到ADC引脚上
2.1 直接分压到底行不行,问题出在哪
很多教程的做法很简单:FSR和一个固定电阻串联,中间抽头接ADC引脚。这个电路做演示没问题,但放到实际项目里,我强烈建议你不要直接这样接。两个原因。
第一是等效源阻抗太高。FSR未受压或轻压时阻抗在几十千欧到兆欧级别,而STM32的ADC是逐次逼近型(SAR),内部有个采样电容,采样开关闭合后靠外部信号源给这个电容充电。如果外部源阻抗太高,充电很慢,在ADC的采样时间内电容电压根本追不上实际信号,最后的转换结果会明显偏低。标准库和HAL库虽然都可以把采样时间调到最长的239.5个周期,但即便如此,几十千欧以上的源阻抗依然会让结果不可靠。
第二是信号范围不匹配。直接把FSR分压,选固定电阻值很尴尬:选大了,大力段输出接近电源电压,变化极缓;选小了,轻载段输出又压得很低。等你用ADC采样完,发现有效变化区间可能只有几百个码值,白白浪费12位分辨率。
2.2 我推荐的调理方案:分压 + 运放缓冲
实际项目里我一般这么做:FSR接VCC,下面串联固定电阻到GND,分压节点送入一个轨到轨运算放大器,接成电压跟随器,运放输出再送到STM32的ADC引脚。运放的作用是阻抗变换,把高阻且不稳定的分压节点变成低阻输出,ADC采样电容的充放电就不再受传感器阻值变化的影响。
如果希望进一步放大信号,可以把跟随器改成同相放大器,增益设置在2~5倍之间。注意STM32的ADC参考电压通常是3.3V,运放输出绝对不能超过这个值,所以放大倍数要结合分压范围反推。我用的运放一般是LMV321、OPA333这类单电源轨到轨运放,工作电压3.3V,输出摆幅能比较接近电源轨。用LM358这种老款运放在5V供电下还能看,3.3V单电源下输出摆幅不够,不建议。
分压电阻的选择,以FSR402为例,我通常会先在轻载到中载的受力区间标定,然后让这个区间的分压电压落在ADC的50%~90%量程范围内。比如目标区间是0~2kg,实测传感器电阻从100kΩ变化到约2kΩ,固定电阻R取10kΩ时,分压节点电压大致从0.3V变化到2.75V,ADC值区间约为93~853。这个区间虽然不算拉满,但已经足够区分力度变化,而且不会在重压时过早饱和。如果你想在某个特定小力度区间获得高分辨率,分压电阻甚至可以选得更大或更小,这个需要通过实测调整,而不是照抄网上某个固定值。
3. ADC配置要点:采样时间、参考电压与阻抗匹配
3.1 STM32 ADC的基本工作逻辑
STM32F103系列的ADC是12位逐次逼近型,它内部有一个采样保持电路,转换过程分两个阶段:采样阶段,内部开关接通,外部信号给采样电容充电;转换阶段,开关断开,ADC对这存下来的电压逐次逼近编码。所以对外部电路来说,采样阶段相当于瞬间挂了一个电容负载上去,如果外部信号源不能快速把这个电容充满,测出来的值就会偏小。
F103的数据手册里,ADC的外部输入阻抗建议一般不超过50kΩ,并且最好在ADC引脚附近并联一个10nF~100nF的电容,用来稳定外部信号。这里的电容不能加得太大,因为每次采样时,外部源要通过自身阻抗给这个电容和内部采样电容充电,如果外部源阻抗高且外部电容大,前一次采样消耗的电荷还没恢复,下一次采样就来了。
3.2 采样时间、ADC时钟和校准
再说采样时间。STM32的ADC采样时间可以配置成1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个ADC时钟周期。对于FSR这种输出阻抗波动大的信号源,即使加了运放缓冲,我依然建议把采样时间设置为55.5周期以上。如果没加运放直接分压接ADC,那就老老实实用239.5周期,并且配合外部电容。
ADC时钟同样重要。F103的ADC最高工作在14MHz,系统时钟72MHz时,分频器取6,得到12MHz的ADC时钟,这个组合比较稳妥。注意ADC时钟太高,采样时间即使拉满,实际充电时间也会被压缩。
上电后先做ADC校准,这也是很多新手容易跳过的。校准用于消除ADC内部的电容失配和零点误差,官方手册里明确要求每次上电后要执行一次。标准库有ADC_ResetCalibration和ADC_StartCalibration,HAL库有HAL_ADCEx_Calibration_Start,两种库都要做。不校准的话,零点和增益误差可能让结果偏差几十到上百个码值。
3.3 参考电压的工程处理
F103的VREF+引脚在LQFP系列封装里和VDDA是同一个引脚,你没法外接独立的参考源。实际做项目时,为了让ADC参考电压尽量干净,我习惯在VDDA引脚上单独加一个LC滤波,比如串一个10Ω电阻,再接一个1μF陶瓷电容到地,数字电源VDD和模拟电源VDDA不要直接短接。如果你的MCU是STM32F4系列,并且封装引出了独立VREF+引脚,可以考虑用REF3330这类基准源给ADC专门供电,效果会更好。
4. 标准库实现:寄存器级配置的完整过程
4.1 为什么仍然值得看标准库
现在新项目多数已经转向HAL库,但标准库的代码在执行逻辑上更直观,每一个外设的初始化、每一次转换的启动和标志位判断都是显式的。如果你刚接触STM32,我建议至少把标准库版本跑一遍,你会真正理解“配置外设时钟→初始化GPIO→初始化ADC→校准→启动转换→等待标志→读数据”这个链条,而不是在CubeMX里点完一个ADC通道就完事。
4.2 标准库下完整的ADC单通道读取函数
下面这段是我在F103C8T6上验证过的代码,PA1作为模拟输入,读取单通道ADC值,做主函数里轮询使用。
void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟 = 72MHz / 6 = 12MHz GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_Cmd(ADC1, ENABLE); ADC_SoftwareStartConvCmd(ADC1, ENABLE); } uint16_t ADC1_Read(void) { while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); } uint16_t ADC1_ReadAverage(uint8_t times) { uint32_t sum = 0; for(uint8_t i = 0; i < times; i++) { sum += ADC1_Read(); } return (uint16_t)(sum / times); }这里我把ADC配置成了连续转换模式,启动一次后,硬件会自动开始下一次转换,每次读EOC标志并取值即可。因为FSR是缓变信号,不需要定时器触发,软件直接启动最简单。取平均的次数不建议太多,FSR本身有机械响应特性,取32次平均已经足够平滑,再多会造成明显延迟。
4.3 标准库版本要注意的坑
标准库的版本和Device头文件一定要匹配,F103系列请使用STM32F10x_StdPeriph_Lib_V3.5.0或对应的Github维护版本。新手最常见的报错是ADC_SampleTime_239Cycles5未定义,通常是库版本太老,或者用了F0/F4的标准库但没打开对应宏定义。另外,标准库没有自动生成初始化代码,外设时钟的开启顺序千万别写反,要先开GPIO和ADC的APB2时钟,再配GPIO,最后配ADC,否则寄存器写入无效。
5. HAL库实现:CubeMX配置与两种数据读取方式
5.1 CubeMX配置里容易被忽略的选项
用HAL库时,大部分工作被CubeMX承担了,但有几个选项必须仔细设置。
打开ADC1,选择IN1通道,对应引脚PA1。在Parameter Settings里,将Resolution设为12位,采样时间拉到最大(239.5 Cycles),开启连续转换模式。时钟配置里注意APB2外设时钟如果是72MHz,ADC Prescaler选择6分频,得到12MHz。如果你同时还需要用串口打印调试数据,记得USART1也打开。
生成代码之后,你会得到一个hadc1句柄。初始化函数MX_ADC1_Init()已经在main函数里被调用,但注意HAL库不会自动执行校准,需要手动加一行。
5.2 轮询模式读取
轮询模式适合采样频率不高的场景,代码最简单:
uint16_t adc_read_poll(void) { HAL_ADCEx_Calibration_Start(&hadc1); HAL_ADC_Start(&hadc1); if(HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { return HAL_ADC_GetValue(&hadc1); } return 0xFFFF; }有个小细节:HAL库的校准函数执行完一次后,只要你不改ADC分频和分辨率,不需要反复执行。轮询模式下,即使CubeMX里开了连续转换,我依然建议每次读取前调用HAL_ADC_Start,读取后调用HAL_ADC_Stop。因为在轮询模式下如果保持连续转换,下一次HAL_ADC_PollForConversion可能读到的是陈旧数据,尤其是在主循环里做了其他耗时操作时。每次启动、读取、停止的代价很小,数据时序更可控。
5.3 DMA循环模式读取
如果你需要同时采集多路FSR,或者想把采样任务交给后台,DMA是正路。CubeMX里在ADC1页面添加DMA请求,Mode选择Circular,Data Width保持Word。生成代码后,主程序中这样初始化:
#define ADC_BUF_SIZE 32 uint32_t adc_buf[ADC_BUF_SIZE]; void adc_dma_start(void) { HAL_ADCEx_Calibration_Start(&hadc1); HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE); } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC1) { // adc_buf 里的 32 次采样已经就绪,可在这里做平均 } }DMA回调里要注意,HAL_ADC_ConvCpltCallback默认是在DMA传输完毕后由中断触发的,如果开了循环模式,这个回调会周期性进入。平均的计算要快,不要在回调里做串口打印或长延时,否则会影响下一次DMA传输。数据缓冲区用uint32_t类型,虽然ADC结果是12位,但HAL库的DMA搬运以字为单位,直接定义uint32_t adc_buf[]最稳妥。
HAL库的一个典型坑是:如果DMA配置成了Normal模式,一次搬运完成后ADC会停在那里,必须重新调用HAL_ADC_Start_DMA才能继续。工程上做连续监测时,最好用Circular模式。另外F103的DMA1和ADC1的请求映射关系,CubeMX自动处理,手动移植时很容易搞错,这块建议让CubeMX生成,不要手写。
6. 标定与拟合:把ADC值换算成真实压力值
6.1 为什么每个传感器都要单独标定
FSR的批次一致性差,同一型号、同一生产批次的两只传感器,在同样压力下ADC值可能差20%以上。所以想读出一个相对可信的压力数值,必须有标定环节。我的做法是做一个简易施压装置:传感器平放在硬质桌面上,上方覆盖一块略大于感应区的硬质压板,然后放标准砝码。压板的作用是保证压力均匀分布,否则你用手按压,同一个砝码在不同力度、不同接触角度下读数波动很大。
6.2 最少两点标定和更好的分段线性标定
最粗糙的标定是两点法:记录空载ADC值和某已知重量的ADC值,然后线性映射。这对FSR来说误差很大,因为它的响应曲线是弯曲的。更实用的做法是做一个多点标定表,比如取0g、100g、200g、500g、1kg、2kg这六个点,记录每个点的稳定ADC平均值,然后在软件里做分段线性插值。
const uint16_t adc_table[] = {10, 150, 300, 650, 1200, 2400}; const uint16_t gram_table[] = {0, 100, 200, 500, 1000, 2000}; uint16_t adc_to_gram(uint16_t adc_val) { if(adc_val <= adc_table[0]) return gram_table[0]; if(adc_val >= adc_table[5]) return gram_table[5]; for(int i = 0; i < 5; i++) { if(adc_val >= adc_table[i] && adc_val < adc_table[i+1]) { uint32_t gram = gram_table[i]; gram += (uint32_t)(gram_table[i+1] - gram_table[i]) * (adc_val - adc_table[i]) / (adc_table[i+1] - adc_table[i]); return (uint16_t)gram; } } return 0xFFFF; }分段线性表的好处是明确、可控,不依赖数学库,运算量极小。标定曲线存在哪一段误差大,只需要在那一带增加标定点密度就行。我实际标定中感受最深的是轻载段,0~200g范围内ADC变化非常剧烈,需要多取几个点;1kg以上,ADC变化开始放缓,此时标定点可以稀疏一些。
6.3 要不要做曲线拟合
如果你嫌分段表占内存,也可以做最小二乘拟合。但以FSR的固有非线性,一次多项式拟合效果很差;二次多项式在轻载段会有明显偏差,三次以上多项式才有改善。STM32上做三次多项式拟合需要用到浮点运算,F103没有硬件FPU,每秒做几百次没问题,但如果你同时跑显示和通信,会加重主循环负担。对比下来,多数项目我依然选择分段线性表,简单、直观、好维护。只有当你需要连续输出“接近真实压力”的数值,并且你有条件认真采集几十个标定点时,再做多项式拟合。
7. 实测环节的坑与优化建议
7.1 蠕变会让你的数据“自己飘”
FSR有一个很讨厌的特性叫蠕变:施加一个恒定的力,电阻值并不会保持不动,而是会随时间缓慢下降,导致ADC读数缓慢上升。这个漂移在最初几秒最明显,之后会慢慢趋于稳定。所以标定的时候不要一放上砝码就立刻读数,等3到5秒,读数稳定了再记录。实测项目中,如果你是需要周期性检测压力,每次上电后做一个空载归零,能抵消一部分长期漂移。
7.2 机械结构比软件滤波更重要
我帮朋友调过一个坐姿监测项目,软件滤波做了各种平均数和中值滤波,但数据还是抖动。最后发现问题是传感器背面垫了一层软海绵,坐上去之后海绵的形变让传感器的受力不断变化。换成硬质支撑板之后,数据立刻稳定了。薄膜压力传感器只有在受压面平整、背面有刚性支撑时,重复性才会好。这一点一定在设计结构时提前考虑,靠软件是救不回来的。
7.3 引脚和连接线的处理
FSR的引线一般是FPC软排线或者焊盘,焊接时温度不能过高,也不要让焊锡渗到感应区域。频繁弯折靠近引出端的部分会导致线路断裂,实际项目中最好用FPC连接器或者用热压方式固定,再灌胶保护。多个传感器阵列时,线缆捆扎不要太紧,否则会引入机械拉力,导致零点偏移。
7.4 多通道采集时注意通道间串扰
如果你要做四路或者更多FSR同步采集,ADC用扫描模式加DMA时,相邻通道之间会有串扰。尤其通道源阻抗差异较大时,一个通道的高压信号可能影响下一个通道的低压信号。工程上我一般在这类项目里采用分时采集+足够采样时间+通道切换后多采几次丢弃前几拍的方法,宁可损失一点速度,也要保证每一路数据的真实性。
薄膜压力传感器看似简单,真正用起来之后才会发现,决定数据质量的往往不是单片机的ADC位数,而是你对传感器特性的理解、信号调理电路的设计、机械结构和标定流程是否到位。先跑通标准库版本,再用HAL库重新实现一遍,这条路走下来,你对STM32的ADC部分和两种固件库的开发方式的体会会比直接抄CubeMX深得多。