eefocus_4087784 发表于 2026-9-1 15:15:37

零知派——基于STM32F103RBT6实现GSR皮肤电导情绪波动实时监测仪

​      情绪这东西看不见摸不着,但身体会"诚实"地留下痕迹——紧张、焦虑、突然被吓一跳的瞬间,手心会不自觉地冒汗。这背后是交感神经系统在起作用:汗腺活动增强,皮肤电导率跟着升高。GSR(Galvanic Skin Response,皮肤电反应)传感器测的就是这个变化

目录
一、系统接线部分
1.1 硬件清单1.2 接线方案表1.3 具体接线图1.4 接线实物图二、安装与使用部分
三、代码讲解部分
3.1 非阻塞状态机3.2 双时间尺度滤波3.3 按键中断及消抖3.4 局部刷新缓存3.5 非阻塞任务调度四、项目结果演示
4.1 操作流程4.2 视频演示五、GSR测量原理深度解析
5.1 传感器工作原理5.2 ADC量化与阈值设计六、常见问题解答(FAQ)
Q1:触发得特别频繁/特别迟钝?Q2:为什么校准的时候要求"保持手指静止不动"?
项目概述      
      基于皮肤活动原理,以零知派标准板(主控STM32F103RBT6) 为核心,搭建了一套集实时采集、数字滤波、情绪波动检测、彩色显示、串口数据记录于一体的GSR情绪波动监测仪,电极开路(未佩戴)自动识别提示,接触恢复后无缝续接。支持按键手动重新校准基线,适应环境/接触状态变化。

项目难点
      问题描述:在裸机环境下同时完成ADC采样、显示刷新、串口发送、LED闪烁和按键响应,如果用delay()阻塞方式,任何一个任务都会卡住其他所有任务

解决方案: 采用状态机+时间戳的非阻塞架构。每个任务独立记录上次执行的时间戳,每次loop()循环中检查millis() - lastTime >= interval,满足条件才执行

一、系统接线部分1.1 硬件清单
序号硬件名称型号/规格数量
1主控板零知派标准板(STM32F103RBT6)1
2TFT彩屏ST7789 240×240 SPI接口1
3GSR传感器皮肤电导传感器模块(模拟输出)1
4LED5mm LED1
5按键6×6mm轻触开关2
6杜邦线公对公/公对母若干若干

1.2 接线方案表      以下所有引脚定义均来自项目Config.h,请严格对照代码定义接线,不得随意更改

模块Config.h 宏定义对应零知派引脚接线说明
GSR传感器GSR_PINA0传感器 SIG → A0(模拟输入)
GSR传感器—GND / VCCGND→GND,VCC按模块要求接3.3V或5V
ST7789屏幕TFT_CSD10ST7789直插零知派标准板,无需单独接线
ST7789屏幕TFT_DCD2
ST7789屏幕TFT_RSTD4
ST7789屏幕硬件SPI固定引脚D13 / D11
ST7789屏幕—GND / VCC
提示LEDALERT_LED_PINA5外接限流电阻后接LED正极,负极接GND
翻页按键BTN_NEXT_PIND0另一端接GND(内部上拉,代码里INPUT_PULLUP)
功能按键BTN_ACT_PIND1另一端接GND(内部上拉)

[*]两个按键均使用INPUT_PULLUP模式,无需外接上拉电阻,按下时引脚被拉低

1.3 具体接线图      GSR传感器的电极请紧贴食指和中指(同一只手),指套收缩力度适中
https://file1.elecfans.com/web3/M00/75/4A/wKgZPGqWblyAYcvoABAPoGNTdkI596.pngGSR模块使用5V供电,按键的另一端连接到GND

