简介:基于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 ECHO | TIM2_CH2 | PA1,上拉输入 |
| PWM 输出 | 振动电机 | TIM3_CH1 | PA6,经 MOS 管驱动 |
| USART | 语音合成模块 | USART1 | PA9/PA10,加 10k 上拉 |
| ADC | 电池电压 | ADC1_IN0 | PA0,两个电阻分压 |
| GPIO 输出 | 超声波 TRIG | 任意 GPIO | PA0 推挽输出 |
| GPIO 输出 | LED 警示灯 | 任意 GPIO | PC13,低电平点亮 |
这里有一个容易忽略的点:振动电机和 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_CH2(PA1),定时器时基设为 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 | 大于 120cm | 0% | 常灭 | 无 |
| CAUTION | 60 到 120cm | 30% | 慢闪 2Hz | 无 |
| WARN | 30 到 60cm | 60% | 快闪 8Hz | “前方注意” |
| STOP | 小于 30cm | 100% | 常亮 | “请停止” |
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 属于 APB1,STOP 模式下定时器时钟会停,所以通常用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 开始,记录每次状态切换的实际测量值,然后据此调整阈值和振动强度,直到通过标准测试。
本文还有配套的精品资源,点击获取