☰
STM32+薄膜压力传感器:从分压电路到数据标定的完整教程
2026/10/4 1:24:21 网站建设 项目流程

做嵌入式这几年,和“力的感知”打交道最多的场景无非是:坐姿监测、足底压力分布、机器人触觉反馈、智能床垫、握力训练设备,还有踩一下就能发出警告的防夹手系统。这类项目里,性价比最高、上手最快的传感器,就是薄膜压力传感器。我最近刚好用STM32完整做了一遍数据采集,而且同一套硬件分别用标准库和HAL库各实现了一版。这篇文章就把整个过程拆开讲清楚,从硬件选型、分压电路设计,到ADC采样、软件滤波、标定和上位机展示,全部记录下来,方便打算做类似项目的朋友直接“抄作业”。

1. 项目概述与方案选型

1.1 薄膜压力传感器到底怎么测力

薄膜压力传感器(FSR,Force Sensing Resistor)本质上是一个压敏电阻,核心结构是一层柔性基板、一层可印刷的压敏材料,再加上一对电极。你不按压它的时候,压敏材料内部导电颗粒之间的接触点很少,电阻特别大,动不动就是几兆欧;一旦表面受力,导电颗粒被压到一起,导电通路变多,电阻迅速下降,压力足够大时甚至可以降到几百欧姆甚至更低。

这种“压力变大,电阻变小”的特性,和我们常用的光敏电阻、热敏电阻很像,唯一的区别是它的变化量是受力的影响而不是光照或温度。所以测量薄膜压力传感器的关键不是直接测电阻,而是通过一个分压电路把电阻变化转换成电压变化,再交给STM32的ADC采集。

常见型号有RP-C7.6、RP-C18.3、FSR402等,区别主要在于感应区域的直径和量程。我这次用的是RP-C7.6,感应区直径7.6mm,静态电阻能做到1MΩ以上,按压力度大时能到1kΩ以下,拿来验证采集流程完全够用。

1.2 为什么同时用标准库和HAL库

很多新手刚接触STM32,经常纠结到底学标准库还是HAL库,其实这个问题没有标准答案。

标准库是意法半导体早期主推的固件库,特点是封装层薄、贴近寄存器,很多老工程师都是从它入门的。它的问题也很明显:ST从F1到F4再到H7,标准库的代码并不能一套搬过去,迁移工作量比较大。

HAL库则是配合STM32CubeMX工具使用的,图形化配置外设,生成工程后填业务代码,同一套HAL库接口可以跨系列复用。它的缺点是封装层级多,运行效率略低于寄存器,调试时一旦要深入底层,会感觉被框架包裹了一层。

我这次之所以两套都写,主要是因为项目后期要移植到不同型号的开发板,标准库版本先快速验证电路可行,HAL库版本通过CubeMX配置后直接生成工程,最后在同一块板子上对比两个版本的ADC读取结果,确保没移植错。下面这组对比可以帮你快速判断自己该用哪套:

对比维度标准库HAL库
入门曲线需要自己配置RCC、GPIO等,代码量大,适合理解底层CubeMX图形化配置,生成工程快,适合快速开发
运行效率相对高,函数调用层级少封装较厚,性能略低但通常够用
跨芯片移植不同系列寄存器差异大,迁移麻烦API统一,换芯片只需重新生成工程
调试方式配合寄存器手册,逻辑更透明配合HAL源码,看状态机,调试稍绕
社区资料量老项目、老教程多近年新项目、CubeMX教程为主

如果你目标是快速实现功能,或者要频繁换芯片型号,HAL库更省心;如果你想彻底搞懂ADC采样到底是怎么一回事,建议标准库跑一遍,再回头看HAL库的封装就一目了然。

2. 硬件准备与电路设计要点

2.1 硬件清单与选型建议

这套采集系统用到的东西非常少,属于典型的最小可用方案。

  • STM32开发板一块,我用的是STM32F103C8T6“最小系统板”,性价比高,资源足够,ADC、串口、USB都齐全。如果手头有F407或者其他型号,接线和代码逻辑完全一样,只需要注意自己板子的ADC通道对应引脚。
  • 薄膜压力传感器一片,注意区分“不带电路模块”和“带模块”两种。带模块的一般已经集成了分压电阻和运放,输出直接是模拟电压;不带模块的就是两脚器件,需要自己搭分压。
  • 固定电阻若干,阻值建议先准备10kΩ和100kΩ各几个,后面分压电路计算时会用到。
  • 杜邦线若干,能短则短。实验阶段用杜邦线没问题,但做足底压力这种动态检测时,传感器引线越长越容易引入噪声,最好使用屏蔽线。
  • 显示或反馈设备,OLED和USB转串口模块二选一,我建议串口模块加上,因为调试数据时串口最方便。

