1. 桌面宠物项目的整体架构设计思路
1.1 为什么选择STM32F103C8T6作为主控
做桌面宠物这个想法,最早来源于我想在办公桌上放一个能互动的小玩意儿。市面上成品要么太贵,要么功能太单一,索性自己从头搭一套。核心需求很明确:体积小、成本低、能驱动多个舵机做动作、最好还能扩展传感器做交互。
主控选型上我对比过几种方案。ESP32性能强、自带无线,但价格偏高,而且桌面宠物不需要联网功能,多出来的射频部分纯属浪费。Arduino Nano生态好、上手快,但算力有限,跑多路舵机PWM时容易顾此失彼。树莓派Pico也不错,双核M0+,但当时供货不稳定,批量做的话风险大。
最终锁定STM32F103C8T6,理由很实在:这颗芯片在市场上流通了十几年,资料铺天盖地,价格压到了十块钱以内,48引脚LQFP封装手工焊接难度可接受,72MHz主频跑舵机控制和传感器轮询绰绰有余。它自带3个通用定时器,每个定时器能输出4路PWM,理论上单芯片就能驱动12路舵机,对桌面宠物来说完全够用。另外它的CAN、I2C、SPI、USART外设齐全,后续想加陀螺仪、超声波、OLED屏幕都有接口可用。
注意:市面上STM32F103C8T6有国产替代版本,引脚和寄存器基本兼容,但Flash实际容量和擦写寿命可能有差异。如果项目对稳定性要求高,建议从正规渠道采购原厂芯片,或者在代码里做容量检测。
1.2 硬件架构的分层设计
整个桌面宠物的硬件架构我分成了四层:电源层、主控层、执行层、感知层。这样分层的好处是每层职责清晰,调试时能快速定位问题出在哪一层。
电源层负责把锂电池的7.4V或者USB的5V转换成各模块需要的电压。舵机通常需要5V到6V供电,STM32需要3.3V,传感器有的要5V有的要3.3V。我用了一片AMS1117-3.3做降压给主控,舵机电源单独走一路DCDC降压模块,避免舵机启动时的电流冲击把主控拉复位。
主控层就是STM32F103C8T6最小系统板,加上晶振电路、复位电路、BOOT选择电路、SWD调试接口。最小系统板我建议直接买现成的,自己画的话要注意晶振负载电容匹配,8MHz晶振配20pF电容比较稳妥。
执行层主要是舵机。桌面宠物一般需要4到6个自由度:头部旋转、头部俯仰、左右手臂各一个、身体旋转或者尾巴摆动。我选的是SG90微型舵机,便宜、体积小、扭矩够用。如果要做更精细的动作,可以换成金属齿轮的数字舵机,但价格会翻几倍。
感知层包括超声波测距模块、红外接近传感器、MPU6050陀螺仪、光敏电阻等。这些不是必须的,但加上之后宠物能感知周围环境,互动性会强很多。比如超声波检测到有人靠近就抬头,光敏电阻检测到天黑就进入睡眠模式。
1.3 舵机选型与控制方式对比
舵机这块我踩过不少坑,值得单独说说。市面上常见的舵机分两类:模拟舵机和数字舵机。模拟舵机便宜,但响应速度慢,死区大,多个舵机同时动作时容易抖动。数字舵机内部有MCU做闭环控制,响应快、精度高,但价格贵,而且对电源要求更高。
桌面宠物用SG90这类模拟舵机就够了,但要注意几个参数:工作电压4.8V到6V,堵转扭矩1.8kg·cm左右,响应速度0.1秒每60度。如果你要做快速挥手的动作,SG90可能会跟不上,这时候可以考虑MG90S金属齿轮版本,扭矩更大,速度也快一些。
控制方式上,舵机靠PWM信号驱动,周期20ms,脉宽0.5ms到2.5ms对应0度到180度。STM32的定时器配置成50Hz,然后调节CCR寄存器的值就能改变脉宽。这里有个细节:多个舵机最好用同一个定时器的不同通道,这样它们的PWM周期完全同步,不会出现相位差导致的抖动。
实操心得:舵机电源和主控电源一定要分开走线,共地但不共电源。我一开始图省事用同一路5V供电,结果舵机一动主控就复位,查了半天才发现是电流冲击导致的。
2. 核心电路设计与关键元器件选型
2.1 最小系统电路的设计要点
STM32F103C8T6最小系统看着简单,但有几个地方容易出错。首先是复位电路,官方推荐用10k上拉电阻加100nF电容,复位按键并联在电容两端。这个RC常数决定了复位脉冲宽度,10k乘以100nF等于1ms,足够芯片完成复位。如果你用更大的电容,复位时间会变长,但没必要。
晶振电路是另一个关键点。STM32F103的外部高速晶振范围是4MHz到16MHz,我选8MHz是因为PLL倍频到72MHz刚好是9倍,计算简单。负载电容的选择要看晶振规格书,一般20pF左右。如果晶振不起振,先检查负载电容是否匹配,再检查PCB走线是否太长。晶振到芯片的走线要尽量短,最好包地处理。
BOOT电路决定了芯片从哪启动。BOOT0接低电平从Flash启动,接高电平从系统存储器启动。正常运行时BOOT0接地,但留一个跳线帽方便进入串口下载模式。BOOT1一般不用管,接个下拉电阻就行。
SWD调试接口最少需要四根线:3.3V、GND、SWDIO、SWCLK。我习惯再加一根RESET,这样调试器能直接复位芯片,不用手动按复位键。SWDIO和SWCLK各接一个10k上拉电阻,提高信号稳定性。
2.2 电源管理与复位电流问题
电源设计是桌面宠物硬件里最容易翻车的地方。整个系统的电流需求分三块:STM32核心约50mA,传感器加起来约30mA,舵机是耗电大户,每个SG90空载约100mA,堵转时能到700mA以上。如果同时驱动4个舵机,峰值电流可能超过3A。
我用的是两路独立供电方案:一路5V 2A的DCDC模块专门给舵机供电,另一路5V转3.3V的LDO给主控和传感器。两路电源共地但不共正极,这样舵机的电流波动不会影响主控。DCDC模块选的是MP1584,输入7V到28V,输出5V可调,效率能到90%以上。
复位电流这个问题值得展开说。STM32的复位引脚内部有上拉电阻,但阻值较大,容易受干扰。外部加一个10k上拉和100nF电容到地,能有效滤除毛刺。另外,如果舵机和主控共用电源,舵机启动瞬间的电压跌落可能触发主控欠压复位。解决方法是加大主控侧的滤波电容,我在3.3V输出端并了220uF电解加100nF陶瓷,实测能扛住舵机堵转的冲击。
注意:锂电池供电时要注意过放保护。我用的18650电池自带保护板,但如果你用软包电池,最好外加一个DW01加8205A的保护电路,防止电池过放到3V以下损坏。
2.3 PCB布局布线与走线规则
PCB设计这块我用的嘉立创EDA,免费而且元件库全。板子尺寸控制在5cm乘7cm以内,双层板足够。布局原则是先大后小、先高后低:先放舵机接口和电源模块,再放主控和传感器,最后放阻容小元件。
走线规则方面,电源线要粗。我算过,3A电流走1盎司铜厚的话,线宽至少要1.5mm,所以我电源主干走2mm。信号线0.3mm就够了,但PWM信号线最好远离电源线,避免耦合干扰。晶振走线要包地,长度不超过10mm。
层叠厚度对双层板来说,板厚1.6mm是标准值,铜厚1盎司。如果你要做阻抗控制,比如USB差分线,那得用四层板,但桌面宠物用不上。高低压隔离方面,舵机电源和主控电源在PCB上要分区,中间用磁珠或者0欧电阻连接,地平面也要分开,最后单点接地。
螺丝孔的制作在嘉立创EDA里很简单,放一个焊盘,内径3.2mm,外径6mm,然后勾选“金属化”就行。如果要做沉头孔,得在机械层画个更大的圆。
实操心得:PCB打样前一定要做DRC检查,特别是线宽和间距。我有一次忘了改默认线宽,结果电源线只有0.25mm,上电后直接烧断。另外,嘉立创EDA的“制造文件”导出要选Gerber格式,AD导入Gerber转PCB时注意层映射别搞错。
3. 舵机控制与嵌入式软件实现
3.1 STM32定时器PWM配置详解
STM32F103C8T6有四个定时器,TIM1是高级定时器,TIM2到TIM4是通用定时器。每个通用定时器有四个独立通道,可以输出四路PWM。我用了TIM2的四个通道控制四个舵机,TIM3的两个通道控制另外两个。
配置步骤是这样的:先开时钟,RCC_APB1PeriphClockCmd打开TIM2和GPIOA的时钟。然后配置GPIO为复用推挽输出,速度50MHz。接着配置定时器时基:预分频器设为71,自动重装载值设为19999。这样定时器时钟是72MHz除以72等于1MHz,计数20000次就是20ms,正好是舵机的PWM周期。
捕获比较寄存器CCR的值决定脉宽。CCR等于500对应0.5ms,舵机转到0度;CCR等于2500对应2.5ms,舵机转到180度。中间线性对应。我写了个函数,输入角度返回CCR值:CCR等于500加上角度乘以2000除以180。
void Servo_SetAngle(TIM_TypeDef* TIMx, uint8_t Channel, uint8_t Angle) { uint16_t ccr = 500 + (uint16_t)Angle * 2000 / 180; switch(Channel) { case 1: TIM_SetCompare1(TIMx, ccr); break; case 2: TIM_SetCompare2(TIMx, ccr); break; case 3: TIM_SetCompare3(TIMx, ccr); break; case 4: TIM_SetCompare4(TIMx, ccr); break; } }多个舵机同时动作时,最好用同一个定时器,这样PWM周期完全同步。如果分到不同定时器,虽然周期都是20ms,但相位可能差几毫秒,导致动作看起来不协调。
3.2 多舵机协同动作的调度策略
桌面宠物要做得生动,舵机不能各动各的,得有协同。比如“打招呼”这个动作,需要头部抬起、手臂挥动、身体微微前倾,三个舵机按特定时序配合。
我用的方法是关键帧插值。先定义几个关键姿势,每个姿势记录所有舵机的角度。然后设定过渡时间,在两个姿势之间做线性插值。比如从“休息”到“打招呼”用500ms,每20ms更新一次角度,总共25帧。这样动作看起来平滑自然,不会像机器人那样一顿一顿的。
typedef struct { uint8_t head_rot; uint8_t head_pitch; uint8_t arm_left; uint8_t arm_right; uint8_t body; } Pose_t; Pose_t pose_rest = {90, 90, 90, 90, 90}; Pose_t pose_wave = {90, 120, 150, 90, 100}; void Pose_Transition(Pose_t* from, Pose_t* to, uint16_t duration_ms) { uint16_t steps = duration_ms / 20; for(uint16_t i = 0; i <= steps; i++) { float t = (float)i / steps; Servo_SetAngle(TIM2, 1, from->head_rot + (to->head_rot - from->head_rot) * t); Servo_SetAngle(TIM2, 2, from->head_pitch + (to->head_pitch - from->head_pitch) * t); Servo_SetAngle(TIM2, 3, from->arm_left + (to->arm_left - from->arm_left) * t); Servo_SetAngle(TIM2, 4, from->arm_right + (to->arm_right - from->arm_right) * t); Servo_SetAngle(TIM3, 1, from->body + (to->body - from->body) * t); Delay_ms(20); } }这个方案简单有效,但有个缺点:Delay_ms是阻塞的,期间没法处理传感器输入。如果要做得更流畅,可以放到定时器中断里,每20ms更新一帧,主循环只管状态机切换。
3.3 传感器数据采集与串口通信
感知层我加了超声波和光敏电阻。超声波用的是HC-SR04,触发引脚给10us高电平,然后读回响引脚的高电平持续时间,除以58就是厘米数。光敏电阻接ADC,光照越强电压越低,通过ADC值判断白天黑夜。
串口通信用来调试和上位机控制。STM32的USART1接PA9和PA10,波特率115200。我写了个简单的协议:帧头0xAA,然后一个字节的命令,一个字节的数据,最后校验和。上位机发指令控制舵机角度,STM32收到后执行并回传当前状态。
void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { static uint8_t rx_buf[4]; static uint8_t rx_idx = 0; rx_buf[rx_idx++] = USART_ReceiveData(USART1); if(rx_idx >= 4) { rx_idx = 0; if(rx_buf[0] == 0xAA && rx_buf[1] + rx_buf[2] == rx_buf[3]) { Command_Handler(rx_buf[1], rx_buf[2]); } } } }注意:串口中断里不要做耗时操作,比如舵机动作。收到命令后置个标志位,主循环里再执行。否则中断嵌套可能导致数据丢失。
4. 常见问题排查与实操避坑指南
4.1 舵机抖动与复位问题排查
舵机抖动是最常见的问题,原因通常有三个:电源不稳、PWM信号干扰、机械负载过大。
电源不稳的排查方法很简单:用示波器看舵机电源引脚,如果舵机动作时电压跌落超过0.5V,那就是电源功率不够。解决办法是加大滤波电容或者换更大电流的DCDC模块。我一开始用LM2596,3A标称但实际到2A就烫得不行,后来换成MP1584才稳定。
PWM信号干扰的表现是舵机偶尔抽动,不按指令走。这时候要检查信号线是否和电源线平行走线,如果是,把它们分开或者加屏蔽。另外,信号线上串一个100欧电阻能有效抑制振铃。
机械负载过大时,舵机会发出嗡嗡声但转不动。SG90的堵转扭矩只有1.8kg·cm,如果你做的关节摩擦力太大,舵机就带不动。解决方法是加轴承或者用更光滑的材料做转轴。
复位问题我遇到过两次。一次是舵机和主控共用电源,舵机启动时主控复位。另一次是SWD接口的复位引脚没接,调试器一连接就复位。前者靠独立供电解决,后者把RESET线接上就好了。
4.2 PCB设计常见错误与修正
PCB打样回来发现错误是最让人头疼的,我总结了几条常见问题:
| 问题现象 | 可能原因 | 修正方法 |
|---|---|---|
| 芯片不工作 | 电源引脚虚焊或短路 | 用万用表测3.3V和GND是否短路 |
| 晶振不起振 | 负载电容不匹配或走线太长 | 换20pF电容,缩短走线并包地 |
| 舵机不动 | PWM引脚映射错误 | 查数据手册确认复用功能 |
| 串口无输出 | TX/RX接反 | 交换TX和RX再试 |
| 复位键无效 | 复位电路电容太大 | 换100nF电容 |
还有一个坑是丝印方向。嘉立创EDA里元件的丝印方向要和实际焊接方向一致,否则焊上去才发现引脚对不上。我习惯在PCB上留一个方向标记,比如在板子左下角画个圆点。
实操心得:PCB打样前先打印一张1比1的布局图,把元件放上去比划一下,确认接口位置和外壳匹配。我有一次没做这一步,板子回来发现舵机接口被外壳挡住了,只能重新打样。
4.3 嵌入式开发环境与工具链选择
开发环境我用的是Keil MDK加VSCode组合。Keil负责编译和调试,VSCode负责写代码和版本管理。Keil的优点是调试器支持好,ST-Link和DAPLink都能直接用。缺点是编辑器太老,代码补全弱。
VSCode里装几个插件就能当嵌入式IDE用:C/C++提供智能补全,Cortex-Debug提供调试支持,STM32-for-VSCode提供项目生成。用VSCode加Makefile加OpenOCD也能编译下载,但配置起来麻烦,适合喜欢折腾的人。
DAPLink是我常用的调试器,比ST-Link便宜,而且支持拖拽下载。把编译好的hex文件拖到DAPLink的U盘里就自动烧录,非常方便。不过DAPLink的SWD线要短,超过20cm就容易连接失败。
FreeRTOS移植在STM32F103C8T6上很成熟,网上教程一堆。但桌面宠物这种简单应用,裸机跑状态机就够了,上RTOS反而增加复杂度。如果你要同时处理多个传感器和舵机动作,可以考虑用RTOS的任务调度。
4.4 常见问题速查表
| 问题 | 排查步骤 | 解决方案 |
|---|---|---|
| 上电无反应 | 测3.3V、测晶振、测复位 | 逐级排查电源和时钟 |
| 舵机全抖 | 测电源纹波、查PWM周期 | 独立供电、加滤波电容 |
| 串口乱码 | 查波特率、查晶振频率 | 确认时钟配置和波特率一致 |
| 程序跑飞 | 查堆栈大小、查中断优先级 | 加大堆栈、调整优先级 |
| 下载失败 | 查BOOT引脚、查SWD线 | BOOT0接地、缩短SWD线 |
| 传感器无数据 | 查I2C地址、查上拉电阻 | 用逻辑分析仪抓波形 |
这张表是我调试时总结的,基本上覆盖了八成以上的问题。遇到新问题先查表,查不到再用示波器和逻辑分析仪抓波形。
5. 项目扩展与进阶方向
5.1 从桌面宠物到仿生机械臂的升级路径
桌面宠物做熟之后,很自然想升级到更复杂的机械结构。仿生机械臂是个好方向,核心技术和桌面宠物一脉相承,但复杂度高一个量级。
总线舵机是机械臂常用的执行器,和PWM舵机不同,总线舵机通过串口通信,每个舵机有独立ID,可以串联。好处是接线简单,一根线串到底,而且能读取舵机的位置、温度、负载等反馈信息。飞特舵机是常见品牌,但价格不便宜,一套六自由度机械臂的舵机成本可能上千。
双轴舵机的机械结构更复杂,需要三维建模。我一般用SolidWorks画图,然后3D打印。关键点是舵机安装座的公差要控制好,太紧装不进去,太松会晃动。打印材料用PLA就行,强度够用,ABS容易翘边。
从桌面宠物升级到机械臂,软件上要改的是运动学算法。桌面宠物用关键帧插值就够了,机械臂需要逆运动学求解,根据末端位置反推各关节角度。这块可以用现成的库,比如ROS里的MoveIt,但学习曲线陡峭。
5.2 加入无线通信与上位机控制
桌面宠物加上无线模块后,可玩性会高很多。ESP32自带WiFi和蓝牙,可以通过串口和STM32通信,做一个无线桥接。STM32负责实时控制,ESP32负责网络通信,分工明确。
上位机我用的Python加PyQt,通过串口或者WiFi发送指令。界面很简单:几个滑块控制舵机角度,几个按钮触发预设动作,一个文本框显示传感器数据。Python的pyserial库处理串口很方便,socket库处理WiFi也不难。
import serial import time ser = serial.Serial('COM3', 115200, timeout=1) def send_command(cmd, data): frame = bytes([0xAA, cmd, data, (cmd + data) & 0xFF]) ser.write(frame) time.sleep(0.02) def set_servo(channel, angle): send_command(channel, angle)这个上位机代码很简单,但够用了。如果你想做得更漂亮,可以用Processing或者Unity做3D可视化,实时显示宠物的姿态。
5.3 开源项目参考与学习路线建议
嵌入式学习路线我建议这样走:先玩Arduino熟悉基本概念,然后转STM32学寄存器操作,接着学FreeRTOS理解任务调度,最后根据兴趣选方向——想做控制就学PID和运动学,想做物联网就学网络协议和云平台。
开源项目方面,GitHub上搜“STM32 desktop pet”或者“robot arm STM32”能找到不少参考。但要注意,很多项目代码质量参差不齐,有的连基本的错误处理都没有。我建议先看Star多的项目,然后自己重写一遍,不要直接抄。
嵌入式八股文面试题里常问的volatile、内存对齐、中断优先级这些,在实际项目中都会遇到。比如舵机控制里,如果PWM更新放在中断里,共享变量就要加volatile,否则编译器优化后可能读不到最新值。
最后分享一个小技巧:调试嵌入式代码时,善用串口打印。但不要每行都打印,那样会拖慢程序。我的做法是定义一个DEBUG宏,发布版本关掉,调试版本打开。打印信息要带时间戳和模块名,方便定位问题。
这个桌面宠物项目我从画板子到写代码到调试,前后花了大概三周业余时间。中间烧过一块板子,换过两次电源方案,舵机也烧了两个。但看到它动起来的那一刻,觉得都值了。如果你也想做,建议先从最小系统板加一个舵机开始,跑通了再逐步加功能,不要一上来就画大板子,那样出问题很难定位。