简介:基于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~30k | PA0 | ADC1_IN0 | 定时器触发单次转换 |
| 食指弯曲 | 弯曲传感电阻 10k~30k | PA1 | ADC1_IN1 | 定时器触发单次转换 |
| 中指/无名/小指 | 同类型传感器 | PA2~PA4 | ADC1_IN2~IN4 | 定时器触发单次转换 |
| 手背姿态 | MPU6050 | PB6/PB7 | I2C1 | DMP 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 | 时钟树配置 | 外部晶振倍频到 72MHz |
| stm32f10x_tim.c | 定时器配置 | 产生 ADC 触发信号和 PWM 输出 |
| stm32f10x_flash.c | Flash 等待周期配置 | 保证 72MHz 下 Flash 访问稳定 |
| stm32f10x_adc.c | ADC 采集 | 读取五路弯曲传感器电压 |
这套结构也正好回答了常见的嵌入式面试题:任务间通信用过哪些机制?这里用的就是事件标志组,而不是全局变量。全局变量在简单裸机程序里没问题,但在 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 被配成 72MHz,ADC 还需要再分频,通常取 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 虚拟串口传输时,接收端看到的是一串无边界的字节流。协议必须解决两个问题:找帧头和校验数据完整性。推荐使用一个简单的固定长度帧,帧格式如下。
| 偏移 | 长度/字节 | 内容 |
|---|---|---|
| 0 | 1 | 帧头 0xAA |
| 1 | 1 | 长度 0x1E |
| 2 | 4 | Q0~Q3 四元数,每个 1 字节压缩值 |
| 6 | 4 | roll/pitch/yaw,每个 1 字节压缩值 |
| 10 | 10 | 五路手指弯曲角度,每路 2 字节原始 ADC |
| 20 | 2 | CRC16/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 这条链路已经打通,问题范围可以缩小到协议解析层。
本文还有配套的精品资源,点击获取