选传感器的时候还有一个容易忽略的点:有些薄膜压力传感器是无压力时开路,有些传感器模块内部已经接了固定电阻,输出逻辑反过来了。买之前一定看清规格书是“电阻型”还是“电压模块型”,不然分压电路会算反。

2.2 分压电路设计与参数计算

传感器本身是可变电阻,无法直接给ADC一个稳定电压,所以要构造分压网络。推荐接法是这样的:

  • VCC(3.3V)接传感器的一端
  • 传感器的另一端接固定电阻的一端
  • 固定电阻的另一端接GND
  • 传感器的另一端和固定电阻之间的节点引出到STM32的ADC引脚

也就是传感器放在高端,固定电阻放在低端,ADC从中间抽头取电压。这个接法下,传感器电阻越小,分到的电压就越少,ADC读数越低;无压力时传感器电阻极大,ADC读数接近VCC满量程。

那这个固定电阻到底选多大?原则是根据量程中心电阻来匹配。举个例子,假设无压力时传感器电阻是1MΩ,满量程时是1kΩ,那么固定电阻取10kΩ时,无压力的输出电压约为:

Vout = 3.3 × 10kΩ / (1MΩ + 10kΩ) ≈ 0.032V

满量程压力时输出电压约为:

Vout = 3.3 × 10kΩ / (1kΩ + 10kΩ) ≈ 3.0V

这样中间节点就能输出大约0.03V到3.0V的变化范围,覆盖了STM32 ADC的绝大多数量程。如果固定电阻选100kΩ,那无压力时输出电压约0.3V,满量程时约3.3V,但传感器的非线性会在这个阻值下被放得更明显,曲线形态不一样。实际上选多大,还要结合你的量程,一个简单技巧是把固定电阻选在传感器“你关注的压力范围对应的电阻”附近,这样分辨率最理想。

如果你只是想快速验证传感器有没有反应,那10kΩ到20kΩ之间随便选一个就能看到明显变化。真正做产品时,还需要在后面加一级运放做信号调理,或者考虑传感器的驱动能力问题,但这篇文章以采集流程为主,分压接法足够覆盖大部分场景。

关于ADC引脚的选择,也提醒一句:STM32F103的ADC1通道0到通道7都在PA0到PA7上,PA0是最常用的,我这次就用的PA0。接线时不要把传感器输出接到PB0这类引脚上,除非你配置的是ADC的另外一个通道。

3. 标准库实现:从寄存器到数据的完整流程

3.1 初始化GPIO与ADC

标准库写ADC采集,最核心的是四步:开启GPIO和ADC时钟、配置GPIO为模拟输入、配置ADC工作模式、使能ADC并进行校准。

时钟方面,STM32F103的ADC挂载在APB2总线上,最高时钟是14MHz。我直接用默认的系统时钟72MHz,分频给ADC用,所以配置RCC时要把APB2上ADC的时钟使能。

void ADC1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); }

配置成模拟输入的目的是让引脚内部的施密特触发器断开,避免数字引脚部分产生噪声,同时也让ADC采样的是纯粹的模拟电压。

然后配置ADC本体。我用的是软件触发、单通道、连续转换模式。连续转换可以省去每次启动转换的指令,读取的时候直接拿最新结果。

void ADC1_Config(void) { ADC_InitTypeDef ADC_InitStructure; RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC 时钟 = 72MHz / 6 = 12MHz 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_0, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }

这里的ADC_RegularChannelConfig中最后一个参数是采样周期,我选的55.5个周期。ADC时钟12MHz时,每个周期约83.3ns,55.5个周期就是大约4.6µs的采样时间。如果选择1.5个周期,转换速度更快,但源阻抗比较高、信号源比较“软”的时候,采样时间太短会导致采样电容没充满,采集值反而不稳定。对薄膜压力传感器这种源阻抗随压力剧烈变化的器件,我建议要么采样周期选长一点,要么在ADC引脚对地加一个0.1µF的电容作为电荷缓存。

校准这段代码在标准库里特别重要。它做的是让ADC内部电容阵列在转换前校正一次,消除芯片制造时引入的偏移误差。如果你忘了校准,可能测出来的值会整体偏移几十到上百个LSD,导致后面的电压换算出问题。

3.2 读取、电压换算与数据滤波

初始化完成之后,主循环里读取就很简单了,直接拿寄存器值:

