资讯详情

资讯详情

STM32 EMG假肢手实战:信号调理、手势识别与PWM舵机控制

简介一套以STM32微控制器为核心的机器人假肢肌电项目资料面向嵌入式开发、生物医学工程及机器人爱好者重点解决如何采集人体肌电信号并驱动伺服电机模仿手势的工程问题。压缩包共包含十八个文件整体大小约十一兆其中有五个C语言源文件、六份PDF资料以及多个传感器数据手册与滤波模块说明覆盖从信号采集、滤波处理到电机驱动的完整开发链路。源码部分展示了双传感器采集、信号平滑滤波、控制器初始化和脉宽调制控制等关键模块文档部分则提供参考手册、传感器规格和假肢结构说明能帮助读者理解硬件选型依据与底层驱动原理。目前已有130人学习下载适合具备C语言基础、希望深入实践嵌入式系统与生物信号处理的开发者是一份兼顾理论讲解与实际工程的综合项目参考。1. EMG假肢手项目绕不开的不是电机而是信号调理一个EMG假肢手的电路拓扑和一个五自由度舵机玩具几乎一样主控、电源、几路PWM、若干伺服电机。差距全在最上游那几百毫伏的信号上——肌肉收缩时皮肤表面的电位只有几十微伏到几毫伏20Hz到500Hz以外全是噪声伺服电机一动作电流冲击还会顺着供电线窜回采样端。所以这个项目的功夫不在电机而在信号调理、特征提取和手势状态切换这三段。下面按感知、决策、执行三条线拆开写。如果你有STM32裸机开发和C语言基本功跑通整条链路要不了太久想真正戴在手臂上稳定演示还得在阈值和供电上多花几个小时。常见毕业设计里基于STM32的假肢手核心也就是这件事把五路舵机变成一只手再用两路EMG信号告诉它该抓还是该放。2. STM32上的EMG信号调理与特征提取2.1 为什么EMG信号要先放大整流滤波而不是直接接ADC表面EMG是差分信号源两个信号电极贴在肌肉肌腹处读出的是皮肤表面的电位差。它的幅度范围大致在几十微伏到几毫伏比STM32的ADC量程低得多12位ADC、3.3V参考电压每个LSB约0.8mV原始信号直接接进去会被量化噪声淹没软件里做多少倍增益都救不回来因为低于量化台阶的细节已经丢了。因此在ADC之前必须做硬件放大常见做法是用仪表放大器实现差分输入增益设在500到1000倍。这样1mV的肌电信号被放大到0.5V到1V落在ADC最灵敏的中段区间。放大器之后还要做一次带通滤波。运动伪迹和电极极化电压集中在5Hz以下会把放大器的动态范围占掉大半工频和无线电干扰则在高频段进入放大器。我在第一版里直接买带放大和滤波的表面肌电模块输出0到3.3V的模拟量三根线接到ADC就能用省掉一周的模拟前端调试时间。等整条链路跑通回头再自己搭仪表放大电路也不迟那时你已经知道每一级滤波应该放在哪、带宽设置多少才合适。提示自己搭前端时两个信号电极要顺着肌肉走向贴参考电极固定在手腕骨性位置参考电极不要贴在肌肉上否则共模信号会被当作差模信号放大波形看起来像噪声一样乱跳。2.2 让ADC采到可分析的肌电信号采样率、参考电压与增益EMG能量集中在20到500Hz理论采样率1kHz就能满足奈奎斯特定理我一般用2kHz给数字滤波留出过渡带也让特征窗口能取到整数个采样点。下面是一份可以直接照抄的参数表参数数值说明采样率2000 Hz覆盖EMG有效频段并留余量ADC位宽12 bitSTM32F1系列原生支持参考电压3.3 VVDDA要单独滤波不能直接接电源硬件增益10001mV信号放大到约1V高通截止20 Hz去掉运动伪迹和基线漂移低通截止500 Hz去掉高频噪声和混叠ADC的参考电压直接决定量化分辨率。3.3V除以4096每个LSB约0.8mV放大后的信号最大接近1V约占量程四分之一既不会满幅削波又有足够的信噪比。采样方式我习惯用定时器触发加DMA循环CPU完全不用管数据搬运#define EMG_BUF_LEN 256 static uint16_t emg_raw[EMG_BUF_LEN]; void emg_start(void) { // TIM6 每 500us 触发一次 ADC转换结果由DMA写入缓冲区 HAL_ADC_Start_DMA(hadc1, (uint32_t *)emg_raw, EMG_BUF_LEN); HAL_TIM_Base_Start(htim6); }这里的定时器配置是72MHz主频、预分频71、自动重载值499计数频率1MHz周期正好500微秒对应2kHz采样率。DMA模式必须设置成Circular循环缓冲区写满后自动回到开头否则采集一会儿就停了。第二个参数强制转换成uint32_t*是HAL库的接口限制实际写入的是uint16_t值高四位在12位模式下恒为0。2.3 在C语言里实现带通滤波与MAV/RMS特征帧ADC原始值里混着直流偏置、基线漂移和高频毛刺直接算特征值会得到一个虚高的常数。常见做法是软件里先做一阶高通把20Hz以下的基线成分切掉再做全波整流最后在滑动窗口内累计出包络。#define HP_A 0.94f // 2kHz采样率下对应截止频率约20Hz static float hp_x1 0.0f; static float hp_y1 0.0f; float emg_highpass(float input) { float y HP_A * (hp_y1 input - hp_x1); hp_x1 input; hp_y1 y; return y; }一阶高通的系数来源是RC滤波器的离散化RC约8ms采样周期0.5msa RC / (RC T) ≈ 0.94。这个滤波器在每个采样点上只有一次乘法和两次加法放在中断里做都毫无压力。高通输出整流之后不需要再加数字低通后面的特征窗口本身就是滑动平均会把整流后的纹波平滑成一条缓慢变化的包络线。窗口长度选择决定响应速度和稳定性的平衡。128ms窗口对应256个采样点我让窗口每次滑动64ms也就是50%重叠特征更新率约15Hz。typedef struct { float mav; float rms; uint16_t zc; } EmgFeature; void emg_extract_feature(const float *filtered, uint16_t len, EmgFeature *feat) { float sum_abs 0.0f, sum_sq 0.0f; uint16_t zc 0; for (uint16_t i 1; i len; i) { sum_abs fabsf(filtered[i]); sum_sq filtered[i] * filtered[i]; if (filtered[i - 1] 0.0f filtered[i] 0.0f) { zc; } } feat-mav sum_abs / (float)len; feat-rms sqrtf(sum_sq / (float)len); feat-zc zc; }MAV在中等力度时已经能清晰区分激活和静息RMS对收缩峰值更敏感高强度时区分度比MAV好。过零率暂时只做参考不参与阈值判断等到后面要区分捏合和握拳时可以用它辅助判断收缩的平稳程度。这段代码用到了fabsf和sqrtf记得包含math.h。2.4 静息基线标定给阈值一个不是拍脑袋的起点把激活阈值写成固定值是EMG项目里最容易翻车的地方。换一个使用者、重新贴一次电极、或者手臂摆放姿势不同基线噪声和信号幅度都会变硬编码阈值在实验室能用换个人就失灵。常见做法是上电后做一个短标定手臂完全放松采集200ms的滤波后数据计算基线的均值和标准差然后按四倍标准差定激活阈值。typedef struct { float mean; float sigma; float active_th; float release_th; } Baseline; void calibrate_baseline(const float *samples, uint16_t len, Baseline *b) { float sum 0.0f; for (uint16_t i 0; i len; i) sum samples[i]; b-mean sum / (float)len; float var 0.0f; for (uint16_t i 0; i len; i) { float d samples[i] - b-mean; var d * d; } b-sigma sqrtf(var / (float)len); b-active_th b-mean 4.0f * b-sigma; b-release_th b-mean 2.0f * b-sigma; }激活阈值取四倍标准差理论上静息信号超过它的概率只有万分之三左右误触发的可能性很低。释放阈值取两倍标准差是为了制造迟滞进入激活的门槛高退出激活的门槛低信号在临界区域抖动时通道状态保持稳定。标定时如果发现滤波后均值明显不为零先检查传感器偏置、放大器和ADC参考电压不要急着调阈值。EMG本身是交流信号整流后静息均值应该接近零均值偏高说明前面还有直流问题没解决。3. 手势识别与状态机两路EMG怎么变成稳定的手势意图3.1 两通道分出手势屈肌和伸肌的组合电极贴放位置直接决定分类上限。通道1贴在前臂屈侧的肌腹处握拳时指浅屈肌和掌长肌收缩明显MAV升高通道2贴在伸侧对应指总伸肌伸掌时MAV升高参考电极固定在手腕骨性位置。两个通道的高、低组合可以分出四类状态手势通道1屈侧通道2伸侧执行目标静息低低保持当前角度握拳高低四指弯曲伸掌低高四指伸展捏合中高低拇指和食指对捏握拳和捏合都由屈肌产生区别在于收缩强度。所以判定时不能只用低、高两档还要看通道1的MAV是中还是高这需要再设一个中等强度的判定档位。捏合时肌肉动员的运动单位少MAV大约只有握拳的六到七成经验值是在激活阈值上加一档具体倍数按实际测量标定。屈腕动作会让两块肌群同时收缩两个通道同时变高这个手势在两通道方案里很难区分先避开不做可靠性比覆盖面更重要。3.2 阈值迟滞和连续帧稳定单独一个特征帧不能作为切换依据。手势识别里最常见的故障是抖动手环式来回切换——肌肉力量在阈值附近轻微波动一帧高一帧低舵机就会在握拳和伸掌之间疯狂摆动。我一般用一个极小的状态机每帧先算出原始手势如果连续两帧一致才切换当前手势否则保持原状。typedef enum { GRIP_IDLE 0, GRIP_FIST, GRIP_OPEN, GRIP_PINCH } GestureId; typedef struct { GestureId current; GestureId candidate; uint8_t keep_cnt; } GestureState; void gesture_update(GestureState *gs, GestureId raw) { if (raw gs-candidate) { gs-keep_cnt; if (gs-keep_cnt 2) { gs-current raw; gs-keep_cnt 0; } } else { gs-candidate raw; gs-keep_cnt 0; } }这个候选加确认的机制把单帧误判过滤掉肌肉偶然的抽动最多影响一帧下一帧立刻恢复原状不会造成舵机动作跳变。代价是大约一帧到两帧的额外延迟对假肢手来说完全在可接受范围内。判定每帧原始手势之前通道激活状态要单独维护不能每帧直接拿最新MAV和阈值比大小。我单独维护一个active状态变量bool ch_active_update(EmgChannel *ch, float mav) { if (!ch-active mav ch-baseline.active_th) ch-active true; if (ch-active mav ch-baseline.release_th) ch-active false; return ch-active; }两个阈值构成迟滞回路激活阈值和释放阈值之间的区间不会产生高速翻转通道状态输出比原始MAV值稳定得多15Hz的特征更新率不会让手势结果跟着抖。3.3 手势到舵机角度的映射表确认手势后下一步是把它翻译成五路舵机的目标角度。五舵机假肢手把拇指简化成一个弯曲自由度其余四指各一个。角度映射我直接写成常量表每行对应一个手势每列对应一根手指的期望角度。typedef enum { F_THUMB 0, F_INDEX, F_MIDDLE, F_RING, F_LITTLE } FingerId; /* 手势角度表行是手势列是手指单位是度 */ static const float gesture_angles[4][5] { /* GRIP_IDLE */ { 90.0f, 90.0f, 90.0f, 90.0f, 90.0f }, /* GRIP_FIST */ { 20.0f, 10.0f, 10.0f, 10.0f, 10.0f }, /* GRIP_OPEN */ {170.0f, 170.0f, 170.0f, 170.0f, 170.0f }, /* GRIP_PINCH */ { 90.0f, 15.0f, 160.0f, 160.0f, 160.0f }, }; void apply_gesture(GestureId g) { for (int i 0; i 5; i) { set_finger((FingerId)i, gesture_angles[g][i]); } }捏合手势让拇指保持中立食指弯曲到15度中间三指微微弯曲形成指尖对捏的姿态。表里的角度写的是舵机输出轴的目标角度舵机装上假肢手后安装方向可能相反这一点留到PWM映射层统一处理不要在表里填负角度或者反向值那样多调几次就会混乱。4. 用STM32定时器输出多路PWM驱动伺服电机手4.1 舵机要的是0.5~2.5ms脉冲不是任意占空比机械臂里说的伺服电机分两类工业伺服带编码器和驱动器走脉冲串或总线控制位置闭环在驱动器内部完成假肢手用的舵机则是直流电机加减速箱加电位器反馈全部集成在一个小塑料壳里控制方式简单得多——20ms一个周期高电平宽度决定输出轴角度。脉宽舵机角度0.5ms0°1.0ms45°1.5ms90°2.0ms135°2.5ms180°高电平宽度和角度的对应关系由舵机内部电位器反馈电路决定不是标准PWM占空比。把PWM频率配成1kHz是新手最容易犯的错误舵机会发烫、抖动甚至冒烟。假肢手指尖的动作速度本来就要求不高50Hz的刷新率远够用。4.2 定时器分频与引脚映射五路PWM的具体配置STM32F103主频72MHz要输出20ms周期的PWM配置PSC71后计数频率变成1MHzARR19999计数器每增加1正好是1微秒。0.5ms脉宽对应比较值5002.5ms对应2500角度计算非常直观。引脚分配要避开ADC输入和调试接口。我常用的方案是TIM2加TIM3组合出五路定时器通道引脚手指TIM2CH1PA0拇指TIM2CH3PA2食指TIM2CH4PA3中指TIM3CH1PA6无名指TIM3CH2PA7小指TIM2的CH2PA1特意留空不用这样可以避免和附近外设的复用冲突。五个PWM通道同时启动时定时器之间不需要同步舵机对微秒级的相位差异根本不敏感。4.3 角度到比较值映射与PWM初始化在CubeMX里把预分频和自动重载值填好PWM映射到对应引脚后初始化代码只需要启动通道并让所有手指回到90度中立位。void servos_init(void) { HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_4); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); set_finger(F_THUMB, 90.0f); set_finger(F_INDEX, 90.0f); set_finger(F_MIDDLE, 90.0f); set_finger(F_RING, 90.0f); set_finger(F_LITTLE, 90.0f); }角度到比较值的换算集中在一个函数里#define CCR_MIN 500 /* 0.5ms 脉宽 */ #define CCR_MAX 2500 /* 2.5ms 脉宽 */ uint16_t angle_to_ccr(float deg) { if (deg 0.0f) deg 0.0f; if (deg 180.0f) deg 180.0f; return (uint16_t)(CCR_MIN (deg / 180.0f) * (CCR_MAX - CCR_MIN)); } void set_finger(FingerId id, float deg) { uint16_t ccr angle_to_ccr(deg); switch (id) { case F_THUMB: __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr); break; case F_INDEX: __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, ccr); break; case F_MIDDLE: __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_4, ccr); break; case F_RING: __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); break; case F_LITTLE: __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, ccr); break; } }set_finger的switch看起来冗长但好处是所有引脚映射关系集中在一个函数里换板子或者调整通道时只改这一处。更新比较值的时机放在特征帧更新里不要在ADC中断里逐路修改。若舵机在目标角度附近来回嗡鸣可以让当前角度和目标角度的差值小于3度时跳过更新减少舵机齿轮的磨损。提示角度范围两端要留出至少10度到20度的机械余量。廉价舵机持续顶着限位会堵转发热电流上升后不仅损坏舵机还会把供电电压拉低。4.4 舵机供电独立信号地和功率地必须共地舵机同时启动的瞬时电流超过1A很常见尤其是从静止状态突然加速的那一下。如果和信号电路共用同一路LDO电压跌落会直接体现在ADC采样值上肌电波形叠加周期性尖峰特征值忽高忽低。常见做法是舵机使用独立电源比如7.4V锂电池接BEC降压到5V或6V信号电路单独用3.3V线性稳压两者之间只保留一个共地连接点。单点共地既保证控制信号有参考地平面又避免功率电流流过信号地产生干扰。舵机电源端加一颗470微法以上的电解电容放在舵机接线插座附近用于吸收启停瞬间的电流冲击。调试阶段如果USB一插、舵机一动MCU就复位优先怀疑电源而不是程序。USB口的供电能力只有500mA左右假肢手的动力不要从USB取USB只留给调试器和MCU供电。5. 排错与验证先查信号链再查电机5.1 把滤波前后的波形捞出来看信号链路没有确认之前不要去调分类阈值。上电后先让串口以特征帧速率输出MAV和RMS在PC端用串口绘图器看波形。手动握拳时波形幅度应该随力度明显上升松开后回到基线附近。printf(mav:%d rms:%d\n, (int)(emg_feat.mav * 1000.0f), (int)(emg_feat.rms * 1000.0f));波形纹丝不动就查ADC配置和DMA是否启动波形乱跳就查供电、参考电压和电极贴放波形干净清楚只剩分类错误时再去调迟滞阈值。这一步能省掉至少半天盲目排错的时间。5.2 ST-Link连不上的踩坑流程遇到no stm32 target found或者提示debug authentication相关的报错时先看SWDIO、SWCLK、GND、3.3V四根线是否连接正确再看目标板是不是没上电、NRST有没有被外接电路拉低。排除了物理连接之后最常见的隐藏问题是程序里把SWD引脚重映射成了普通GPIO下载器访问不到内核。处理办法是按住复位键后点击下载在下载器开始连接的瞬间松开如果不行把BOOT0拉高重新上电目标板进入ISP模式用串口工具执行全片擦除再恢复BOOT0并重新烧录。这个流程在舵机项目里几乎每台设备都遇到过毕竟GPIO不够时最容易借用的就是PA13和PA14。5.3 伺服手的第一轮全行程自检整机联调前先跑一遍舵机自检按顺序让每根手指完成90度到0度、再到180度、最后回到90度的往复。void servo_self_test(void) { const FingerId fingers[5] {F_THUMB, F_INDEX, F_MIDDLE, F_RING, F_LITTLE}; for (int i 0; i 5; i) { set_finger(fingers[i], 90.0f); HAL_Delay(500); set_finger(fingers[i], 180.0f); HAL_Delay(500); set_finger(fingers[i], 90.0f); HAL_Delay(500); } }这轮自检能同时暴露线序、安装方向和机械干涉三类问题也能验证供电余量是否足够如果多点同时运动时出现复位或抽动说明电源容量不足。若发现PWM通道和手指对应错位直接调整set_finger里的通道映射或者在映射表里统一翻转角度方向角度方向问题不要在多个地方分别修复统一在映射层改后面的手势表才不会跟着乱。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →