简介:基于STM32的宠物陪伴机器人完整工程,专为嵌入式、物联网、人工智能及相关专业的在校学生、教师和企业开发者设计,既可作为课程设计、毕业设计,也适合项目演示和课堂大作业,帮助快速理解机器人控制与物联网通信的整体实现流程。压缩包共375个文件、12.32MB,内容以C语言源码为主(75个c文件、62个h文件),并包含Keil工程配置文件、编译生成的axf/hex/map固件、14张PNG示意图、4个TXT说明文档以及批处理辅助脚本,目录划分清晰,可直接用Keil打开编译查看。目前已有309人学习下载,在同类课设资源中具有一定参考价值。项目代码经过功能验证,稳定可靠,内部集成STM32主控逻辑、ESP8266通信模块、FatFs文件系统、LCD显示、定时器驱动等关键代码,并提供编译脚本与固件输出,既适合入门到进阶的嵌入式实验,也方便在此基础上二次开发,最终扩展为功能完整的宠物陪伴机器人。
1. 为什么说课程设计项目的好与坏,不在源码而在工程思路
打开一个名为“基于STM32的宠物陪伴机器人源码及完整资料(课程设计).zip”的包,第一眼看到的通常不是清晰的工程逻辑,而是散落在各处的 SystemInit、main.c、stm32f1xx_hal_uart.c,以及一大堆注释风格各异的代码。真正拉开成绩差距的往往不是那几行 PWM 输出,而是硬件引脚表、电源分区、状态机设计和联调顺序。这里顺着 STM32 宠物陪伴机器人的完整落地路径展开:先从硬件选型确定系统架构,再把舵机、超声波、语音模块的驱动源码逐个写透,然后剖析源码工程结构并给出联调排错方法,最后落到验收和答辩的加分细节。适合正在复现课程设计、做嵌入式毕业设计或第一次接触 STM32 工程源码的读者。
2. 先射箭再画靶:宠物陪伴机器人的硬件选型与 STM32 系统架构
选型决定后续代码量和调试难度。不少课程设计把时间浪费在“板子能点亮但项目做不完”上,根源就是一开始没把运动方案、引脚占用和电源拓扑想清楚。
2.1 用舵机还是直流电机:先把运动方案定下来
宠物陪伴机器人的核心动作是头部转动、尾巴摆动、爪子抬升和投喂,不需要跑得快,反而要求每个动作可解释、可复现。课程设计里最常见的运动方案是用 2 到 3 个舵机搭出关节,而不是做一套轮式底盘。SG90 便宜、轻、响应快,适合头部和尾巴这类轻负载;MG996R 扭矩达到 11kg·cm,适合承担爪子落料的力矩。真正要留意的是两者峰值电流差异,MG996R 启动瞬时电流能到 2.5A,这在后续电源分区里会再次提到。
| 舵机型号 | 堵转扭矩 | 工作电压 | 典型脉宽范围 | 峰值电流 |
|---|---|---|---|---|
| SG90 | 1.8kg·cm | 4.8V~6.0V | 500~2400us | 约 250mA |
| MG996R | 11kg·cm | 4.8V~6.6V | 500~2500us | 约 2.5A |
如果题目没有强制要求“移动”,我的建议是不要上直流电机。一旦引入驱动芯片和编码器,调试面就从“一个 GPIO 输出 PWM”扩展到电机 PID、死区补偿、PWM 频率选择,对课程设计的时间非常不友好。L298N 在 1A 以上负载时导通压降接近 2V,很多标称 6V 的电机实际只有不到 4.5V,表现就是板子正常但轮子无力,排查比舵机麻烦得多。如果你想对比工业伺服方案,那属于 stm32 控制伺服电机 485 的领域,通信协议和 PWM 驱动完全不同,这里只提一句不展开。市面上不少鱼缸喂食器用的就是这套“舵机加落料斗”的硬件逻辑,说明这个方案在小型自动化设备里确实有成熟复用价值。
2.2 STM32F103C8T6 的资源分配与引脚防冲突
主控方面,F103C8T6 是课程设计里的默认选项:Cortex-M3 内核、72MHz 主频、64KB Flash、20KB RAM。三个 PWM 通道、一路串口、一路超声波加上若干 GPIO,总占用不超过一半资源。选更大容量芯片不会带来评审加分,反而会让时钟树、启动文件这些环节引入更多变量。APM32F103C8T6 经常被当作替代品,引脚和寄存器兼容,程序可以直接烧录,但 Flash 容量必须一致,遇到诡异现象先跑一个 GPIO 点灯程序验证时钟是否正常。
引脚分配的关键是外设复用冲突。舵机 PWM 放在 TIM2_CH1/CH2/CH3,对应 PA0/PA1/PA2,这三个引脚不占用 USART1,是最顺手的组合。语音模块和调试日志共用 USART1,位置在 PA9/PA10。超声 Trig 放 PB0、Echo 放 PB1,蜂鸣器放 PB5,板上 LED 用 PC13。
| 外设对象 | 引脚 | 复用功能 | 电气方向 |
|---|---|---|---|
| 头部舵机 | PA0 | TIM2_CH1 | PWM 输出 |
| 尾巴舵机 | PA1 | TIM2_CH2 | PWM 输出 |
| 爪子舵机 | PA2 | TIM2_CH3 | PWM 输出 |
| 超声 Trig | PB0 | 普通 GPIO | 高速推挽输出 |
| 超声 Echo | PB1 | 普通 GPIO | 浮空输入 |
| USART1 TX | PA9 | 复用推挽 | 到语音模块 RX |
| USART1 RX | PA10 | 复用输入 | 到语音模块 TX |
| 无源蜂鸣器 | PB5 | 普通 GPIO | 推挽输出 |
| 板载 LED | PC13 | 普通 GPIO | 开漏输出 |
void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0; // Trig GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_1; // Echo GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; // 不要上拉 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_13; // 板载LED GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); }PC13 开漏输出是因为板载 LED 通常接在 3.3V 与 PC13 之间,拉低点亮。Echo 引脚不开上拉,HC-SR04 空闲时内部把 Echo 拉低,回波到达时由模块驱动拉高,外部上拉电阻会让高低电平判断点偏移,近距离测距值系统性偏大。这是排查超声误差时最容易忽略的点。
2.3 复位、电源和时钟:系统跑不稳,先别怀疑代码
硬件问题排在优先级第一位。最常见“舵机一转,芯片复位”的原因是舵机瞬时大电流把主板 3.3V 拉低,不是程序 Bug。常见做法是电池组 6V 或 7.4V 先供舵机,再从同一电池组引出经过 AMS1117-3.3 给 MCU 供电,两路共地。在舵机供电端并联 470uF 电解电容吸收启动尖峰,这个问题基本消失。
时钟配置从 8MHz 外部晶振 PLL 倍频到 72MHz,HAL 库标准写法如下。
RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); }如果你把 8MHz 晶振换成 16MHz,PLLMUL 要改成 MUL4,并同步检查 APB1 分频系数,否则串口波特率和 HAL_Delay 都会跑偏。用 STM32CubeMX 修改单片机型号时,外设和引脚会自动重建,但启动文件和链接脚本里的 Flash 大小不会自动匹配:F103C8 是 64KB,ROM Size 写 0x10000,填成 0x20000 后程序能烧进去,运行时却可能出现奇怪的数据覆盖。
注意:如果出现“舵机一转就复位”,优先检查电源分区与共地,而不是去改程序里的 PLL 配置。
3. 把动作和感知落地:STM32 外设驱动的源码与参数标定
这章处理三个核心模块:舵机 PWM、超声波测距和语音交互。每个模块都给出可直接抄进工程的代码,以及调整参数时容易踩的坑。
3.1 定时器 PWM 驱动舵机:从频率、脉宽到角度映射
舵机内部是直流电机、减速齿轮组和反馈电位器,控制电路比较输入脉宽对应的目标角度与当前电位器反馈,驱动电机转到目标位置。只要周期在 20ms 左右、高电平时间在 0.5ms 到 2.5ms 之间,舵机就会输出期望角度。高电平时间的精度决定角度精度,因此必须由硬件定时器输出 PWM,而不是用延时函数模拟。
| 参数 | 值 | 说明 |
|---|---|---|
| TIM2 计数频率 | 1MHz | 72MHz / 72 预分频,每 us 计 1 次 |
| ARR | 19999 | 计 20000 次,周期 20ms 对应 50Hz |
| CCR 起始值 | 1500 | 1.5ms 脉宽,舵机处于约 90 度 |
| 角度映射 | pulse = 500 + angle/180*2000 | 线性近似,需实测微调 |
void Servo_Init(TIM_HandleTypeDef *htim, uint32_t Channel) { TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 1500; // 上电先回中间位置,防抖动 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim, &sConfigOC, Channel); HAL_TIM_PWM_Start(htim, Channel); } void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t Channel, float angle) { uint16_t pulse = (uint16_t)(500.0f + angle / 180.0f * 2000.0f); if (pulse > 2450) pulse = 2450; // 极限位留余量,避免堵转 if (pulse < 550) pulse = 550; __HAL_TIM_SET_COMPARE(htim, Channel, pulse); }代码里把脉宽限制在 550 到 2450us,而不是激进地使用 500 到 2500。多数舵机的实际机械行程比标称角度略小,直接顶到极端会让齿轮组堵转发热,课程设计连续演示十分钟就可能损坏舵机。正确做法是上电后先发 1500,观察舵机是否在几何中位;然后以 50us 步进递增,记录机械限位点的脉宽值,把映射公式里的上下限改成实测值。串口接入角度命令后,这个标定过程可以被记录成日志,答辩时能直接展示。
如果舵机默认 50Hz 周期下的左右速度太慢,不要通过改 ARR 提高频率,标准舵机的内部控制电路是按 50Hz 周期设计的,频率提高到 100Hz 以上会发热并缩短寿命。想要更快动作,就把角度变化速率写死在状态机里,每次任务只推进一小步。
3.2 超声波避障:Echo 高电平时长到距离的换算细节
HC-SR04 的工作过程是:Trig 拉高 10us 以上,模块发送 8 个 40kHz 脉冲,然后 Echo 输出高电平,高电平持续时长等于声波往返时间。距离 cm 等于微秒数除以 58,或者说乘以 0.017。测距的关键在于时间戳分辨率,HAL_GetTick 只有毫秒级,1ms 误差对应 17cm,完全不可用,所以要借用内核 DWT 周期计数器。
void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } float HC_SR04_GetDistance(void) { uint32_t start, end; uint32_t timeout = 30000; HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); DWT_Delay_Us(12); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_RESET) { if (--timeout == 0) return -1.0f; // 无回波,返回无效值 } start = DWT->CYCCNT; timeout = 30000; while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_SET) { if (--timeout == 0) return -1.0f; } end = DWT->CYCCNT; return (float)(end - start) / 72.0f * 0.017f; // 核心频率72MHz }DWT->CYCCNT 是 Cortex-M3 内核自带的周期计数器,每个 CPU 周期加一,不占用定时器外设,对单路超声测距最轻量。SM72MHz 下把周期差除以 72 就得到微秒。返回 -1 表示测距失败,主逻辑必须先判断这个异常值,否则 -1 会被当成“非常近的障碍”触发误避障。尾迹时间戳的读取放在 while 循环里,连续测三次取中间值做中值滤波,能滤掉多径反射造成的零散尖峰。
超声波探头正对软质物体时回波衰减明显,容易测出跳变距离。演示时尽量让障碍物是纸箱或墙面,减少软材料的干扰。
3.3 交互反馈:语音模块串口指令、蜂鸣器与状态灯
宠物陪伴机器人需要给人反馈,最简单方案是串口接语音模块。常见模块如 JQ8900 通过 UART 指令触发预存储的 mp3 文件,波特率一般是 9600,但不同批次可能默认 38400,集成前先单独测试模块返回值,再写入工程。
void Voice_Play(uint8_t index) { uint8_t frame[4] = {0xAA, 0x02, index, 0xA1}; HAL_UART_Transmit(&huart1, frame, 4, 100); } void Buzzer_Beep(void) { for (uint8_t i = 0; i < 10; i++) { HAL_GPIO_WritePin(BUZZ_PORT, BUZZ_PIN, GPIO_PIN_SET); DWT_Delay_Us(250); HAL_GPIO_WritePin(BUZZ_PORT, BUZZ_PIN, GPIO_PIN_RESET); DWT_Delay_Us(250); } }语音指令帧的校验字节计算方式因模块型号而异,有的直接发 0xAA 0x02 index 0xA1 就能识别,有的把校验算成其它值。买模块时向卖家要指令表,并把命令封装统一放在 voice.c,以后换模块只改这一个文件。脉冲蜂鸣器这里用 IO 翻转产生 2kHz 方波,只适合播“嘀嘀”提示音;如果要播旋律,换用定时器 PWM 输出频率可变的信号,占空比固定 50% 即可。
PC13 板载 LED 用来做状态指示,比串口日志更直观:开机闪两下表示自检通过,常亮表示进入避障等待,快速闪烁表示超声故障。这样演示现场不用开串口也能看出程序走到了哪个状态。
4. 源码工程结构与联调:让课程设计代码按你预期跑起来
从 zip 解压到全部外设动起来,中间隔着源码阅读和排错两个环节。这章按“看懂目录—理顺调度—串口联调”的顺序展开。
4.1 从 zip 到可烧录工程:目录识别、芯片包与工程修复
解压后先不要急着双击 uvprojx,先看目录结构,判断工程用的是标准库还是 HAL 库。HAL 库工程里 Drivers 文件夹内部有清晰的 STM32F1xx_HAL_Driver 子目录;标准库工程则是一堆 stm32f10x_xxx.c 直接摊在用户目录下。
| 路径 | 作用 | 课程设计主要改动点 |
|---|---|---|
| Core/Src | 系统时钟、main、中断回调 | main.c 写主逻辑 |
| Core/Inc | 头文件目录 | 新增模块头文件 |
| Drivers/STM32F1xx_HAL_Driver | HAL 库实现 | 不修改 |
| Hardware 或 App | 板级驱动:servo.c、ultrasonic.c | 重点阅读 |
| MDK-ARM | Keil 工程文件 | 双击打开 |
打开工程后如果报找不到 stm32f1xx.h,多半是 STM32 芯片包没装。在 Keil 的 Pack Installer 里搜索 STM32F1xx_DFP 并安装,头文件和启动文件会自动补全。keil5 兼容 c51 和 stm32 安装时,很多人把两个套件装到同一目录,结果 ARM 编译器无法切换,建议分目录安装,用 License Management 分别选择 C51 和 ARM 授权。用 STM32 ST-LINK Utility 连接下载时,Flash Download 页面会列出算法,中容量 F103C8 不要勾成高容量 512KB 算法,地址错位会产生难以排查的 HardFault。
4.2 主循环还是 FreeRTOS:用状态机把多个任务串起来
宠物陪伴机器人不需要完整 RTOS,裸机状态机完全够用,但要把所有动作组织成有限状态,而不是堆一长串 if 在 while 里。
typedef enum { STATE_INIT, STATE_IDLE, STATE_SEARCH, STATE_AVOID, STATE_FEED, STATE_BACK } RobotState; void Robot_Task_Handler(void) { static RobotState state = STATE_INIT; uint32_t now = HAL_GetTick(); switch (state) { case STATE_INIT: if (now > g_start_tick + 200) { state = STATE_IDLE; } break; case STATE_IDLE: if (g_ultrasonic_cm < 30.0f) { state = STATE_SEARCH; } break; case STATE_SEARCH: Servo_SetAngle(&htim2, TIM_CHANNEL_1, g_scan_angle); if (g_ultrasonic_cm < 10.0f) { state = STATE_FEED; Voice_Play(1); } break; case STATE_FEED: state = STATE_BACK; break; case STATE_BACK: state = STATE_IDLE; break; default: state = STATE_INIT; break; } }状态机的好处是每个状态只做一件事,调试时可以强制把 state 赋成 STATE_FEED 单独验证投喂动作。主循环里禁止直接调 HAL_Delay 做长时间等待,那样会让超声测距和舵机扫描全部停摆;需要延时的动作改成“记录目标时间,非阻塞查询”,或者直接利用 HAL_GetTick 的差值判断时间片。FreeRTOS 需要项目里的变量和中断协同,不上 RTOS 时状态机方案对课程设计而言更稳。
如果你想把课程设计做出嵌入式内核的深度,不用动 Linux 级的内核源码,对照 FreeRTOS 源码看消息队列的阻塞唤醒机制就够了。把超声模块放进一个任务,把投喂动作放进另一个任务,两个任务用队列传距离值,代码表达会比状态机更接近现代嵌入式开发方式。
4.3 串口调试与常见错误排查:从 printf 到 no target found
printf 重定向是联调第一件事,没有日志排错几乎靠猜。在 usart.c 里补上 fputc,并把微库勾选上。
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }接下来可以做一个最简单的串口命令解析,用角度和距离命令代替反复改代码烧录。
void Debug_Command_Parse(char *cmd) { if (strncmp(cmd, "servo", 5) == 0) { int angle = atoi(cmd + 6); Servo_SetAngle(&htim2, TIM_CHANNEL_1, (float)angle); printf("servo set to %d\r\n", angle); } else if (strncmp(cmd, "dist", 4) == 0) { float cm = HC_SR04_GetDistance(); printf("dist: %.1f cm\r\n", cm); } }这条命令解析规则是:收到以 servo 开头的字符串,提取后面的数字作为目标角度;收到 dist 则打印当前距离。串口接收中断把字节攒成整帧,遇到换行符交给 Debug_Command_Parse,日志和指令在同一通道完成。这样一个模块一个模块验证,避免“全部接好一次调不通”的窘境。
联调过程中最常碰到的几类问题,按出现频率列一下:
- 报 error: no stm32 target found,优先检查 SWD 的 SWDIO、SWCLK、GND 三根线,按住板子复位键再点下载,多数焊接不良的板子用这个办法能缓解。
- 设备管理器里 stm32 virtual com port 带叹号,通常是 USB 转串口驱动版本与系统不匹配,换一个 USB 口或重装驱动一般能消失。
- printf 输出卡死,检查中断优先级里 SysTick 是否被更高优先级中断长时间抢占,或者 HAL_UART_Transmit 的超时时间从 0xFFFF 改小。
- 串口收到大量乱码,先确认 HSE 晶振实际频率和代码里的数值一致,8MHz 晶振配成 MUL9 是 72MHz,配成 MUL16 会直接超出 72MHz 上限。
- 舵机抖动,大概率不是代码问题,而是舵机电源与 MCU 电源没分,或者电容容量不足。
烧录顺序建议固定为:先电源指示灯亮,再烧一个 GPIO 闪烁工程,然后分别验证串口、舵机、超声,最后才把状态机整机带起来。每验证一个模块就多一条日志,整体联调时只看状态切换,不看底层寄存器。
5. 验收细节与答辩加分技巧:把课程设计做成作品
演示和答辩环节的准备工作,往往比写代码更能拉开差距。下面这些细节是课程设计验收现场最常见的扣分点,也是快速提分的部分。
5.1 展示前检查这 5 个细节
电池电压。演示通常持续 5 到 10 分钟,MG996R 连续动作时电压掉得快,演示前用万用表测舵机满行程下的电压,低于 4.8V 就换电池或并联电容。
舵机线束固定。三根线在关节活动处容易折断,用热缩管包住焊点,扎带固定在支架上。这不是外观分,而是可靠性分。
复位按钮并 10nF 电容。反复烧录后 NRST 引脚容易被调试器干扰,并联小电容能让复位信号更干净,减少偶发复位。
删除调试残留。联调用的 printf 注释掉,测试用的固定延时删掉,保持代码干净。答辩时老师翻代码,看到一堆测试垃圾比看到 Bug 更扣分。
整机重新上电测试。从关机状态开机完整跑三遍,观察状态灯和串口日志,确保上电瞬间舵机不会猛甩到极限角度。
5.2 答辩讲代码的两条路线:状态机和中断优先级
第一条路线是讲状态机:为什么初始化后 200ms 才进入待机?为什么超声波距离小于 30cm 进入扫描、小于 10cm 才投喂?这些阈值从哪里来?回答时要能说出“30cm 是舵机摆动一个周期内能发现宠物的安全距离,10cm 是爪子落料能覆盖的范围”,而不是“我随便设的”。展示状态切换日志,配合硬件动作,比背 PPT 有力得多。第二条路线是讲中断优先级:为什么 Echo 用轮询而非外部中断?在 F103C8T6 上,单个回波测量期间 CPU 本来就空闲,轮询读取 DWT 周期计数器反而比外部中断加输入捕获更简单;如果有多个超声模块才需要把 Echo 接到 EXTI 上。这个取舍讲清楚,老师就知道你理解中断而不是只会用 HAL 库。
扩展方向不用贪多,挑一个点做出效果。想加视觉识别可以研究 k210 与 stm32 通讯,串口帧按长度、命令、校验字设计,避免粘包;想加远程控制,参考 stm32 8266 宿舍控制灯开发实战的思路,用 ESP8266 做远程投喂开关;想提升语音质量,stm32 数播 iis 设置里的 MCLK 与采样率配置值得研究,换成 I2S 音频解码后能播放更自然的声音。把舵机角度标定过程沉淀成串口日志,每一组 pulse 值都对应一组实测角度,这组数据比“心得体会”更能说明你会调试嵌入式系统。
本文还有配套的精品资源,点击获取