uint16_t adc_val = ADC_GetConversionValue(ADC1); float voltage = (float)adc_val / 4096.0f * 3.3f;

STM32F103的ADC是12位分辨率,所以满量程值是4096,对应3.3V参考电压。这里有个隐藏问题:很多开发板上的3.3V并不是精准的3.3V,而是LDO输出带一点误差,比如3.28V或3.32V。如果你要精确测量,应该用万用表实测一下VREF引脚电压,然后把刚才公式里的3.3换成实测值。

标准库版本我通常会把读取和滤波封装成一个函数,这样主程序里不用关心怎么算:

#define FILTER_N 10 static uint16_t filter_buf[FILTER_N]; static uint8_t filter_index = 0; static uint32_t filter_sum = 0; uint16_t ADC_Filter(uint16_t new_val) { filter_sum -= filter_buf[filter_index]; filter_sum += new_val; filter_buf[filter_index] = new_val; filter_index = (filter_index + 1) % FILTER_N; return (uint16_t)(filter_sum / FILTER_N); } uint16_t Read_Pressure_Value(void) { return ADC_Filter(ADC_GetConversionValue(ADC1)); }

这个滑动平均滤波几乎不占额外计算量,10次平均能明显改善手持按压时读数抖动的现象。不过要记住,滑动平均会带来一部分滞后,如果你做的是高频冲击力检测,比如锤击计数,那平均窗口就得缩短,或者在拿数据时用“N个值去除最大最小再平均”的中值平均。

4. HAL库实现:CubeMX配置与代码开发

4.1 CubeMX工程配置要点

HAL库版本我用STM32CubeMX生成工程,配置过程比手写标准库快很多,但有几个选项容易选错,这里逐个说一下。

第一步选芯片型号,我这次还是用STM32F103C8T6。在Pinout视图中,先把SYS的Debug改成Serial Wire,防止烧录后引脚被复用导致第二下载失败。再打开RCC的HSE,选择Crystal/Ceramic Resonator,这样用外部晶振才能把主频跑到72MHz。

然后是ADC配置。展开Analog,在ADC1里勾选IN0,对应PA0引脚。配置参数时我选择Independent Mode,扫描模式关闭,连续转换打开,采样周期同样选55.5周期,这个和标准库版本保持一致,方便对比结果。

串口也要开,因为数据最终要靠串口发到电脑上看。在Connectivity里选USART1,Mode选Asynchronous,波特率设115200,其他默认。串口在后续调试中作用很大,建议始终保留。

时钟树这里特别关键。CubeMX默认HCLK是8MHz或16MHz,需要手动输入72,然后让软件自动计算分频系数。ADC的时钟在RCC设置页面里还要单独看,我选的ADC预分频是6分频,让ADC工作时钟稳定在12MHz,低于14MHz上限,这样采样的线性度和噪声都处于比较理想的状态。

工程设置里,Toolchain选MDK-ARM,Code Generator勾选“Generate peripheral initialization as a pair of .c/.h files per peripheral”,这样每个外设的初始化代码会单独生成一个文件,后续改动不容易把文件搞乱。

4.2 HAL库读取ADC的三种方式

CubeMX生成工程后,ADC初始化已经由HAL_ADC_MspInit函数配置好了,业务代码里只需要一句话启动转换:

HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint16_t adc_val = HAL_ADC_GetValue(&hadc1);

HAL_ADC_PollForConversion的第二个参数是超时时间,单位毫秒。如果转换没有在规定时间内完成,函数会返回HAL_TIMEOUT。正常配置下一般几微秒就完成转换,这里给10ms纯粹是保险,不会影响运行效率。

HAL库读取ADC还有两个变体。一个是中断方式,每次转换完成触发中断,适合低功耗场景。另一个是DMA方式,适合连续采集大量数据,比如要采集足底压力随时间变化曲线,可以用DMA自动把几十个周期的数据搬进内存,主程序只负责处理数据:

uint16_t adc_buffer[100]; HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, 100);

DMA方式的优势是CPU完全不用管ADC转换过程,但我实际测试中发现,DMA配置要在启动前把DMA对应的初始化代码检查一遍,尤其是循环模式(Circular Mode)是否打开,设置不对的话DMA传输一次就停了,后续数据不再更新。这个问题在标准库版本里不会遇到,因为标准库的DMA和HAL库封装方式差别比较大,换HAL库后要特别注意。

HAL库版本我同样封装了一个读取函数:

uint16_t Read_Pressure_Value_HAL(void) { uint16_t adc_val = 0; if (HAL_ADC_Start(&hadc1) == HAL_OK) { if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { adc_val = HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); } return adc_val; }

