ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32手指运动识别手套:从ADC采集到手势判定完整方案

STM32手指运动识别手套:从ADC采集到手势判定完整方案 简介面向嵌入式与机电一体化方向本科毕业设计的手指运动识别手套完整工程包基于STM32F103C8T6实现手指弯曲与伸直动作的实时检测、数据处理及蓝牙或Wi-Fi无线传输。内容按需求分析、技术选型、系统设计分模块展开涵盖电容/光电传感器接入、ucos操作系统移植、无线通信与接收端反馈等关键环节适合需要搭建同类智能手套项目的学生对照学习。压缩包共116个文件大小15.11MB以55个.c源文件和51个.h头文件为主体包含STM32标准外设库、ucos内核源码、1个Keil工程文件.uvproj另附Python辅助脚本、汇编启动文件、Markdown说明文档以及jpg/gif演示素材目录结构清晰便于按模块检索。目前已有167人学习浏览资源具备较高参考价值。整包提供了从工程框架到底层驱动的完整示例可借鉴传感器数据采集、任务调度、无线透传等实际写法配合说明文档和演示图片能直观了解手指动作识别流程有效缩短毕业设计的前期调研与开发周期。1. 手指运动识别手套把 STM32 从课堂搬到手腕上本科毕业设计选这个题目最大的收益不是“做出一个手套”而是把嵌入式开发里最常用的几块能力一次串完传感器模拟量采集、多通道 ADC 与 DMA、数据滤波、阈值分类、串口与上位机交互。一个手指运动识别手套看起来只是把弯曲传感器贴在手套上再用 STM32 读电压、判姿势但真正动手后你会发现难点全在细节——传感器的一致性、佩戴位置的偏差、基线漂移、阈值怎么定、手势之间互相干扰怎么处理。这篇文章不写“项目介绍”直接给出从器件选型到代码落地的完整方案你照着搭就能跑通第一版。适合正在做本科毕设的嵌入式方向学生也适合想拿一个具体项目练手 STM32 的开发者。2. 从传感器到 GPIO看懂手指弯曲信号是怎么变成数字的2.1 弯曲传感器是核心但先要分清楚阻抗型和电压型手指运动识别手套的传感部分通常使用柔性弯曲传感器Flex Sensor。常见做法是选阻值型弯曲传感器它本质上是一个可变电阻平直状态时阻值在 10kΩ 到 25kΩ 之间具体看型号弯曲角度越大阻值越大。这种传感器的典型线性度不算好误差在 ±5% 到 ±10% 之间但对手势识别这种粒度的应用来说足够。你需要避开的坑是不同批次、甚至同一批次不同位置的传感器平直状态阻值差异可能很大所以后面第三章的标定环节不能省。也有人会考虑用 IMU六轴/九轴传感器来做手指姿态识别。IMU 方案的优点是精度高、能识别手指在三维空间的角度但问题也明显你需要给每个手指节都配一个 IMU成本高I2C 总线上挂多个从设备还要处理地址冲突。对于本科毕设弯曲传感器 STM32 是性价比最高、工作量最可控的组合——你只需要一个 ADC 通道接一个传感器五根手指就是五个 ADC 通道。2.2 硬件连接方式分压电路才能让 STM32 读到有效电压STM32 的 ADC 只能读电压不能直接读电阻。所以弯曲传感器要先构成一个分压电路。典型接法如下3.3V —— 弯曲传感器 —— A(ADC引脚) —— 固定电阻 —— GNDA 点的电压计算公式是V_adc 3.3V × R_fixed / (R_flex R_fixed)这里R_flex是弯曲传感器的当前阻值R_fixed是你选用的固定电阻。R_fixed的取值非常关键它决定了 ADC 读数的动态范围。很多人直接把R_fixed设为 10kΩ但这样在传感器平直时R_flex≈10kΩ读到的电压是 1.65V弯曲到最大时R_flex≈30kΩ电压变成 0.825V——满量程 3.3V 只用了四分之一ADC 分辨率浪费严重。我一般会这样算先查传感器手册拿到平直和最大弯曲的阻值然后让这两个状态对应的电压差尽量大。如果平直 10kΩ、最大弯曲 40kΩR_fixed用 15kΩ 时电压从 1.98V 变化到 0.9V差了 1.08V比 10kΩ 方案的 0.825V 好得多。这样后续判手势时每个手指的电压区间拉得更开阈值才好设。注意弯曲传感器是两端的没有正负极之分。但焊接时焊点处很容易折断建议用热缩管包住焊点再固定在手套上。2.3 STM32 的选型F103 就够用别为性能溢付手指运动识别手套对主控的要求并不高五个 ADC 通道、一个串口、若干 GPIO跑在 72MHz 的 STM32F103C8T6 完全够用。这块芯片的 ADC 是 12 位分辨率满量程 4095按 3.3V 参考电压算理论分辨率约 0.8mV。你要是想省事可以直接用 STM32F103C8T6 最小系统板成本低资料多Keil 环境下调试也顺手。但有一个细节要注意STM32F103 的 ADC 输入阻抗有限制官方建议外部信号源阻抗不要超过 4kΩ 左右具体数值在参考手册里有说明。弯曲传感器阻值在 10kΩ 到 40kΩ 之间变化如果你的分压网络等效阻抗太高ADC 采样时内部采样电容来不及充满读数会偏低。解决办法是开 ADC 的采样时间到最大档239.5 个周期或者干脆在 ADC 引脚和分压点之间加一个电压跟随器。做毕设时先把采样时间拉到最大能解决大部分读数不稳的问题。2.4 器件清单与成本估算照着买就行器件型号/规格数量备注主控STM32F103C8T6 最小系统板1带 USB 转串口更省事弯曲传感器Flex Sensor 2.2 英寸或 4.5 英寸5尺寸决定弯曲灵敏度固定电阻15kΩ1% 精度5精度影响一致性手套普通棉质或弹性手套1不要太厚杜邦线/软导线30AWG 硅胶线若干柔韧性好面包板/洞洞板任意1原型阶段用面包板3.3V 稳压AMS1117-3.3如果板卡不带1注意压降和纹波3. 用 STM32 的 ADC 多通道采集把五根手指的数据拉齐3.1 ADC 多通道的两种扫描方式别被“多通道”三个字带偏STM32F103 的 ADC 支持最多 18 个通道在 F103 上实际可用的外部通道是 16 个但 ADC 本身只有一个转换器所谓“多通道”是靠扫描 连续转换完成的。这里有两种常见的配置方式非扫描模式 手动切换通道每次转换前改 ADC 的采样通道寄存器转换完再切换。代码简单但在五通道场景下多了一堆寄存器操作转换效率低。扫描模式 DMA配置 ADC 扫描一组通道每次转换完成后自动切换到下一个通道全部通道转换完会产生一次 DMA 请求把数据搬进内存数组。这是推荐做法CPU 完全不参与数据搬运。我的建议是直接用扫描模式 DMA。代码量多几行但稳定性强很多而且 DMA 把数据送到内存后主循环只需要直接读数组不用关心 ADC 什么时候转换完。3.2 STM32CubeMX 配置要点参数这样设如果你用 STM32CubeMX 生成工程需要注意以下几个配置项配置项推荐值说明ADC ModeIndependent单 ADC 即可Scan Conversion ModeEnabled多通道必须开Continuous ConversionEnabled连续转换DMA 自动搬数DMA Continuous RequestsEnabled保持 DMA 一直搬Number Of Conversion5五根手指Conversion TriggerSoftware用 HAL_ADC_Start_DMA 触发ADC Conversion Sample Time239.5 Cycles最大采样时间扛高阻抗五个通道的 Rank 顺序要和你的硬件接线对应。比如你把手套的食指、中指、无名指、大拇指、小拇指分别接在 PA0 到 PA4就在 CubeMX 里把 Conversion 通道依次配成 ADC_CHANNEL_0 到 ADC_CHANNEL_4。提示ADC 引脚不要和串口引脚冲突。F103 最小系统板上 PA9/PA10 通常是串口 1PA0-PA4 没有冲突可以放心用。3.3 ADC 采样的完整代码DMA 搬运 软件数字滤波这是核心代码直接在 main.c 里补齐/* 定义 ADC 缓冲区五通道存多组数据做滑动滤波 */ #define ADC_CHANNEL_NUM 5 #define ADC_FILTER_DEPTH 8 uint16_t adc_raw[ADC_CHANNEL_NUM] {0}; uint16_t adc_filter_buf[ADC_CHANNEL_NUM][ADC_FILTER_DEPTH] {0}; uint8_t adc_filter_index 0; uint16_t adc_filtered[ADC_CHANNEL_NUM] {0}; /* 定时器中断里调用或直接放主循环每 10ms 读一次 */ void ADC_Update(void) { uint8_t ch 0; uint8_t i 0; uint32_t sum 0; /* 启动一次 DMA 搬运五通道数据会自动填充 adc_raw */ HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_raw, ADC_CHANNEL_NUM); /* 等 DMA 搬运完成 */ while (HAL_ADC_GetState(hadc1) HAL_ADC_STATE_BUSY_INTERNAL); /* 写入滑动滤波缓冲区 */ for (ch 0; ch ADC_CHANNEL_NUM; ch) { adc_filter_buf[ch][adc_filter_index] adc_raw[ch]; } adc_filter_index; if (adc_filter_index ADC_FILTER_DEPTH) { adc_filter_index 0; } /* 计算滑动平均值作为滤波后的结果 */ for (ch 0; ch ADC_CHANNEL_NUM; ch) { sum 0; for (i 0; i ADC_FILTER_DEPTH; i) { sum adc_filter_buf[ch][i]; } adc_filtered[ch] (uint16_t)(sum / ADC_FILTER_DEPTH); } }这段代码的逻辑是每个更新周期触发一次 DMA 搬运五通道数据进入adc_raw然后存入一个环形缓冲区做滑动滤波最后输出adc_filtered供上层手势判定使用。滑动滤波的作用是消除手指轻微抖动和传感器噪声带来的毛刺深度 8 大约对应 80ms 的延迟对手势识别来说完全可接受。需要注意两个参数ADC_FILTER_DEPTH越大越平滑但延迟越高。做静态手势识别可以取 16做动态手势比如快速握拳张开建议取 4 到 8不然会明显感觉“跟不上手”。HAL_ADC_Start_DMA不能连续调用要等上一次转换完成。上面代码里加了一个状态检测while (HAL_ADC_GetState(...) BUSY_INTERNAL)但更好的做法是只在主循环里跑一次启动DMA 连续模式会一直更新数据不需要重复触发。3.4 把 ADC 原始值映射成手指弯曲角度我们真正关心的是“手指弯曲了多少度”而 ADC 值只是电压的数字化表示。惯用做法是采集两个基准状态手指完全伸直时的 ADC 值作为最小值flat_value手指最大弯曲时的 ADC 值作为最大值bend_value然后再做线性映射。/* 标定结构体每个手指存一组基准值 */ typedef struct { uint16_t flat_value; /* 平直时 ADC 值 */ uint16_t bend_value; /* 最大弯曲时 ADC 值 */ } FingerCalib_t; FingerCalib_t finger_calib[ADC_CHANNEL_NUM]; uint8_t angle_min 0; uint8_t angle_max 100; /* 把 ADC 值映射到 0-100 的弯曲度 */ uint8_t ADC_To_Angle(uint8_t ch, uint16_t adc_val) { uint16_t flat finger_calib[ch].flat_value; uint16_t bend finger_calib[ch].bend_value; int32_t angle 0; if (bend flat) return 0; /* 注意分压电路里 ADC 值随弯曲增大而减小方向要判断对 */ if (flat bend) { angle (int32_t)(flat - adc_val) * (angle_max - angle_min) / (flat - bend) angle_min; } else { angle (int32_t)(adc_val - flat) * (angle_max - angle_min) / (bend - flat) angle_min; } if (angle 0) angle 0; if (angle 100) angle 100; return (uint8_t)angle; }这里的angle表示手指弯曲程度0 代表完全伸直100 代表完全弯曲。为什么用 0-100 而不是 0-90 度因为弯曲传感器的输出和真实角度并非严格线性用 0-100 的“弯曲度”概念来处理实现的是线性近似在后续阈值分类时更加直观。4. 手势识别算法从阈值判定到多手指联动判断4.1 多级阈值分类单手指状态先定下来采集到每根手指的弯曲度之后第一步是把单根手指的状态归为“伸直”“半曲”“全曲”三个离散状态。这里不能只设一个阈值常见的做法是用两个阈值配合迟滞区间避免手指在临界位置时状态来回跳变。typedef enum { FINGER_STRAIGHT 0, /* 伸直 */ FINGER_HALF, /* 半曲 */ FINGER_FULL /* 全曲 */ } FingerState_t; #define THRESHOLD_HALF_UP 45 /* 进入半曲的阈值 */ #define THRESHOLD_HALF_DOWN 30 /* 退回伸直的阈值 */ #define THRESHOLD_FULL_UP 75 /* 进入全曲的阈值 */ #define THRESHOLD_FULL_DOWN 60 /* 退回半曲的阈值 */ FingerState_t finger_state[ADC_CHANNEL_NUM]; void Finger_UpdateState(uint8_t ch, uint8_t angle) { switch (finger_state[ch]) { case FINGER_STRAIGHT: if (angle THRESHOLD_HALF_UP) finger_state[ch] FINGER_HALF; break; case FINGER_HALF: if (angle THRESHOLD_FULL_UP) finger_state[ch] FINGER_FULL; else if (angle THRESHOLD_HALF_DOWN) finger_state[ch] FINGER_STRAIGHT; break; case FINGER_FULL: if (angle THRESHOLD_FULL_DOWN) finger_state[ch] FINGER_HALF; break; } }迟滞区间的核心思想是进入某个状态和离开某个状态使用不同的阈值。以半曲为例弯曲到 45 才进入半曲但伸直要降到 30 才退出半曲。这中间 15 个单位的区间就是迟滞带能有效抑制临界点附近的抖动。如果你发现手势识别时状态跳变频繁优先调大迟滞区间而不是去调滤波参数。4.2 结合五个手指的状态来识别手势有了单指状态后手势判定就可以写成一张状态组合表。例如常见的几个手势typedef enum { GESTURE_NONE 0, GESTURE_FIST, /* 握拳 */ GESTURE_OPEN_PALM, /* 张手 */ GESTURE_POINT, /* 食指指向前方 */ GESTURE_OK, /* 拇指食指比 OK */ GESTURE_SCISSORS /* 剪刀手 */ } GestureId_t; #define THUMB 0 #define INDEX 1 #define MIDDLE 2 #define RING 3 #define LITTLE 4 GestureId_t Gesture_Detect(void) { uint8_t full_count 0; uint8_t straight_count 0; for (uint8_t ch 0; ch ADC_CHANNEL_NUM; ch) { if (finger_state[ch] FINGER_FULL) full_count; if (finger_state[ch] FINGER_STRAIGHT) straight_count; } /* 握拳五根手指全曲 */ if (full_count 5) return GESTURE_FIST; /* 张手五根手指全伸直 */ if (straight_count 5) return GESTURE_OPEN_PALM; /* 食指指食指伸直其余全曲 */ if (finger_state[INDEX] FINGER_STRAIGHT full_count 4) return GESTURE_POINT; /* 剪刀手食指和中指伸直其余全曲 */ if (finger_state[INDEX] FINGER_STRAIGHT finger_state[MIDDLE] FINGER_STRAIGHT full_count 3) return GESTURE_SCISSORS; /* OK 手势拇指和食指指尖相对判定略复杂留到 4.3 讲 */ return GESTURE_NONE; }这个判定函数的逻辑是先做整手统计再用单指条件过滤。full_count 4这个条件在食指伸直的情况下等价于排除了“中指单独伸直”的可能。因为四根手指都弯曲时唯一伸直的那根是谁已经固定了不需要再单独判断其他手指的状态。4.3 OK 手势的特殊处理拇指的弯曲度判断要反向OK 手势是典型的“看起来简单、做起来麻烦”的判定。问题出在拇指的弯曲方向和传感器安装方式上食指、中指、无名指、小拇指的弯曲方向一致但拇指是内收弯曲当你把传感器贴在拇指上时它可能不是弯曲而是被拉伸。这就导致同一个FINGER_FULL状态在拇指上的物理含义和在其他手指上不同。解决办法是单独标定拇指的映射方向。在ADC_To_Angle函数里判断flat bend的那个分支就是处理电压随弯曲增大而上升的情况。拇指可能表现为弯曲时 ADC 值上升而不是下降。你在标定前先用手掰一下拇指传感器确认这个方向反过来就把flat和bend对调再初始化一次。OK 手势的判定逻辑可以这样写拇指为全曲或半曲状态食指为半曲状态指尖和拇指接触时食指不会完全弯到底中指无名指小拇指全曲或半曲均可但要排除握拳因为握拳时食指是全曲状态。/* OK 手势拇指全曲/半曲食指半曲不是伸直也不是全曲 */ if ((finger_state[THUMB] FINGER_FULL || finger_state[THUMB] FINGER_HALF) finger_state[INDEX] FINGER_HALF finger_state[MIDDLE] ! FINGER_STRAIGHT finger_state[RING] ! FINGER_STRAIGHT finger_state[LITTLE] ! FINGER_STRAIGHT) { return GESTURE_OK; }这个判定里食指必须是半曲状态。如果你测试发现 OK 手势总是被识别成握拳大概率是食指弯曲角度过大进入了FINGER_FULL可以适当调高THRESHOLD_FULL_UP或者调整标定时的“最大弯曲”基准值。4.4 识别稳定性问题停留时长的确认机制手指从做出一个手势到稳定停在那个姿态中间会有过渡过程。如果不做处理手势识别结果会在一瞬间闪过多个状态比如从握拳到张手的过程中先变成“剪刀手”再变成“张手”。常见做法是持续确认机制同一个手势连续出现 N 次才认为有效通常 N 取 3 到 5 次配合 10ms 的采样周期就是 30 到 50ms 的延迟体感上察觉不到。GestureId_t gesture_current GESTURE_NONE; GestureId_t gesture_candidate GESTURE_NONE; uint8_t gesture_vote_count 0; #define GESTURE_VOTE_THRESHOLD 4 void Gesture_Update(void) { gesture_candidate Gesture_Detect(); if (gesture_candidate gesture_current) { gesture_vote_count 0; return; } if (gesture_candidate GESTURE_NONE) { gesture_vote_count 0; return; } gesture_vote_count; if (gesture_vote_count GESTURE_VOTE_THRESHOLD) { gesture_current gesture_candidate; gesture_vote_count 0; /* 此时可以向上位机发送一次新手势事件 */ USART_SendGesture(gesture_current); } }这段投票逻辑的作用是“确认手势变化再上报”。注意它把GESTURE_NONE单独处理了如果你在做一个手势的途中手指稍微松开导致一瞬间没有命中任何手势规则不会重置投票计数只有手势真正变化时才重新计票。5. 穿戴标定与调试数据校准、佩戴一致性跟手性排查5.1 第一步采集两个基准状态做快速标定任何新人戴上手套后第一件事就是重新标定。因为每个人的手指长度、粗细以及手套松紧程度不同传感器弯曲的幅度也会不同。标定过程建议做成一个串口交互命令通过串口发送指令提示用户分别做“完全伸直”和“完全弯曲”两个动作各保持 2 秒单片机在这段时间内持续采样并取平均值。void Calibration_Start(uint8_t ch, uint8_t mode) { uint32_t sum 0; uint32_t sample_count 0; /* mode: 0标定平直值1标定弯曲值 */ while (sample_count 200) { if (/* 串口收到 C0 或 C1 指令 */. ) { sum adc_filtered[ch]; sample_count; } HAL_Delay(10); } if (mode 0) finger_calib[ch].flat_value (uint16_t)(sum / sample_count); else finger_calib[ch].bend_value (uint16_t)(sum / sample_count); }200 次采样大约耗时 2 秒取平均值能消除手部微小抖动。5.2 用串口波形观察 ADC 原始值排干扰的第一手段调试过程中最烦人的问题就是“明明手上没动数据在跳”。排查手段是先把原始 ADC 值通过串口发到上位机看波形。很多嵌入式开发者会直接用串口助手打印数值但数据刷新太快时根本看不清。我常用的做法是在串口助手里开启“时间戳显示”然后程序里以固定的 50ms 周期发送数据uint16_t timestamp 0; void UART_SendTelemetry(void) { char buf[64]; timestamp; sprintf(buf, %d,%d,%d,%d,%d,%d\r\n, timestamp, adc_filtered[0], adc_filtered[1], adc_filtered[2], adc_filtered[3], adc_filtered[4]); HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), 100); }把数据粘贴到 Excel 或直接用串口助手的曲线绘制功能就能看到每个通道的噪声幅度。如果噪声表现为 3-5 个 ADC 码值的快速抖动属于正常范围滑动滤波已经能压住。如果跳动超过 20 个码值优先检查接线是否牢固、传感器分压点是否受到手指运动时线缆弯折的影响。如果所有通道同时跳动要考虑电源问题比如用 USB 供电时电流不足或者开关电源纹波过大。5.3 从 2.2 英寸到 4.5 英寸传感器尺寸差异对灵敏度的影响这是实测中非常明显的一个点2.2 英寸的弯曲传感器灵敏度更高但有效弯曲区域短贴在小手指上正好4.5 英寸的传感器更适合食指和中指因为这两个手指较长传感器需要覆盖两个指节才能采集到完整弯曲。如果你用 4.5 英寸传感器贴在小手指上可能出现的情况是小手指弯曲 90 度时传感器才弯曲 30 度ADC 变化范围被压缩到很小。解决方案是给每个手指单独选型或者统一用 2.2 英寸、然后佩戴时把传感器固定在指背正中央确保最大弯折点落在传感器敏感区。5.4 手势识别响应“慢半拍”的排查顺序第 4 章实现了投票确认机制如果出现响应慢排查顺序应该是先确认是滑动滤波延迟、投票延迟还是串口上报延迟。滑动滤波器的时间常数可以通过ADC_FILTER_DEPTH调整深度 8 的延迟大约 40ms。投票机制的延迟是GESTURE_VOTE_THRESHOLD × 周期4 次投票即 40ms。两者叠加在 100ms 以内体感上属于正常范围。如果明显觉得慢优先把投票阈值从 4 降到 2——这个改动对稳定性的影响通常不大但对响应速度的提升很明显。另一个隐蔽的坑是while (HAL_ADC_GetState(hadc1) HAL_ADC_STATE_BUSY_INTERNAL);这个循环。DMA 搬运在连续模式下不会特别慢但如果你的主循环里有其他耗时操作比如HAL_UART_Transmit的阻塞发送就会拖慢整个周期。建议改用 DMA 发送串口数据或者把串口发送改成中断方式。5.5 进阶方向手势模板化与最近邻分类阈值分类能做到 6-8 个手势的可靠识别但如果你想识别更复杂的手势比如数字 1 到 9、握拳并旋转手腕阈值规则会变得非常繁琐。更工程化的方案是直接采集手势模板让用户每个手势做 50 次把五根手指的弯曲度均值作为模板识别时计算当前弯曲度向量和所有模板的欧氏距离distance sqrt( sum( (angle_i - template_i)^2 ) )选距离最小的模板作为识别结果。这种思路用 STM32F103 完全跑得动——五维向量的欧氏距离计算量极小在 72MHz 下几百个时钟周期就能完成。模板数量可以从 5 个拓展到 15 个不需要重新设计电路或换主控。具体做法是在标定完finger_calib后再增加一个手势模板学习流程。串口发送“LEARN 3”提示用户摆出手势 3 的姿势保持 5 秒这期间每 100ms 采集一次弯曲度向量累计 50 组后求平均存入 Flash。识别时每 100ms 算一次距离取距离最小的手势号作为结果同样加上迟滞判定防止频繁跳变。Flash 存储几个模板只需要很小空间每个模板五字节50 个模板也才 250 字节F103 的 64KB Flash 完全无压力。存储地址建议放在最后一页避免和代码区冲突F103C8T6 的最后一页从 0x0800FC00 开始。写入前先擦除整页更新模板时注意不要频繁写 Flash磨损寿命按 1 万次算日常调试足够。本文还有配套的精品资源点击获取
返回列表