1.4 接线实物图通电前请再次确认所有接线无误,特别是VCC和GND不要接反
https://file1.elecfans.com/web3/M00/75/4A/wKgZPGqWbmyAJ6dSABfAOtRExj8721.png
二、安装与使用部分2.1 开源平台-输入"情绪波动实时监测仪"并搜索-代码下载自动打开
https://file1.elecfans.com/web3/M00/75/4A/wKgZO2qWboCAUWFnAAdYB_AuD24998.png2.2 连接-验证-上传
https://file1.elecfans.com/web3/M00/75/4B/wKgZPGqWbo2AGT_6AAwwu2nYxrc883.png2.3 调试-串口监视器
https://file1.elecfans.com/web3/M00/75/4B/wKgZPGqWbpuAMGdlAAwZRcW7nqw387.png
三、代码讲解部分项目代码结构
      本项目代码分为四大模块:Config.h(参数全局配置)、GSRMonitor.h/.cpp(采集与检测)、DisplayManager.h/.cpp(显示驱动)、ButtonManager.h(按键)
3.1 非阻塞状态机
void GSRMonitor::update() {
uint16_t raw = analogRead(GSR_PIN);
lastRaw = raw;

if (!checkContact(raw)) {
    // 开路:不喂入滤波/基线/历史,避免脏数据污染;函数立即返回,
    // 不阻塞、不等待——接触恢复后自然从下一次采样继续。
    systemState = STATE_NO_CONTACT;
    return;
}
systemState = STATE_RUNNING;

filteredValue = filterRaw(raw);

// 波动展示窗口内冻结基线,避免基线被这次波动本身拉走
if (!alertActive) {
    baseline += (filteredValue - baseline) * BASELINE_EMA_ALPHA;
}

float delta = filteredValue - baseline;
unsigned long now = millis();

// ── 情绪波动(SCR)非阻塞检测 ──────────────────────────────────────────────
bool inCooldown = hasEvent && (now - lastEventTime < EVENT_COOLDOWN_MS);
bool eventNow = false;
if (fabs(delta) > EVENT_THRESHOLD && !inCooldown) {
    eventNow = true;
    hasEvent = true;
    lastEventTime= now;
    eventCount++;
    alertActive    = true;
    alertStartTime = now;
    state = GSR_ALERT;
    DEBUG_PRINT(" 情绪波动检测! delta="); DEBUG_PRINTLN(delta);
}

// ── 历史入列:按DISPLAY_REFRESH_MS周期,而非每次采样都入列 ─────────────────
pendingEventMark = pendingEventMark || eventNow;
if (++pushDivCounter >= (DISPLAY_REFRESH_MS / SAMPLE_INTERVAL)) {
    pushDivCounter = 0;
    pushHistory(filteredValue, pendingEventMark);
    pendingEventMark = false;
}

// ── 提醒窗口非阻塞自动结束:到时间就恢复CALM,采样从未停过 ──────────────────
if (alertActive && (now - alertStartTime >= ALERT_DISPLAY_MS)) {
    alertActive = false;
    state = GSR_CALM;
}
}
      |filtered - baseline| > EVENT_THRESHOLD——只有当快速信号偏离慢速基线超过阈值时才触发
[*]EVENT_COOLDOWN_MS(2秒)内不重复触发同一次波动,防止尖峰抖动导致重复计数
[*]记录alertStartTime,由loop()每次检查是否超时,而不是用delay()卡死

3.2 双时间尺度滤波      这是整个项目的算法核心,实现了信号调理和情绪波动检测的完整链路

