
简介基于STM32的智能导盲拐杖完整开发包面向嵌入式、物联网及电子类专业学生可用于毕业设计、课程设计或项目初期演示。包内含全新源码与详细设计文档代码经测试可直接运行适合从入门到进阶的开发者学习参考。资源共403个文件以C源文件与头文件为主45个c、48个h配合Keil工程文件uvprojx/uvoptx、可执行hex文件、map映射表及md设计文档构成一套完整的嵌入式工程。压缩包仅8.14MB结构清晰便于快速定位源码、配置文件与说明材料。目前已有83人在线学习。除了基础功能实现包中还提供MPU6050姿态融合、STM32定时器等模块代码以及GSM、OLED等外设驱动相关文件可直接修改扩展。配套文档含设计思路、硬件连接与调试要点帮助理解整体架构并对接实际需求是嵌入式智能硬件项目不错的参照范本。1. 智能导盲拐杖STM32 端侧避障的方案为什么值得抄作业智能导盲拐杖这类项目看着门槛低实际做起来很容易卡在三个地方超声波回波时间测不准、不同距离档位的触发逻辑在真实行走中过于灵敏、电池电压掉到 3.6V 以后振动电机带不动。这些都不是原理问题而是外设用法和参数选型问题。本篇文章以最常见的 STM32F103C8T6 方案为主线把传感器选型、定时器捕获、状态机设计、调参方法和最终如何整理一份能交付的设计文档串起来。文中代码可以直接对应到“最新开发”这类源码包里常见的 driver 和 app 目录结构适合想改参数、换传感器或准备基于这套结构做自己项目的开发者。2. 硬件选型与系统供电传感器和驱动框架要一起定做导盲拐杖之前先把硬件引脚表画出来能省掉后面一大半调试时间。很多人上手先写驱动等超声波、语音、振动电机全部堆上去才发现定时器冲突或者 GPIO 不够用这时候再换板子很被动。我一般先按模块列需求再倒推用哪颗芯片、每个外设占哪些引脚最后才画原理图。2.1 主控与外设资源分配STM32F103C8T6 够不够用最常见的选择是 STM32F103C8T6不是因为性能强而是因为它的外设数量刚好覆盖这个场景且资料最全。72 MHz 主频跑状态机绰绰有余64 KB Flash 存代码和语音资源也够紧张。通讯外设方面超声波用定时器输入捕获语音模块走 USART振动电机走 PWM电池电压走 ADC这样串口还有其他空闲可以接调试。外设模块STM32 资源接口建议定时器输入捕获HC-SR04 ECHOTIM2_CH2PA1上拉输入PWM 输出振动电机TIM3_CH1PA6经 MOS 管驱动USART语音合成模块USART1PA9/PA10加 10k 上拉ADC电池电压ADC1_IN0PA0两个电阻分压GPIO 输出超声波 TRIG任意 GPIOPA0 推挽输出GPIO 输出LED 警示灯任意 GPIOPC13低电平点亮这里有一个容易忽略的点振动电机和 LED 都不能由 STM32 GPIO 直接驱动。振动电机启动瞬间电流可能到几百毫安GPIO 的灌电流上限只有 25mA 左右我一般会在输出端加一个 NPN 三极管或小功率 MOS 管PWM 接到基极或栅极把电机地线单独接到电池负极。这样做的另一个好处是避免电机启停瞬间拉低 3.3V 电源导致单片机复位。2.2 测距传感器选型超声波、红外、激光各自适合什么场景导盲拐杖对前端障碍物主要关心的是前方 2 米内的物体有没有、在哪个距离范围。红外接近传感器成本低但对深色物体和户外阳光很敏感黑色衣服几乎测不到。激光测距VL53L0X精度好I2C 接口但量程只有 2 米左右价格高在拐杖这种可能磕碰的场景里并不划算。超声波最适合这个需求HC-SR04 的探测范围在 2cm 到 400cm精度约 3mm对颜色和光照不敏感。常见误用是直接用 GPIO 翻转去测量 ECHO 高电平的时间比如在主循环里 while 等待上升沿再等待下降沿。这种做法在单独调试时没问题一旦加入语音播报和电机 PWM主循环被阻塞距离更新就会变得很慢甚至漏掉回波。正确的做法是把 ECHO 引到定时器输入捕获通道用上升沿和下降沿各触发一次中断在中断里读出计数器值。HC-SR04 本身需要一个 5V 供电而 STM32 最小系统板是 3.3V所以电源部分不能偷懒。TRIG 和 ECHO 引脚虽然看起来能直接连 3.3V 逻辑但 ECHO 在回波时输出的是 5V 高电平长期直接进 PA1 会有风险我通常在 ECHO 到 PA1 之间串一个 1k 电阻或者加一级分压把 5V 降到 3.3V 再进单片机。这个细节很多原理图都不画做出来的板子也未必立刻出问题但工程师该按可靠方式处理。2.3 电源和充电管理3.7V 锂电池到 5V/3.3V 的两级转换整套系统用一节 18650 锂电池或聚合物电池供电标称 3.7V充满 4.2V。单片机需要 3.3V超声波模块需要 5V所以电源拓扑通常是电池电压直接进 5V 升压芯片再从 5V 经过一个 LDO 得到 3.3V。常见做法是用 MT3608 升压到 5.2V 左右再接 AMS1117-3.3 给 MCU。充电管理用 TP4056 模块带过充过放保护。ADC 采集电池电压时需要用两个电阻把电池电压分压到 3.3V 以下比如用 100k 和 10k分压比 11:1这样 4.2V 电池电压采样出来约 0.38V换算回来再乘以校准系数。注意分压电阻的功耗很小但不要直接用 10k 加 10k因为 ADC 输入阻抗不够时会影响采样精度建议后端加一个 0.1uF 电容滤波。3. 距离测量与状态机实现超声波测距核心代码怎么写硬件事项确定之后软件部分的核心就两件事从超声波模块拿到准确的距离值再按距离档位驱动振动和语音。这里的关键是保证距离测量不被业务代码阻塞同时每个距离档位切换都有明确的时序。3.1 用定时器输入捕获测超声波回波我习惯把 ECHO 接到 TIM2_CH2PA1定时器时基设为 72 分频这样计数器每 1us 加一次。TRIG 触发以后只需要等捕获中断里记录上升沿和下降沿两次计数器差值就是整个回波高电平的持续时间。void TIM2_Capture_Init(void) { TIM_TimeBaseInitTypeDef tim_base; TIM_ICInitTypeDef tim_ic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); tim_base.TIM_Period 0xFFFF; tim_base.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz即 1us 计数一次 tim_base.TIM_ClockDivision 0; tim_base.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tim_base); tim_ic.TIM_Channel TIM_Channel_2; tim_ic.TIM_ICPolarity TIM_ICPolarity_Rising; // 先捕获上升沿 tim_ic.TIM_ICSelection TIM_ICSelection_DirectTI; tim_ic.TIM_ICPrescaler TIM_ICPSC_DIV1; tim_ic.TIM_ICFilter 0x0F; // 滤波避免毛刺触发 TIM_ICInit(TIM2, tim_ic); TIM_ClearFlag(TIM2, TIM_FLAG_CC2); TIM_ITConfig(TIM2, TIM_IT_CC2, ENABLE); TIM_Cmd(TIM2, ENABLE); }说明一下这段代码的逻辑预分频 72-1 后TIM2 的计数频率是 1MHz计数器值直接代表微秒数。TIM_ICFilter 0x0F是打开输入滤波能过滤掉 ECHO 线上的短毛刺尤其是电机启动瞬间带来的干扰。捕获极性先配成上升沿。volatile uint32_t echo_start 0; volatile uint32_t echo_ticks 0; volatile uint8_t echo_ready 0; void TIM2_IRQHandler(void) { TIM_ICInitTypeDef tim_ic; if (TIM_GetITStatus(TIM2, TIM_IT_CC2) RESET) return; TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); if (echo_ready 0) { // 上升沿记录当前计数并把捕获极性改为下降沿 echo_start TIM_GetCapture2(TIM2); tim_ic.TIM_Channel TIM_Channel_2; tim_ic.TIM_ICPolarity TIM_ICPolarity_Falling; tim_ic.TIM_ICSelection TIM_ICSelection_DirectTI; tim_ic.TIM_ICPrescaler TIM_ICPSC_DIV1; tim_ic.TIM_ICFilter 0x0F; TIM_ICInit(TIM2, tim_ic); } else { // 下降沿两次计数差就是高电平时间 echo_ticks TIM_GetCapture2(TIM2) - echo_start; echo_ready 1; tim_ic.TIM_Channel TIM_Channel_2; tim_ic.TIM_ICPolarity TIM_ICPolarity_Rising; tim_ic.TIM_ICSelection TIM_ICSelection_DirectTI; tim_ic.TIM_ICPrescaler TIM_ICPSC_DIV1; tim_ic.TIM_ICFilter 0x0F; TIM_ICInit(TIM2, tim_ic); } }代码里用echo_ready而不是在下降沿立刻计算距离是为了避免在中断里做浮点运算。中断里只保存原始计数差值主循环里再把这个值换算成厘米数。距离计算公式是距离(cm) 高电平时间(us) * 340(m/s) / 2 / 10000简化以后约等于echo_ticks * 0.017。float Ultrasonic_GetDistanceCM(void) { float dist; if (echo_ready 0) return -1.0f; dist (float)echo_ticks * 0.017f; echo_ready 0; return dist; }这里把echo_ready在主循环读取时清零保证每次触发只对应一次结果避免读到旧数据。触发超声波用另一个 GPIO输出一个 10us 高电平脉冲触发完成后立刻返回不要用延时等待这样主循环可以继续处理语音和电机。3.2 业务状态机距离档位驱动振动与语音拿到距离值以后直接 if-else 当然也能写但档位一多就会乱。我会用一个枚举状态机维护当前状态每次超声波数据更新时做状态迁移每次迁移后只执行一次对应的输出动作。typedef enum { STATE_WALK 0, STATE_CAUTION, STATE_WARN, STATE_STOP } GuideState; GuideState guide_state STATE_WALK; float g_last_dist_cm 0.0f; void Guide_Update(float dist_cm) { GuideState next_state; if (dist_cm 0.0f || dist_cm 400.0f) return; g_last_dist_cm dist_cm; if (dist_cm 30.0f) next_state STATE_STOP; else if (dist_cm 60.0f) next_state STATE_WARN; else if (dist_cm 120.0f) next_state STATE_CAUTION; else next_state STATE_WALK; if (next_state ! guide_state) { guide_state next_state; Guide_ApplyOutput(guide_state); } }状态迁移的核心是“只在状态改变时更新输出”否则每次超声波循环都重复触发语音播报拐杖会变成一个不断喊叫的设备。Guide_ApplyOutput里做三件事设置振动电机的 PWM 占空比、决定 LED 是否闪烁、按需向语音模块发送提示指令。语音播报是慢速外设串口发送一帧数据也要几十毫秒所以语音指令通常在进入新状态时只发一次。void Guide_ApplyOutput(GuideState state) { switch (state) { case STATE_WALK: TIM_SetCompare1(TIM3, 0); LED_OFF(); Voice_PlayStop(); break; case STATE_CAUTION: TIM_SetCompare1(TIM3, PWM_CAUTION); LED_Blink(2); break; case STATE_WARN: TIM_SetCompare1(TIM3, PWM_WARN); LED_Blink(8); Voice_PlayWarn(); break; case STATE_STOP: TIM_SetCompare1(TIM3, PWM_STOP); LED_ON(); Voice_PlayStop(); break; } }语音模块如果用的是串口 MP3 模块比如常见的那几款通常发送 6 到 10 字节指令就能播放指定音频。我习惯把指令封装成Voice_PlayWarn()这样的函数而不是在主循环里直接写串口发送这样后续换语音方案只需要改一层驱动。振动电机的 PWM 频率设在 1kHz 到 2kHz 之间太低的频率会有明显顿挫感太高可能听不见声音但损耗增加。3.3 按键交互与机制说明拐杖上还需要一个电源键和一个模式键。模式键的作用是切换“行走模式”和“静止探测模式”。静止模式用于等红绿灯或站在某个位置时只用超声波每秒钟探测一次振动频率降低省电。按键处理用外部中断或主循环轮询都行但要注意防抖我一般用状态机加 20ms 延时确认。4. 参数调优与坑点排查滤波、阈值、低功耗代码能跑通只算完成一半。超声波测距在实验室很干净拿到走廊里就会遇到电梯金属门、墙角回声、路过行人衣服反射不稳定等问题。这一章讲几个在现场调参最常用的手段。4.1 测距数据滤波与离群值处理HC-SR04 偶尔会跳出一个明显不对的值比如 1 米处突然测到 300cm。可以用滑动平均值把噪声压下去但在连续行走场景里滑窗过大会让距离变化变迟钝。我一般用窗口大小为 5 的滑动平均既能把单次尖刺削掉又不会让“靠近障碍物”的速度感知延迟太多。#define FILTER_SIZE 5 float filter_buf[FILTER_SIZE]; uint8_t filter_index 0; float filter_sum 0.0f; float Dist_Filter(float new_value) { // 先把旧值移出窗口再放入新值 filter_sum - filter_buf[filter_index]; filter_sum new_value; filter_buf[filter_index] new_value; filter_index (filter_index 1) % FILTER_SIZE; return filter_sum / FILTER_SIZE; }这个滤波器的特点是计算成本极低不需要排序也不需要动态内存。调用位置在Ultrasonic_GetDistanceCM()之后、Guide_Update()之前。如果你发现偶尔还是有个别跳变影响状态机可以先做一步离群值判断如果新值和上次值的差大于 40cm直接丢掉本次采样保持上次结果。但要注意真的撞到障碍物时距离变化也可能超过 40cm所以这个阈值不要设得太小。4.2 提醒阈值和 PWM 强度的映射状态机里的三个阈值30cm、60cm、120cm不是拍脑袋定的要结合拐杖长度和行走速度来看。拐杖触地到人脚的距离大约 90cm前方障碍物进入 120cm 时提示人还有约 1 到 2 秒反应时间60cm 以下是必须明显减速的距离30cm 以下基本要停下了。如果你做的是给儿童使用的小尺寸拐杖阈值应该整体缩小。状态距离阈值振动 PWM 占空比LED 行为语音提示WALK大于 120cm0%常灭无CAUTION60 到 120cm30%慢闪 2Hz无WARN30 到 60cm60%快闪 8Hz“前方注意”STOP小于 30cm100%常亮“请停止”PWM 占空比对应的是 MOS 管的导通时间。30% 占空比能让电机产生明显的震动感但又不吵100% 会让拐杖整体抖动这是故意为之——紧急状态下稍微牺牲握持体验换取高感知度。实际占空比要根据电机型号调整有的微型电机 20% 就已经很强烈有的刷式电机 50% 才刚能感觉到。4.3 用串口输出观测测距波形方便现场调我调试时一定会留一个串口调试口。在 10ms 定时器中断里把距离值打包通过 USART1 发出去格式用最简单的文本比如dist:83.2cm然后用串口助手看波形。这样能快速判断滤波前后的差异、超声波安装角度是否偏了、走到不同地面材质时会不会误触发。void Debug_SendDistance(float dist_cm) { char buf[32]; sprintf(buf, dist:%.1fcm\r\n, dist_cm); USART_SendString(USART1, buf); }还可以在Guide_Update里把状态迁移也打印出来比如state:36。现场测试时推着一辆购物车慢慢靠近观察状态切换是否和人的预期一致。如果 CAUTION 和 WARN 之间来回跳说明滤波不够或阈值间隔太小这时候我会把 WARN 阈值改到 50cm增加滞回区间而不是继续扩大滤波窗口。4.4 低功耗优化睡眠模式、传感器供电开关拐杖用电的消耗大户是超声波模块、语音模块和振动电机。语音模块待机电流有的到 20mA 以上如果常供电不到一天电池就会耗尽。常见的做法是用一个 MOS 管或负载开关控制语音模块电源只在需要播报前 200ms 上电播报完再断电。STM32 在行走模式下不要一直满频跑可以把主频降到 24MHz 或 36MHz。更极致的做法是在两次超声波采样之间的 30ms 内进入睡眠模式用定时器唤醒。超声波采样频率不需要太高每秒 10 次足够采样周期 100ms中间有大量时间可以睡。关键点是把超声波模块的 TRIG 触发、唤醒定时器、捕获中断三者在低功耗模式下合理配合如果用的是STOP模式还需要检查 TIM2 是否能在 STOP 模式下继续工作——F103 里 TIM2 属于 APB1STOP 模式下定时器时钟会停所以通常用SLEEP模式或仅降频处理不要盲目追求 STOP 模式。5. 怎么落地成一版“最新开发”的可交付源码源码包的名字里写着“全新源码详细设计文档”说明读者拿到的应该是一个能直接编译、能看懂结构、能照着改出自己版本的东西。工程化整理比写功能代码更容易被忽视但恰恰是最能看出是否认真的部分。5.1 driver、app、board 三层目录怎么切我不会把全部代码堆在 main.c 里。建议按这样的层次组织board放引脚定义和时钟初始化driver放超声波、语音、PWM 这种芯片级驱动app放状态机和用户逻辑。main.c 只做初始化和启动主循环。app/ guide_fsm.c # 状态机与逻辑 main.c # 主循环 board/ pin_config.c # GPIO、串口、定时器初始化 driver/ ultrasonic.c # HC-SR04 测量与滤波 voice.c # 语音串口指令 motor.c # 振动电机 PWM doc/ design.md # 设计文档这样的好处是换一块 STM32 型号时只需要改 board 层driver 层尽量不涉及板级引脚。比如从 F103C8T6 换到 F103RCT6只需调整 pin_config.c 和启动文件业务逻辑不用动。5.2 设计文档里值得记录的三个测量报告设计文档别写成操作手册我一般会把三类实测数据固定放进去一是超声波在不同材质、不同角度下的误差表二是整机在不同状态下的电流数据三是穿戴者测试的主观反馈表。实测电流数据最有说服力比如静止状态 15mA、提示状态 80mA、紧急振动 180mA这样用户才能估算续航并决定要不要增加电池容量。5.3 把一次避障响应的时序做成验收用例最后一个建议是把“从触发超声波到完成一次有效反馈”的延迟量化出来。测试方法很简单用一个 GPIO 置高模拟障碍物观察超声波触发点到振动电机实际转动的时间差。验收标准通常控制在 150ms 以内超过这个值人在快速行走时会感觉反应迟缓。更严谨的做法是在设计文档里画出一次完整的状态时序从 120cm 进入 CAUTION 开始记录每次状态切换的实际测量值然后据此调整阈值和振动强度直到通过标准测试。本文还有配套的精品资源点击获取