很多人桌上都摆过手办、绿植、加湿器,但能跟你互动的“桌面宠物”并不多见。我自己一直想做个能眨眼、会摇头、拍桌还会吓一跳的小东西,正好手里的STM32单片机可以搞定这件事。这篇写的“STM32单片机智能桌面宠物”,就是基于STM32F103这类主控做成的一只电子宠物:它用OLED屏显示表情,用舵机做动作,用六轴传感器感知震动和手势,整体成本不高、动手门槛也合适,学生党拿来练手或者做毕业设计都很香。
我打算把整个项目的设计过程、硬件选型、软件框架、调试踩坑和后续扩展一次写清楚,内容偏实战,适合有一定单片机基础但没完整做过交互项目的读者。文章不会讲那种“原理全会、上手全废”的空话,重点是你照着这套思路真的能焊出一只会“活”的桌面宠物。
1. 从“能动的摆件”到“桌面宠物”:需求拆解与整体架构
1.1 先想清楚宠物要干什么
很多人做桌面宠物容易跑偏,一上来就想着加屏幕、加摄像头、加语音识别,结果东西越做越贵、代码越写越乱。我一开始就给自己定了个原则:这只宠物首先要“像活的”,其次才是“够聪明”。
什么叫“像活的”?你路过它时它会转头看你;你拍一下桌子它会做出被吓到的表情;你摸它一下它会开心地眨眼睛;你长时间不理会它,它会打瞌睡、低头眯眼。这些行为本质上就是“感知输入 + 状态切换 + 动作输出”的闭环,不需要什么高级算法,一个裸机状态机就能跑得很流畅。
所以我把需求收敛成四个方向:表情显示、肢体动作、输入感知、低功耗待机。四者缺一不可,但都不需要复杂的硬件堆料。
1.2 为什么用STM32而不是树莓派或ESP32
淘宝上很多桌面机器人用的是树莓派或者ESP32-CAM,功能确实强,但代价是体积大、成本高、系统一多就吃电。对于桌面宠物这个场景,STM32的优势非常明显:
- 实时性好:裸机或者轻量RTOS下,中断响应是微秒级,拍桌子的信号能很快触发动作,不像Linux会有调度延迟。
- 成本低:一片STM32F103C8T6蓝色板子十几块钱,整机物料不超过五六十元。
- 功耗可控:正常运行时电流几十毫安,做低功耗后能到微安级别,放在桌面上长期通电完全没压力。
- 生态成熟:网上STM32的例程、搜索引擎里的笔记、各种学习视频,多到看不完,遇到问题至少能找到排错方向。
有一个很容易忽略的点:桌面宠物是近距离交互设备,它的“聪明”不需要云端算力,而在于响应速度和行为逻辑的设计。STM32的定位刚好卡在这里。
1.3 系统架构:传感器、状态机和输出模块如何配合
整个系统的结构可以用一句话概括:输入事件经过状态机处理后,驱动显示和外设动作。
输入侧我选了三个来源:MPU6050六轴传感器检测震动/倾斜/拍击,物理按键作为确认输入,RTC定时器作为日历闹钟。输出侧是0.96寸OLED屏幕、SG90舵机、小蜂鸣器和LED灯。
核心逻辑不放在某个中断里,而是放在一个主循环的状态机中。中断只负责把“发生了什么事”记录到一个事件标志里,主循环读取标志后查询当前状态、切换状态、给出对应的显示和动作输出。
这个架构的好处是:新增一种“宠物反应”时,不用动硬件和底层驱动,只要在状态机里加一个分支就行。比如我想让宠物在每天下午三点提醒我喝水,只加一个RTC闹钟中断,在状态机里加一个“提醒喝水”的待机行为,完全不碰其他模块。
1.4 项目物料清单
我一共用到的物料如下,仅供参考,实际买配件时可以灵活替换:
| 模块 | 型号/规格 | 作用 |
|---|---|---|
| 主控板 | STM32F103C8T6开发板 | 核心控制与逻辑处理 |
| 显示模块 | 0.96寸OLED,I2C接口,128x64像素 | 表情、状态、动画显示 |
| 姿态传感器 | MPU6050六轴模块 | 震动、倾斜、拍打检测 |
| 舵机 | SG90 180度模拟舵机 | 头部偏转、点头摇头动作 |
| 蜂鸣器 | 有源蜂鸣器模块 | 声音反馈 |
| 按键 | 轻触开关 | 手动交互 |
| 光敏电阻 | 5537光敏电阻模块 | 桌面光线感知,自动调亮度 |
| 电源 | USB 5V供电 + AMS1117-3.3 LDO | 给主控和舵机供电 |
如果你手头有STM32F401或STM32G030,也完全可以,Boot引脚换一下、时钟树重新配一下就行。不要被具体型号限制住思路。
2. 硬件选型:主控、屏幕、传感器与舵机的取舍逻辑
2.1 主控板:STM32F103C8T6为什么够用
先说结论:对于桌面宠物这个项目,STM32F103C8T6是性价比很合理的芯片。
它有一颗Cortex-M3内核,最高72MHz主频,Flash 64KB,RAM 20KB。这个资源级别跑一个128x64的OLED显示、一路PWM舵机、I2C读取六轴传感器、一个小型状态机,绰绰有余。更别说芯片库里现成的DMP姿态解算代码,在F103上跑得动,只是稍微吃一点CPU时间。
我特意留了USB接口,因为开发阶段要频繁下载程序,USB转串口同时也能当调试日志输出通道。C8T6没有板载ST-Link,但用一根SWD四线下载线接到ST-Link/V2或者DAP-Link上就行。
有一点要提醒:C8T6的Flash只有64KB,如果后面想加OTA功能会非常紧张。OTA本身不难,但BootLoader加App双分区会把可用Flash削减到40KB左右,表情动画多了可能会挤爆。真打算系统化升级,建议换F103RET6或者其他256KB以上的型号。这块我在第六部分还会展开说。
2.2 OLED选择:I2C接口更省引脚,SPI接口刷得更快
0.96寸OLED有两种常见接口:I2C和SPI。I2C版本只有四根线(VCC、GND、SCL、SDA),占用两个引脚,接线非常省事,F103的PB6/PB7硬件I2C或者任意GPIO模拟I2C都能驱动。缺点是刷新率低,全屏刷一帧大概要十几毫秒,做“眨眼”这种小范围动画倒是够用。
如果想让表情变化更流畅,建议用SPI接口,多接几根线,但刷新速度快了不止一个量级。我在这个项目里先用I2C版本点亮,是为了减少前期排线问题。说实话后面做动画的时候确实感觉I2C刷新有点拖沓,但配合局部刷新技术,肉眼看不出来卡顿。
推荐做法:前期用I2C版省心,后期想要更细腻动画直接换SPI屏,驱动代码可以用U8g2库统一适配,U8g2对这两种接口都有现成移植,省去不少重复造轮子时间。
2.3 MPU6050手势传感器:一拍就吓一跳的秘密
MPU6050是六轴传感器,意思是它内部集成了三轴加速度计和三轴陀螺仪,用来测量物体的加速度和角速度。在桌面宠物项目里,它的主要作用是判断“有没有被拍、有没有被移动、当前是不是倾斜状态”。
我第一次做的时候只用了三轴加速度,采集到的原始数据范围某个轴上大幅跳变,就判定为“震动”。比如手拍桌面瞬间,桌子上的传感器Z轴加速度会瞬间冲到好几个g,这个跳变量级比平时说话震动的噪声大得多,用阈值判断就能稳定触发。
陀螺仪的用处是判断宠物姿态的旋转速度,比如我把宠物底座转了一下,它就能感知到旋转运动,从而做出“晕头转向”的表情。当然你也可以直接用DMP库解算出欧拉角,给宠物增加一个“身体倾斜时表情不变”的稳定功能,这部分我在第三节会说具体实现。
2.4 SG90舵机:动作感的来源
SG90是很常见的9g微型模拟舵机,三根线:红色电源、棕色地线、橙色信号线。信号线接到STM32的定时器PWM输出引脚,通过改变PWM占空比控制舵机转动角度。
很多人听到舵机就觉得难,其实它内部有电机、减速齿轮和反馈电位器,是一个闭环系统,MCU只要给它标准50Hz、脉宽0.5ms到2.5ms的控制信号,它就会自动转到对应角度。这意味着我们完全不需要关心电机电流和位置反馈算法,代码里只需要算好CCR寄存器的值。
需要注意SG90的启动电流经常在500mA甚至更高,如果直接和单片机共用一颗稳压芯片供电,舵机一转就会把电压拉低,然后MCU就复位了。我一开始就吃过这个亏,后来把舵机电源改成从USB 5V经过二极管降压后单独供电,共地但各自走独立的供电路径,问题才解决。这个坑后面会细说。
3. 表情、动作与感知:让宠物真正“活”起来的工作机制
3.1 OLED表情与动画:不只是画一个笑脸
OLED要显示“活”的表情,本质是给位图数组加上时间和状态管理。
最简单的表情是一帧静态图,例如一个圆圆的像素笑脸。但“活”起来需要多帧切换:眨眼的动画从“睁眼”到“半闭”再到“闭眼”再回来;受到惊吓时从“平静”突然变成“嘴巴张大、眼睛瞪圆”。做法是用取模工具把每帧表情都做成一个const uint8_t frame_x[]数组,然后用一个全局变量保存当前帧序号,定时器每隔几十毫秒切换一帧。
我推荐用PCtoLCD2002或者Image2Lcd这类取模软件,把128x64的图形转成字节数组。注意取模方向要跟OLED驱动的清屏扫描方向一致,否则画面会像被切碎的拼图一样错位。
显示上有一个细节:整屏刷新会产生拖影,尤其是切换表情时,上一帧残留会在屏幕上留下一道“扫”的痕迹。我实际用的方案是局部刷新:只更新有变化的那一页页内存区域,不动的地方保持显存数据不变。0.96寸OLED为128列x64行,按纵向8个像素为一个“页”,一共8页,可以只写变化的页,速度快而且画面干净。
3.2 舵机PWM驱动:定时器计算和角度换算
STM32F103的定时器非常强大,我选择TIM2的CH1通道输出PWM控制头部舵机。要把SG90驱动到指定角度,核心是算好PSC(预分频)和ARR(自动重载值)。
系统主时钟72MHz,APB1总线时钟36MHz,定时器时钟在预分频系数不为1时是APB1时钟的两倍,也就是72MHz。为了得到50Hz的PWM,我这样设置:
// 定时器时钟 72MHz // 预分频 PSC = 71,则计数频率 = 72MHz / (71+1) = 1MHz,即每计数1次 = 1us // 自动重载 ARR = 19999,则 PWM 周期 = (19999+1)us = 20ms = 50Hz TIM_TimeBaseInitTypeDef.TIM_Prescaler = 71; TIM_TimeBaseInitTypeDef.TIM_Period = 19999;SG90的角度对应关系:0.5ms脉宽是0度,1.5ms脉宽是90度,2.5ms脉宽是180度。由于计数频率1MHz,脉宽时间(微秒)数值上就等于CCR值。所以50%占空比,也就是90度位置时,CCR=1500。
这个思路可以推广到其他任意舵机,只要知道目标角度,线性换算CCR即可:
uint16_t angle_to_ccr(uint8_t angle) { // angle: 0~180 // 对应脉宽 500us~2500us return 500 + (uint16_t)((uint16_t)angle * 2000 / 180); }3.3 MPU6050手势识别:阈值判定和滤波
MPU6050读取原始数据后,加速度计每个轴返回的ADC字不是直接的物理量,要先换算。我把量程设为±2g,此时灵敏度是16384 LSB/g。也就是说读取到的原始值除以16384,得到的就是以g为单位的加速度。
float ax_g = (float)raw_ax / 16384.0f;手势识别的逻辑不能直接用原始数据,我加了一层滑动平均滤波,取最近20次样本的平均值作为静态基准。当实时值与基准值之差超过0.5g时,判定为一次震动。
“拍桌”检测的关键在于窗口:震动信号是一个很陡的尖峰,持续时间大约几十毫秒。我检测到超阈值后并不会立即触发吓一跳动画,而是再等100ms读取一次,如果接近恢复基准值,才确认为单次拍击。这个防抖窗口能挡住很多误触发,比如旁边的人关门。
MPU6050还负责“被拿起来”的检测。当宠物长时间静止在某一个姿态,然后突然发生较大的角度变化,我判定为“被移动了”,这时候状态机会让宠物做一个“好奇左右看”的动作组合:先低头,再左右摇头,最后正视前方。
3.4 状态机的设计:事件驱动,而不是轮询堆积
桌面宠物的行为逻辑我建议用一个简单的状态机来管理,不要把所有判断写在一个几百行的while(1)循环里。
状态机枚举大致长这样:
typedef enum { PET_IDLE, // 待机,偶尔眨眼 PET_CURIOS, // 好奇,左右看 PET_SCARED, // 受惊,瞪眼张嘴 PET_HAPPY, // 开心,快速点头 PET_SLEEPY, // 犯困,慢速眨眼 PET_SLEEP // 睡眠,屏幕关闭 } pet_state_e;事件来源由中断置位,比如拍桌事件后置event_flags |= EVENT_TAP,然后在主循环里读取并清空。状态转移则根据当前状态和事件查询一张二维表,或者用switch-case配合子函数处理。
这个设计最大的好处是调试很容易:每个状态下,宠物的行为是确定的。出了bug,只要在串口里打印状态编号和触发事件,就能快速定位是不是某条转移条件写错了。
4. 软件工程化:时钟树、定时器、中断与代码组织
4.1 时钟树必须自己心里有数
说句掏心窝的话,STM32入门绕过时钟树,后期多半要踩坑。很多新手第一次用F103,代码里直接抄网上的RCC_Configuration,一旦换了外部晶振频率或者换了芯片型号,外设彻底不转。
F103默认可以使用内部8MHz HSI,但为了更精准的PWM和串口波特率,我用的是外部8MHz晶振,通过PLL倍频到72MHz。系统时钟72MHz,AHB不分频,APB1分频/2得到36MHz,APB2分频/1得到72MHz。这里有个容易忽略点:APB1预分频系数不等于1时,定时器时钟自动翻倍,所以挂在APB1上的TIM2/TIM3/TIM4最终时钟还是72MHz,串口和I2C的时钟则可能是36MHz。
搞不清没关系,用STM32CubeMX图形化配置一遍,能直观看到时钟树每条路径的最终频率。我强烈建议新手先打开CubeMX把时钟树点一遍,再回到Keil工程里代码就有了底层支撑。
4.2 SysTick系统时钟:一切时间调度的基石
桌面宠物需要很多定时场景:表情切换、眨眼间隔、防抖窗口、低功耗进入延时、喂食提醒。我不建议每个功能各开一个TIM定时器,那样资源很快就会被耗尽。
我的做法是统一使用SysTick滴答定时器,配置成1ms中断一次,在中断里累加一个tick_ms全局变量。所有非实时性定时都基于这个tick_ms做差值判断。
volatile uint32_t tick_ms = 0; void SysTick_Handler(void) { tick_ms++; } bool time_elapsed(uint32_t *last, uint32_t interval) { if (tick_ms - *last >= interval) { *last = tick_ms; return true; } return false; }SysTick响应很快,1ms中断一次对状态机来说足够。真正需要精确高频的任务(比如舵机PWM、I2C时序)才交给硬件定时器或DMA处理。
4.3 中断优先级与事件标志:避免中断里做重活
桌面宠物项目里中断通常有这几个:SysTick(1ms)、串口接收、外部按键、RTC闹钟、MPU6050数据就绪。如果中断里都去处理业务逻辑,嵌套一多,整个系统的实时性就崩了。
我的原则是:中断里只做“置标志+最小数据处理”,主循环里再做“业务逻辑+外设操作”。
比如MPU6050有数据就绪,中断里只把原始数据搬进一个全局缓冲区并置位“新数据标志”,主循环发现标志后去读缓冲、做滤波、更新姿态。软件I2C读取MPU6050的过程中若被其他中断打断,时序就会乱,这个架构能有效避免。
STM32的NVIC中断优先级一定要配置合理:SysTick设成最高的优先级,舵机和I2C相关的中断也要比普通外设高半级。否则PWM输出会抖,SPI/I2C传输偶尔会失败。
4.4 代码分层和模块划分
我按下面的目录组织工程代码,这种风格兼顾了可读性和可维护性:
user/ main.c, stm32f10x_it.c bsp/ bsp_oled.c, bsp_mpu6050.c, bsp_servo.c, bsp_key.c app/ app_pet_fsm.c, app_expression.c, app_event.c drivers/ oled_font.c, oled_gui.c, mpu6050_dmp.cBSP层只负责具体的硬件初始化和读写操作,比如bsp_servo_set_angle()、bsp_oled_show_frame()。APP层是行为逻辑,所有“宠物应该表现出什么反应”的代码都放在这里。这样做的好处非常明显:重新换一块屏幕或者换一种传感器,我只需要重写对应的BSP文件,APP层的状态机完全不用动。
开发工具上,我是用Keil MDK为主的,同时也在VSCode里装了EIDE插件做备选,日常调试还是Keil顺手。如果你同时要写C51和STM32,Keil里注意用Pack Installer安装对应的芯片支持包,不要混在两个工程里导致编译器选项互相干扰。
4.5 关于“单片机没有堆栈吗”这个问题,也顺便说一下
热搜里总有人问“单片机C语言没有堆栈吗为什么”。其实是有的,而且很关键。STM32的启动文件startup_stm32f10x_hd.s会分配一块RAM作为主堆栈,Cortex-M3的MSP(主堆栈指针)就指向这里。局部变量、函数调用、中断压栈都靠这块内存。
Keil里魔术棒Options → Target → Stack/Heap里那个Stack大小默认0x400(1KB),我实际调试过,如果状态机和DMP库一起用,1KB的栈在某些分支下会溢出,表现就是程序随机跑飞。建议把Stack加到0x800以上,并多测试递归/浮点运算比较多的分支。这个坑普通教程里几乎没人提。
5. 调试与避坑:从刷不进程序到舵机抖动
5.1 程序下不进芯片:Boot引脚与ST-Link排查
新手最容易卡住的就是“明明点了下载,芯片没反应”。我做成这项目的过程中也遇过几次,排查顺序基本是固定的:
- 检查Boot0和Boot1引脚:正常从Flash启动时,Boot0必须接低电平(GND),Boot1接低电平。如果Boot0被拉到高,芯片会进入ISP模式,SWD下载可能连不上。
- 检查ST-Link是否被识别:设备管理器里能看到“ST-Link”或者“CMSIS-DAP”,看不到就换USB线、换USB口,尤其注意有些USB线只能充电不能传数据。
- 检查接线:SWD只需要SWDIO、SWCLK、GND三根线,部分调试器还需要接3.3V给目标板供电。
- 检查Keil中Debugger设置:ST-Link接口要选SW模式,不要选JTAG,速率可以调低到1MHz,尤其杜邦线较长时,高速下载经常失败。
如果你手上是STM32F103C8T6核心板,90%的情况是Boot0没拉低或者ST-Link驱动没装干净。
5.2 F103硬件I2C的坑与解决:软件I2C更省心
F103的硬件I2C模块一直是很多人的噩梦。标准外设库下配置I2C1读MPU6050,有时候初始化的确能出数据,但跑一段时间后状态寄存器卡在BUSY,整个I2C总线瘫掉。而且这种问题不是每次都能稳定复现,特别难调试。
我后来直接改成软件模拟I2C:用两个GPIO引脚,手动拉高拉低时钟和数据线,按照I2C时序读写。代码如下风格示意:
#define I2C_SCL_H GPIO_SetBits(GPIOB, GPIO_Pin_6) #define I2C_SCL_L GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define I2C_SDA_H GPIO_SetBits(GPIOB, GPIO_Pin_7) #define I2C_SDA_L GPIO_ResetBits(GPIOB, GPIO_Pin_7)为什么软件I2C反而稳?因为不依赖芯片内部状态机的时序错误,只要GPIO配置正确,时序就是准确的。OLED和MPU6050速率要求都不高,400kHz的快速模式完全不必需,200kHz左右的软件I2C也能流畅跑。前提是中断别太多,或者在使用过程中临时关闭能打断数据传输的中断。
5.3 舵机抖动和单片机复位:供电问题是罪魁祸首
第一版时我把SG90直接接在开发板的3.3V上,舵机一旦转动,屏幕就闪一下,然后程序重启。用万用表一测,舵机转动时3.3V电压跌到了2.5V以下,单片机的供电完全达不到工作电压。
SG90在堵转时瞬间电流能到几百毫安,但没有哪个核心板的AMS1117能稳压输出那么大电流。正确做法是单独给舵机供电:外部5V电源(比如USB转出的5V)经过一个100uF电容之后接到舵机红线,MCU的电源仍然用3.3V,两者只共地。
如果你也想用一个电源搞定,至少要选择输出能力1A以上的稳压芯片,并给舵机电源加滤波电容。即便如此,我还是建议“共地分离供电”,这是最稳妥的。
5.4 程序随机跑飞:堆栈和看门狗的联合排查
跑飞是最让人头秃的bug,通常我先排除两个最可能的原因。
第一是堆栈溢出,前面说过Stack大小从0x400调到0x800;第二是printf重定向到了串口,但发送缓冲区处理不当,导致在调试代码里卡死。印象最深的是第一次加DMP库,FPU单元或者浮点运算比较频繁,栈消耗增大明显,MSP直接冲破有效栈顶,程序跳进HardFault_Handler。用Keil的调试器开着HardFault的调用栈看,能定位具体是哪个函数越界。
加看门狗也不能乱加。独立看门狗IWDG设置的时间如果太短,比如喂狗隔了500ms但窗口设成300ms,反而会把原本正常的程序砸死。IWDG的超时时间要根据主循环最长执行时间加余量来设置,我目前用0.5秒超时,配合主循环内喂狗。
5.5 OLED显示异常和MPU6050数据漂移
OLED显示错乱,先检查取模方向和显存扫描方向。U8g2库有一个麻烦点:它默认的缓冲区布局和手里的屏有时不匹配,需要按资料调整u8x8的初始化和翻页顺序。
MPU6050的数据漂移主要是没有做陀螺仪零偏校准。拿到的模块静止放置,读取至少100个样本的陀螺仪原始值,取平均写入偏移寄存器,效果立刻改善很多。DMP库如果版本太老,在F103上会出现输出角度漂移,可以换用较新的运动驱动库版本,或者直接用自写的互补滤波,代码量不大但稳定性很直观。
6. 进阶玩法:低功耗、OTA、RTOS和联动房间设备
6.1 低功耗设计:白天陪伴,深夜沉睡
桌面宠物大多数时间是“摆着”,并没有必要满负荷运行。STM32F103支持睡眠、停止、待机三种低功耗模式。我平时用的是停止模式(Stop),这模式下CPU时钟关闭但SRAM和寄存器内容保持,可以由外部中断或RTC闹钟唤醒。
进入停止模式前,先把OLED刷新暂停、关闭不必要的LED、把I2C总线切换到高阻态。实测停止模式电流可以降到20uA左右,对一片USB长期供电的主控来说几乎可以忽略。唤醒后重新做外设初始化,再按当前状态和上次保存的时间判断下一步行为。
关键点是停止模式唤醒后要重新配置外设时钟,比如SysTick会停掉,需要重新开启并用上一次tick恢复时间基。否则表情动画的延时判断会全部错乱。
6.2 OTA升级:给桌面宠物“补丁”能力
如果想让宠物持续维护,OTA是很好的加分项。最简方案是做IAP升级:BootLoader区负责接收串口或无线传来的固件,写入App区,然后跳转执行;App区运行主逻辑。F103C8T6的64KB确实紧张,我把BootLoader压缩到8KB,App区40KB,剩下留给参数存储。
跳转前查中断向量表和系统内存布局时,App里的SCB->VTOR要设置为FLASH_BASE | APP_OFFSET,否则一切中断都会失败。我踩过一次:Boot跳转后屏幕能亮但串口中断不工作,就是这个偏移没设置。
6.3 FreeRTOS化:任务划分的可行性
如果你的宠物功能越来越多,比如加语音识别、加多个表情菜单、加联网对时,裸机状态机会变得复杂,这时候可以换FreeRTOS。任务划分我会这么安排:
- 显示任务:阻塞等待“显示队列”,收到消息后刷新OLED。
- 感知任务:周期读取MPU6050,滤波并发布事件。
- 动作任务:接收动作指令,控制舵机角度和蜂鸣器。
- 低功耗任务:无事可做时进入休眠。
任务间传递数据建议用一个事件队列,结构体里带事件类型和参数。FreeRTOS的优先级设置上,感知任务要高于显示任务,否则拍桌反馈会滞后。
6.4 联网联动:ESP8266串口透传和本地提醒
桌面宠物再往前一步,就是接入房间网络。最简单的方式是接一个ESP8266模块,使用AT指令通过串口转发数据。宠物可以把当前状态(如“开心”“犯困”)上报到局域网,服务器端或者手机APP可以查询它的心情;反过来手机也能下发一条“摸摸头”指令,让宠物做一个开心的动作。
我自己的习惯是:这类联动功能尽量放在BSP层做,避免破坏状态机结构。模块通信采用独立的串口,用队列缓冲协议数据。如果你愿意,还可以利用MQTT协议接到智能家居平台,实现“宠物在你回家时自动挥手”之类的玩法。
6.5 可扩展的方向和建议
硬件上还能加很多好玩的东西:离线语音识别模块(如SU-03T)让宠物听懂名字;TF卡模块播放音效;PS2摇杆作为遥控器;甚至手写一个手机蓝牙小程序通过HC-05控制它。软件上也可以给表情动画加更多帧、用像素画工具做不同主题皮肤。
但我想说的是:精力的投入要按性价比排序。最影响“像活物”的是状态机的行为逻辑和表情动画的节奏,而不是硬件数量。先把“眨眼、受惊、开心、犯困”四种基本反应做到位,再去堆外围模块也不迟。
写在最后的一点个人经验
这类“智能桌面宠物”项目,真正收获最大的不是让它转起来那一刻,而是盯着状态机一点点把行为调“像”的过程。我实际上手时,光是调整拍桌检测的阈值和眨眼动画的切换间隔就花了两三个晚上,但这段时间学到的东西比单纯看教程多得多。
建议第一次动手的人别急着把所有模块一次焊完,分三步走:先用OLED点亮一个眨眼的表情动画,再接舵机让头部会动,最后加上MPU6050感知。每一步都跑通了再进入下一步,出现bug时就能很快判断是显示问题、驱动问题还是逻辑问题。
如果你也准备做一个,我的建议是把代码仓库好好整理一下,BSP和APP分层清楚。这样不管是你自己半年后再看,还是把项目开源出来给其他爱好者参考,都很舒服。有声音会动、会感知的桌面宠物,真的能让每天对着屏幕干活的日子稍微有趣一点。