<blockquote><p>float GSRMonitor::filterRaw(uint16_t newRaw) {</p>标准的有限窗口滑动平均(FILTER_WINDOW_SIZE=8),8个最近采样值的算术平均,负责压掉高频噪声

3.3 按键中断及消抖// 在中断服务函数里调用:只做时间戳记录和消抖判断,不做耗时操作
void onInterrupt() {
    uint32_t now = millis();
    bool pressed = (digitalRead(_pin) == LOW); // INPUT_PULLUP,按下为低

    if (now - _lastEdge < BTN_DEBOUNCE_MS) {
      _lastEdge = now;
      return; // 消抖窗口内的抖动边沿,忽略
    }
    _lastEdge = now;

    if (pressed) {
      _pressStart = now;
      _isPressed = true;
    } else {
      // 没有对应的按下边沿就先收到"释放",是噪声干扰,不是真实按键动作,
      // 直接丢弃——之前这里缺了这层判断,噪声会拿_pressStart=0算出一个
      // 接近"开机到现在"的巨大时长,被误判成超长按触发休眠
      if (!_isPressed) return;
      _isPressed = false;
      uint32_t duration = now - _pressStart;
      if (duration >= BTN_LONGPRESS_MS) _pendingLong = true;
      else _pendingShort = true;
    }
}
知识点说明
volatile关键字中断和主循环共享的变量必须用volatile修饰,防止编译器优化掉“看似没变化”的读取
CHANGE中断模式同时捕获上升沿和下降沿,一次按键按下+松开产生两次中断,分别记录按下时刻和松开时刻

3.4 局部刷新缓存      显示驱动采用了缓存对比+精准清除的策略:
<blockquote><p>// ─── 数值更新(idx: 0=主数值, 1=基线, 2=偏离)──────────────────────────────</p>
[*]脏检查(Dirty Checking) :只有数值真正变化时才发起SPI传输,大幅降低总线占用
[*]精准清除:fillRect的区域与文字区域严格对齐,不会误擦旁边的显示内容
[*]分层绘制:静态底图(顶栏、分隔线、标签)只画一次,动态数值单独刷新

3.5 非阻塞任务调度      loop()是整个系统的“心脏”,采用时间片轮转的非阻塞架构

<blockquote><p>void loop() {</p>调度时间线示意:
https://file1.elecfans.com/web3/M00/75/4A/wKgZO2qWbr6Abp6TAAA7v-8wp0s358.png所有任务交错执行、互不阻塞,这是裸机环境下实现“多任务并行”的核心设计模式
系统流程图
https://file1.elecfans.com/web3/M00/75/4A/wKgZO2qWbsuAQgqFABZImjUHKBg398.png
四、项目结果演示4.1 操作流程①上电启动
      开始约3秒的基线校准,TFT显示"GSR MONITOR"顶栏
https://file1.elecfans.com/web3/M00/75/4A/wKgZO2qWbteADS7AABuceDzOlIY948.png
[*]若电极佩戴良好,进入数据页,实时显示皮肤电导数值、基线、偏离量(Δ),底部显示状态(CALM/ALERT)和累计波动次数
[*]若电极未接触,屏幕内容区变红提示"NO CONTACT",顶栏保留,接触恢复后自动无缝切回正常显示
②按键操作
      BTN_NEXT短按,数据页与趋势图页之间切换,趋势图页展示最近约12秒的皮电曲线和基线参考虚线
https://file1.elecfans.com/web3/M00/75/4B/wKgZPGqWbuaAC5uRACnFNnRbnEE685.png
[*]BTN_ACT短按:触发手动重新校准,屏幕显示"RECALIBRATING BASELINE...",约3秒后恢复正常显示,基线更新为新的读数。
[*]BTN_ACT长按:屏幕进入ST7789芯片级睡眠(黑屏但非填黑,功耗更低),任意按键可唤醒,唤醒后自动恢复之前所在的页面
③触发情绪波动
https://file1.elecfans.com/web3/M00/75/4A/wKgZO2qWbviAXg3AABkb20fumZQ899.png      轻轻掐一下自己或者做一个引起紧张的小动作,顶栏连接圆点会在正常色/红色之间闪烁约3秒,同时LED同步闪烁,状态行文字变为"ALERT",底部波动计数+1,整个过程屏幕数值不会卡顿,因为采样一直在正常进行

④上位机联动
https://file1.elecfans.com/web3/M00/75/4B/wKgZPGqWbwiAbovFAAQGVFC5r5M935.png      串口按115200波特率连接电脑,打开Excel配合PLX-DAQ插件,实时看到皮肤电导曲线随电极接触/情绪波动变化


4.2 视频演示https://live.csdn.net/v/541960?spm=1001.2014.3001.5501
零知派STM32F103RBT6 GSR情绪波动监测仪
本视频演示零知派标准板(STM32F103RBT6)+ ST7789彩屏搭建的GSR皮肤电导实时监测仪完整操作流程:开机基线校准、数据页/趋势图页切换、模拟情绪波动触发提醒、手动重新校准基线、屏幕休眠与唤醒,以及串口联动Excel实时画图全过程演示,重点展示"情绪波动提醒展示期间采样不中断"这一非阻塞设计的实际效果

五、GSR测量原理深度解析5.1 传感器工作原理      皮肤电导测量基于一个简单的生理事实:汗液是电解质溶液,汗腺分泌越多,皮肤电导率越高

人体汗腺受交感神经系统(Sympathetic Nervous System) 控制。当人经历情绪刺激(紧张、恐惧、兴奋)时,交感神经被激活,汗腺分泌增加,皮肤表面的电导率随之上升。这个变化可以在1~3秒内发生,形成可测量的电信号尖峰
https://file1.elecfans.com/web3/M00/75/4B/wKgZO2qWcVSAJeqwAB9-kXwrpbk425.pngGSR信号的两种成分
GSR信号在时域上可分解为两个分量:

成分全称特性时间尺度对应生理
SCLSkin Conductance Level缓慢变化的“基调”数十秒~数分钟整体情绪唤醒水平、体温、出汗量
SCRSkin Conductance Response快速变化的“尖峰”1~3秒特定刺激引发的瞬时情绪反应
本项目中的慢速EMA基线追踪的就是SCL,而快速滑动平均捕获的就是SCR
外源式(exosomatic)测量法
向皮肤施加一个恒定且人体感觉不到的微小电压(通常<0.5V),然后测量流过皮肤的电流。根据欧姆定律:
      电导 G = 电流 I / 电压 V

由于电压恒定,电流与电导成正比。传感器将这个电流信号转换为模拟电压输出,由MCU的ADC读取

5.2 ADC量化与阈值设计 零知派标准板搭载的STM32F103RBT6内置12位ADC,分辨率达12bit,量化公式为:ADC值 = (模拟电压 / 参考电压) × 4096
以3.3V参考电压为例,最小可分辨电压为30:
      3.3V / 4096 ≈ 0.806mV/LSB

本项目Config.h中定义EVENT_THRESHOLD = 60,对应模拟电压变化为:
      60 × 0.806mV ≈ 48.4mV

这个阈值经过了实际测试校准——既足够灵敏到检测SCR尖峰,又不会因噪声而频繁误触发
双滤波器
如果只用原始ADC值直接判断:原始值波动 = 真实SCR + 基线漂移 + 噪声
https://file1.elecfans.com/web3/M00/75/4C/wKgZPGqWcV-AQ9UHAAAfEuOBshA108.png基线漂移(如手部温度变化导致的缓慢出汗量变化)可能达到数十甚至上百个ADC计数值,远大于SCR尖峰。直接用原始值会导致大量误报,项目的双滤波器方案:
原始ADC → 滑动平均(FIR) → filtered → 与baseline比较 → 检测SCR
                                                                  ↑
                                                             EMA(IIR) ← baseline (SCL)
[*]滑动平均:去除高频噪声(工频、接触抖动)
[*]EMA基线:只跟随SCL的缓慢变化,不受SCR尖峰影响
[*]差值比较:|filtered - baseline|本质上是提取了SCR成分,排除了SCL和噪声

六、常见问题解答(FAQ)Q1:触发得特别频繁/特别迟钝?      A:EVENT_THRESHOLD阈值本来就是因人而异的经验值,和电极接触松紧、个人皮肤出汗量都有关系,建议先通过串口打印delta的实际波动范围,再据此调整EVENT_THRESHOLD
Q2:为什么校准的时候要求"保持手指静止不动"?
      A:校准阶段是在给baseline找一个初始参考点,如果这时候手指移动或电极松动,采到的平均值本身就是错的,后续所有delta计算都会跟着偏

本项目由零知实验室(深圳市在芯间科技有限公司)开发维护,仅供个人非商业性学习使用

页: [1]
查看完整版本: 零知派——基于STM32F103RBT6实现GSR皮肤电导情绪波动实时监测仪