每次读取都调用HAL_ADC_Start其实是比较慢的做法,因为启动和停止都会操作寄存器。在连续转换模式下,其实可以只在初始化时启动一次,之后直接HAL_ADC_GetValue拿数据,效率更高。但要注意,HAL_ADC_Start会重新校准内部状态,如果重复调用,某些情况下会有额外开销。我的建议是分情况:低频率读取就每次Start-Stop,简单可靠;高频率连续采样就初始化启动一次,不停止。

5. 数据标定、滤波与上位机展示

5.1 非线性标定与分段线性插值

薄膜压力传感器最让人头大的问题就是它的电阻-压力曲线不是线性的,在小压力区间特别陡,大压力区间又趋于平缓。如果把ADC值直接按比例当成压力,误差会非常大,所以必须做标定。

标定的思路是用已知重力来建立“ADC值-压力”的对照表。找几个标准重物,比如砝码,放在传感器感应面上,每换一个重量就记录一下稳定后的ADC值。我做了5个点的标定:

砝码重量ADC读数
0g4020
100g3100
300g2250
500g1680
1000g980

把ADC值和重量做成表后,程序里用分段线性插值处理,两个标定点之间的值直接按直线近似。这样虽然没有高次拟合曲线精确,但足够应付大多数交互场景,而且查表代码简单,运行也飞快。

typedef struct { uint16_t adc; uint16_t weight; } CalPoint; const CalPoint cal_table[] = { {4020, 0}, {3100, 100}, {2250, 300}, {1680, 500}, {980, 1000} }; uint16_t Cal_GetWeight(uint16_t adc_val) { uint8_t i; if (adc_val >= cal_table[0].adc) return cal_table[0].weight; for (i = 0; i < 4; i++) { if (adc_val >= cal_table[i + 1].adc && adc_val <= cal_table[i].adc) { uint32_t diff_adc = cal_table[i].adc - cal_table[i + 1].adc; uint32_t diff_w = cal_table[i + 1].weight - cal_table[i].weight; if (diff_adc == 0) return cal_table[i + 1].weight; return cal_table[i].weight + (uint32_t)((uint32_t)(cal_table[i].adc - adc_val) * diff_w / diff_adc); } } return cal_table[4].weight; }

这里有个很重要的细节:标定表里的ADC值是从大到小排列的,因为压力越大,分压点电压越小。如果你接法相反,传感器在低端、固定电阻在高端,那压力越大ADC值越大,查表逻辑要对调。写代码前先确认自己电路里的单调方向。

一个实际经验是:传感器感应区上垫一层薄薄的硅胶垫再放砝码,测出来的数据更稳定。因为传感器本身是柔性材料,如果砝码底面不够平整,受力会集中在局部,造成同一次按压但ADC值跳来跳去。垫了硅胶以后受力均匀,标定表才有参考价值。

5.2 软件滤波实战:滑动平均与中值滤波

ADC采集值在小信号场景下特别容易受电源波动、引线感应和触摸抖动影响。我在两个库的版本里都加了滤波环节,标准库版本用滑动平均,HAL库版本除了滑动平均还加了一层中值滤波,双保险。

中值滤波的思路是连续采5个值,排序后取中间值,可以有效干掉突发干扰脉冲。缺点是会额外消耗RAM和排序时间,所以不要对每个点都做,放大循环里每80ms执行一次就够了。

#define MEDIAN_N 5 uint16_t ADC_MedianFilter(void) { uint16_t buf[MEDIAN_N]; for (uint8_t i = 0; i < MEDIAN_N; i++) { buf[i] = Read_Pressure_Value_HAL(); } for (uint8_t i = 0; i < MEDIAN_N - 1; i++) { for (uint8_t j = i + 1; j < MEDIAN_N; j++) { if (buf[j] < buf[i]) { uint16_t tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; } } } return buf[MEDIAN_N / 2]; }

如果你想把两种滤波组合起来,我建议顺序是先中值,再滑动平均。中值去掉脉冲干扰,滑动平均再把剩余的高频噪声磨平。两个滤波叠加后的滞后时间会稍微增大,但人体按压、坐姿这类慢变化信号完全不受影响,实测稳定度能从大约±30 LSB降到±5 LSB以内。

5.3 串口输出与上位机可视化

数据采集不只是把值读出来,还要能观察分析。最简单的做法是通过串口把数据打出来。我用的帧格式是文本行,一行一个数据,方便串口助手直接查看:

A:4020 V:3.30 F:0g A:3100 V:2.55 F:100g

