简介基于STM32的手指运动识别手套本科毕业设计资料专为嵌入式方向学生完成机电一体化与传感器融合类课题而整理。方案围绕需求分析、技术选型、系统设计展开核心采用STM32F103C8T6主控配合MPU6050惯性传感器及多路ADC采集实现手指弯曲动作检测并给出蓝牙/Wi-Fi无线传输与上位机交互思路。压缩包共116个文件约15.11MB以55个C源码和51个头文件为主覆盖UCOS-II系统内核与任务调度、MPU6050 DMP运动驱动、STM32定时器及ADC外设配置另含Keil工程文件、汇编启动文件、Python辅助脚本和Markdown说明文档便于直接打开工程查看模块划分与代码结构。已有167人学习浏览适合正在开展STM32毕设或嵌入式综合项目设计、需要参考完整代码框架与系统裁剪思路的本科高年级学生。1. 从惯性数据到手指角度STM32 手指运动识别手套的整体方案拿到“手指运动识别手套”这个题目最容易误判的地方是把重点全压在传感器数量上堆一堆弯曲传感器和陀螺仪最后在主控上跑不过来。这套基于 STM32F103C8T6 的源码给我的启发正好相反手指运动识别的工程化重点不是“算”而是“切”——用 MPU6050 的 DMP 输出手部整体姿态四元数用 STM32 内置 ADC 采集每根手指的弯曲程度两类数据再交给 uC/OS 任务做时间对齐和帧打包。源码包里 os_core.c、os_task.c、os_flag.c、os_cpu_a.asm 构成了完整的 RTOS 内核inv_mpu.c 和 inv_mpu_dmp_motion_driver.c 是 InvenSense 官方驱动stm32f10x_rcc.c、stm32f10x_tim.c、stm32f10x_adc.c 则覆盖了时钟、定时器和采集外设。对于正在做毕业设计、准备蓝桥杯嵌入式国赛或者想深入嵌入式内核源码的人来说这是一个能把“任务调度、外设配置、通信协议”一次串起来的中型工程样本。2. MPU6050 DMP 数据通路的初始化顺序2.1 为什么选择 DMP 而不是 MCU 裸算姿态很多基于 stm32 的毕业设计一上来就在 Keil 里写互补滤波或四元数解算代码能跑但调试复杂一个姿态角算错往往要同时查数学公式、传感器量程和中断时序。而 InvenSense 的 inv_mpu.c 和 inv_mpu_dmp_motion_driver.c 把姿态融合算法固化在 MPU6050 内部 DMP 协处理器里STM32 只负责通过 I2C 读取 FIFO 中的四元数。这个选择的好处非常直观第一MCU 的负载明显下降。STM32F103C8T6 虽然有 72MHz 主频但手指识别任务还要做 ADC 采样、USB/串口虚拟端口和无线发送把姿态解算下沉到传感器内部相当于把嵌入式系统中“能由外设完成的事就不占用 CPU”的原则落到了实处。第二DMP 输出的四元数在时间上更稳定。手工写互补滤波时加速度计和陀螺仪的融合系数需要反复调DMP 固件则通过内部运动处理器输出 lsb 格式的四元数量纲固定方便后续上位机直接换算成 yaw/pitch/roll。尤其当你是第一个进入该项目的人代码里还带着 os_cpu_a.asm 这种汇编文件时尽量减少 MCU 侧数学计算能让整套任务的时间预算更宽松。2.2 手指导线与 ADC 通道映射手指运动识别手套的传感层通常分两种信号惯性信号和弯曲信号。MPU6050 负责手背朝向和翻转动作柔性的弯曲传感器则贴在每根手指背面弯曲越大传感器阻值变化越明显。STM32F103 的方案是把弯曲传感器接成一个分压电路输出端直接接到 ADC1 的通道上。信号传感器STM32 引脚外设采样方式拇指弯曲弯曲传感电阻 10k~30kPA0ADC1_IN0定时器触发单次转换食指弯曲弯曲传感电阻 10k~30kPA1ADC1_IN1定时器触发单次转换中指/无名/小指同类型传感器PA2~PA4ADC1_IN2~IN4定时器触发单次转换手背姿态MPU6050PB6/PB7I2C1DMP FIFO 读取设计时需要留意的是弯曲传感器的分压电阻不能选太小否则整个量程的电压变化会被压缩到几十毫伏以内。我一般会先用手持万用表测“伸直”和“握拳”两种状态下的阻值再按中点取分压电阻。例如伸直时约 10kΩ、握拳时约 30kΩ分压电阻取 15kΩ可以让 ADC 输入电压在中点附近摆动满量程利用率更高。2.3 DMP 初始化代码顺序源码包里 inv_mpu.c 和 inv_mpu_dmp_motion_driver.c 的初始化顺序有严格依赖关系必须先 mpu_init再设置传感器量程和 FIFO然后 dmp_load_motion_driver_firmware 加载固件最后使能 DMP 功能。顺序颠倒会导致 dmp 相关接口直接返回错误码。#include stm32f10x.h #include inv_mpu.h #include inv_mpu_dmp_motion_driver.h static signed char gyro_polarity[9] {1, 0, 0, 0, 1, 0, 0, 0, 1}; void MPU_DMP_Init(void) { struct int_param_s int_param {0}; if (mpu_init(int_param) ! 0) { while (1); // I2C 通信失败调试时在此设置断点 } mpu_set_gyro_fsr(2000); // 陀螺仪量程 ±2000dps mpu_set_accel_fsr(2); // 加速度计量程 ±2g mpu_set_sensors(INV_XYZ_GYRO | INV_XYZ_ACCEL); mpu_configure_fifo(INV_XYZ_GYRO | INV_XYZ_ACCEL); dmp_load_motion_driver_firmware(); // 向 MPU6050 RAM 加载 DMP 固件 dmp_set_orientation(inv_orientation_matrix_to_scalar(gyro_polarity)); dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT | DMP_FEATURE_SEND_RAW_ACCEL); dmp_set_fifo_rate(50); // 50Hz每 20ms 出一组四元数 mpu_set_dmp_state(1); }这段代码里dmp_set_fifo_rate(50) 是整个任务时间基准的来源。后续 uC/OS 的传感器任务会按照 20ms 周期读取 FIFO因此该参数必须和任务调度周期保持一致。如果 fifo_rate 和任务周期不一致可能出现读到的四元数还是上一帧的情况手势识别会感觉“慢半拍”。另外一个容易踩的坑是 I2C 引脚和 MPU6050 AD0 地址的对应关系。AD0 接地时地址为 0x68接 VCC 时为 0x69。如果手边有两块模块或者曾经把模块从 3.3V 供电板拆到 5V 供电板优先检查这个引脚。烧录后如果发现 mpu_init 返回非零用逻辑分析仪抓 PB6/PB7 波形确认是否有 ACK 信号而不是直接改代码。3. uC/OS 任务拆解与 STM32 外设驱动分配3.1 源码包里 os_* 文件在系统中承担什么这套源码的内核是 uC/OS-II而不是裸机 while 循环。文件清单里的 os_core.c、os_task.c、os_flag.c 是内核核心模块os_cpu_a.asm 是 Cortex-M3 的汇编移植层。很多嵌入式学习路线会从裸机点灯直接跳到嵌入式 Linux中间缺了 RTOS 这一层这套代码正好把这个空缺补上。os_core.c 负责初始化调度器和中断控制os_task.c 提供 OSTaskCreateExt 等任务管理接口os_flag.c 用于事件标志组同步os_cpu_a.asm 则实现 PendSV 异常下的任务上下文切换。源文件功能在本项目里的具体作用os_core.c内核调度、延时管理OSTimeDlyHMSM 延时决定任务周期os_task.c任务创建、删除、堆栈检查创建传感器任务和通信任务os_flag.c事件标志组传感器数据就绪后通知通信任务os_cpu_a.asm汇编级上下文切换PendSV_Handler 保存/恢复寄存器stm32f10x_rcc.c时钟树配置外部晶振倍频到 72MHzstm32f10x_tim.c定时器配置产生 ADC 触发信号和 PWM 输出stm32f10x_flash.cFlash 等待周期配置保证 72MHz 下 Flash 访问稳定stm32f10x_adc.cADC 采集读取五路弯曲传感器电压这套结构也正好回答了常见的嵌入式面试题任务间通信用过哪些机制这里用的就是事件标志组而不是全局变量。全局变量在简单裸机程序里没问题但在 RTOS 多任务环境下因为任务切换可能发生在任意指令之间读改写操作不保证原子性数据同步就会出问题。3.2 时钟、Flash 和 ADC 外设的基础配置开始写任务前先把最小硬件环境跑稳。STM32F103C8T6 外部接 8MHz 晶振通过 PLL 倍频到 72MHz。这里涉及 stm32f10x_rcc.c 和 stm32f10x_flash.c 两个文件RCC 负责 PLL 配置和各个外设总线时钟使能Flash 控制器的等待周期必须和主频匹配。如果只开了 RCC 而没设置 FLASH_ACR高速运行时会随机跑飞表现形式是串口输出乱码或硬件中断异常。void System_Periph_Init(void) { ErrorStatus status; RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); status RCC_WaitForHSEStartUp(); if (status SUCCESS) { FLASH_SetLatency(FLASH_Latency_2); // 72MHz 下需要 2 个等待周期 RCC_HCLKConfig(RCC_SYSCLK_Div1); RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 72MHz给 ADC1 使用 RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 36MHz给 USART2/3 使用 RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); } }上面这段是典型的 STM32F103 时钟初始化模板。注意 APB2 总线上的 ADC 时钟不能超过 14MHz否则采样精度会明显下降。如果 PCLK2 被配成 72MHzADC 还需要再分频通常取 6 分频得到 12MHz。实际项目里晶振电容也值得看两个负载电容不是随便选的一般按晶振厂商标称负载电容计算典型值为 18pF~22pF。如果示波器看 HSE 起振不稳先检查匹配电容和 PCB 走线寄生电容而不是换芯片。3.3 传感器任务和通信任务如何用事件标志组同步任务划分是整套代码的骨架。这里将主流程拆成两个任务传感器采集任务 App_TaskSensor 和数据发送任务 App_TaskComm。传感器任务每 20ms 周期读取 DMP 四元数和五路 ADC 结果然后通过 OSFlagPost 置位事件标志通信任务等待标志位一旦置位就把数据打包通过无线模块发出。#define APP_TASK_SENSOR_PRIO 5 #define APP_TASK_COMM_PRIO 6 #define APP_TASK_SENSOR_STK_SIZE 512 #define APP_TASK_COMM_STK_SIZE 512 OS_EVENT *pStrDataFlag; static void App_TaskSensor(void *p_arg) { long quat[4]; short gyro[3], accel[3]; unsigned long sensor_timestamp; while (1) { OSTimeDlyHMSM(0, 0, 0, 20); // 与 FIFO 速率一致 dmp_read_fifo(gyro, accel, quat, sensor_timestamp); adc_read_finger_angle(adc_value); // 读取五路弯曲电压 packet_build(quat, adc_value); // 组装成待发送帧 OSFlagPost(pStrDataFlag, BIT_QUAT_READY, OS_FLAG_SET, err); } } static void App_TaskComm(void *p_arg) { OS_FLAGS flags_rdy; while (1) { flags_rdy OSFlagPend(pStrDataFlag, BIT_QUAT_READY, OS_FLAG_WAIT_SET_ANY, 0, err); if (flags_rdy BIT_QUAT_READY) { radio_send_frame(send_buf, send_len); } } } int main(void) { System_Periph_Init(); USART_Config(); ADC_Config(); MPU_DMP_Init(); pStrDataFlag OSFlagCreate(0, err); OSTaskCreateExt(App_TaskSensor, (void *)0, App_TaskSensorStk[APP_TASK_SENSOR_STK_SIZE - 1], APP_TASK_SENSOR_PRIO, APP_TASK_SENSOR_PRIO, App_TaskSensorStk, APP_TASK_SENSOR_STK_SIZE, (void *)0, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR); OSTaskCreateExt(App_TaskComm, (void *)0, App_TaskCommStk[APP_TASK_COMM_STK_SIZE - 1], APP_TASK_COMM_PRIO, APP_TASK_COMM_PRIO, App_TaskCommStk, APP_TASK_COMM_STK_SIZE, (void *)0, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR); OSStart(); return 0; }任务优先级里传感器任务比通信任务高一档也就是数字 5 小于 6。通信任务里尽量做“把 DMA 缓冲地址交给串口外设”这种非阻塞操作不要在 OSFlagPend 之后做 CRC 软计算这种耗时的循环。如果必须做把 CRC 放到传感器任务里先算好通信任务只负责搬运。这样 20ms 周期的抖动会更小手套快速握拳时也不至于丢包。4. 将传感器数据变成可执行的手势指令DMA、无线帧与上位机协议4.1 串口加 DMA 是发送任务不卡顿的前提很多人在毕业设计里直接用阻塞式USART_SendData发送数据任务一多就发现传感器采集周期被拉长。问题在于USART_SendData只把数据填入发送寄存器真正发送完成需要等一个字节移出移位寄存器。数据量大时主频再好也扛不住长串发送占用 CPU。这个项目里的数据帧大概每帧 30 字节左右如果按 50Hz 手势刷新率计算每秒 1500 字节波特率建议不低于 115200。发送端配置 USART DMA数据帧打包完成后只需要设置 DMA 传输长度并使能通道CPU 可以立刻回到传感器任务。void USART_DMA_SendFrame(uint8_t *buf, uint16_t len) { DMA_ClearFlag(DMA1_FLAG_TC4); DMA_Cmd(DMA1_Channel4, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, len); DMA_Cmd(DMA1_Channel4, ENABLE); }DMA1 的 Channel4 默认映射到 USART1_TX。配置时要注意DMA_PeripheralIncMode关闭、DMA_MemoryIncMode开启外设地址固定为(uint32_t)USART1-DR内存地址指向发送缓冲区。发送完成中断里可以加一个DMA_ClearFlag但不必在中断里做数据拷贝否则 DMA 的优势会打折扣。4.2 帧协议设计无线传输或是 USB 虚拟串口传输时接收端看到的是一串无边界的字节流。协议必须解决两个问题找帧头和校验数据完整性。推荐使用一个简单的固定长度帧帧格式如下。偏移长度/字节内容01帧头 0xAA11长度 0x1E24Q0~Q3 四元数每个 1 字节压缩值64roll/pitch/yaw每个 1 字节压缩值1010五路手指弯曲角度每路 2 字节原始 ADC202CRC16/MODBUS实际项目里四元数一般用 3 字节定点数传输这里为了简化表格用 1 字节。CRC 校验用 CRC16/MODBUS 比较常见算法短而高效适合 STM32 这类不带硬件 CRC 外设的芯片。static uint16_t crc16_modbus(uint8_t *p, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc ^ *p; for (int i 0; i 8; i) { if (crc 1) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }CRC 初值 0xFFFF多项式 0xA001这是 Modbus 最常用的参数组。接收端在解析时先找 0xAA然后读长度字段长度字段不匹配就直接重新同步匹配再等完整长度字节到达最后跑一遍同样 CRC。如果 CRC 不对优先考虑无线模块丢字节或串口波特率不准确而不是算法本身。4.3 Qt 上位机与手势分类上位机通常用 Qt 做嵌入式配套工具QSerialPort 类直接支持帧接收和超时处理。解析到手部姿态和手指弯曲角后手势识别可以做成阈值判断五指弯曲 ADC 值全部小于阈值同时 pitch 角接近 -90°判断为握拳所有手指伸直且 roll 角接近 0°判断为张开手掌。这种规则对康复训练场景足够稳定不需要机器学习。while (port-waitForReadyRead(50)) { QByteArray data port-readAll(); for (char c : data) { if (parse_state FIND_HEAD (uint8_t)c 0xAA) { parse_state GET_LEN; } else if (parse_state GET_LEN) { frame_len (uint8_t)c; parse_state GET_DATA; } } }看到这里你可能会问如果把waitForReadyRead放在 Qt 的 GUI 线程里界面会不会卡死答案是会。所以 Qt 上位机里应该用 QThread 接收串口然后通过信号槽把解析好的手势结果抛给主窗口。否则调试时拖拽窗口就丢帧容易误判为 STM32DMA 配置有问题。5. Keil 调试与 ST-Link 下载故障排查5.1 烧录前先确认 Keil 工程和芯片包拿到源码包后第一件事不是打开工程编译而是确认 Keil 版本和芯片支持包是否匹配。新装的 Keil5 如果没有安装 stm32 芯片包Device 下拉框里找不到 STM32F103C8编译下载时也会提示“No target created”之类的错误。正确做法是在 Pack Installer 里安装 Keil.STM32F1xx_DFP 系列芯片支持包安装完成后重新打开工程这时左侧项目树的 Device 一栏才会显示正确的芯片型号。如果工程是从别的电脑拷贝过来的还有可能遇到编译选项里 Preprocessor Symbols 不匹配的问题。查看 C/C 选项卡确认定义了USE_STDPERIPH_DRIVER和STM32F10X_MD否则stm32f10x.h会不知道当前是哪个具体型号外设时钟宏定义全部失效。5.2 Error: no STM32 target found! 的几种常见原因编译通过后点击烧录最常见的是Error: no STM32 target found! If your product embeds debug authentication。看到前半句先不要怀疑代码烧录选项优先按以下顺序检查硬件。首先看线路连接。SWD 调试只需要 SWDIO、SWCLK、GND、3.3V 四根线但一定要共地。如果 JTAG 转 SWD 的转接板没有接 GND目标芯片不会响应调试指令。其次是目标板供电ST-Link 的 3.3V 输出能力有限如果手套板上还有蓝牙模块最好单独给板子供电只把 ST-Link 的 SWDIO 和 SWCLK 接上。最后要检查复位引脚是否被拉低。板上有复位按键电路时如果复位电容漏电导致 NRST 一直被拉低CPU 处于复位状态调试器自然找不到目标。在 Keil 配置里Utilities 页的 Setting 中把Connect under Reset选项打开同时把 Reset 方式改成 Hardware Reset往往能解决大多数“目标板程序跑飞导致 SWD 被占用”的情况。如果还是连不上用 ST-Link Utility 的Connect under reset模式擦除整个 Flash再把 Keil 烧录选项复位。5.3 STM32 Virtual COM Port 叹号处理设备管理器里出现STM32 Virtual COM Port带黄色感叹号这个问题和固件无关属于 ST-Link 板载虚拟串口驱动没有被 Windows 正确识别。先把 ST-Link 拔下卸载设备树里带叹号的项然后重新安装 ST 官方虚拟串口驱动。装完驱动后需要重新插拔一次 ST-Link让 Windows 重新枚举设备。驱动装好之后验证数据传输是否正常可以在 Keil 里打开逻辑分析仪实时观察 MAILBOX 或全局变量中的四元数数据。更直接的办法是使用 ST-Link Utility 烧写一段只发固定数据的测试程序把串口助手的波特率设置为 115200如果 PC 端能收到周期稳定的字节流说明 STM32 串口、USART DMA 和 ST-Link VCP 这条链路已经打通问题范围可以缩小到协议解析层。本文还有配套的精品资源点击获取