其中A表示ADC原始值,V表示换算电压,F表示标定后的压力负荷。这种格式看起来直观,写日志也方便,但不适合机器解析。如果你的上位机需要批量处理数据,可以采用定长二进制帧,比如帧头0xAA、0x55,后面跟2字节ADC值、2字节压力值、1字节校验和。上位机按协议解包更高效。

如果不想自己写上位机,有几个现成工具可以快速上手:

  • 串口调试助手,直接看文本输出,适合功能验证。
  • 匿名上位机或VOFA+,支持波形显示,配置简单,串口发数据就能实时画曲线。
  • Python的pyserial库,适合做数据分析和离线索引,几行代码就能把压力数据记录下来并画图。

我实验时用的方案是VOFA+的“JustFloat”协议,把ADC值和计算结果以float二进制发过去,电脑端立刻能看到压力变化的实时曲线,调试传感器响应速度非常直观。

6. 常见问题与排查技巧实录

做这个项目时我踩了不少坑,把典型的症状和排查思路整理成下面的速查表,方便你对照解决:

症状可能原因排查方法与解决
未按压时ADC读数接近4095或接近0分压接法接反,传感器与固定电阻位置交换检查电路方向,确认传感器是接在VCC侧还是GND侧
ADC读数跳变剧烈,波动超过±50 LSB采样周期太短、引线过长、电源不稳采样周期改成55.5周期以上,缩短杜邦线,电源加0.1µF去耦电容
按压时读数变化很小固定电阻选得和传感器量程不匹配根据量程中心电阻重新选择分压电阻,必要时加运放
连续读取后数值缓慢漂移ADC参考电压受温度影响或电源发热使用高精度LDO,检查3.3V引脚电容
HAL库能编译但ADC不工作CubeMX里未使能ADC时钟或引脚被复用重新检查CubeMX的ADC1配置和GPIO复用设置,确认PA0没有配置成其他外设
标准库工程加HAL库代码后编译报错两个库的固件头文件冲突,核心寄存器定义重复新建独立工程,不要混用标准库和HAL库中间层文件
放砝码时读数缓慢变化传感器表面受力不均或没有硅胶垫在传感器感应面垫一层薄硅胶,让受力均匀化
DMA模式只采一次就不再更新未开启DMA循环模式或未设置循环地址在CubeMX里将DMA Mode改为Circular,确保内存缓冲区和外设地址配置正确

除了表格里的问题,还有两个经验值得单独说。

第一个是电源。STM32开发板一般通过USB供电,但USB口的5V经过板载LDO降到3.3V后,纹波并不小。如果直接用这个3.3V给传感器分压电路供电,ADC采集到的噪声会比预期大。我在VCC和GND之间并联了一个10µF电解电容和一个0.1µF陶瓷电容,噪声改善非常明显。如果项目对精度要求高,建议用单独的线性稳压芯片给传感器供电,比如AMS1117-3.3或更安静的HT7333。

第二个是传感器本身的重复性问题。同一片薄膜压力传感器,在完全相同的压力下,你按压100次和第一次按压时测出的电阻就有可能略有不同。这是因为压敏材料的导电颗粒每次被挤压后,恢复位置不可能是完美的。所以标定表只能做参考,做产品时尽量用“区间判别”而不是绝对阈值。比如压力分“轻、中、重”三个等级,比精确到1g靠谱得多。

7. 实操心得与后续扩展思路

这次项目做完,我最深的感觉是:标准库和HAL库并没有谁替代谁,本质上它们都一样是访问STM32寄存器的工具。真正决定项目成败的,是电路设计、采样算法和标定流程。标准库版本让我能清楚看到ADC校准、采样周期这些底层细节到底影响了什么;HAL库版本让我在换芯片、加功能时节省了大量改初始化代码的时间。

如果你现在也要做薄膜压力传感器数据采集,我的建议是先从标准库版本入门,把ADC初始化、读取、电压换算跑通,然后把同样的逻辑迁移到HAL库工程里做对比。两个版本都跑一遍,你对STM32 ADC的理解会比只看一种库深刻很多。

最后再分享一个小技巧:不管用哪种库,都建议把“读取传感器数值”封装成一个独立函数,上层业务只关心这个函数返回的“过滤后数值”,不要在主循环里到处写ADC读取代码。我这次标准库版本封装成Read_Pressure_Value(),HAL库版本封装成Read_Pressure_Value_HAL(),上层逻辑完全一致,换库只动这一处,既方便对比也方便以后移植。这个思路同样适用于下一步要加的OLED显示、蓝牙发送、USB转串口上报等功能,主程序和硬件层彻底解耦,后面扩展起来会非常轻松。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询