💡 划重点

  • 主控选型:ESP32-WROOM-32,双核240MHz,Wi-Fi/BLE双模。
  • 显示交互:两块1.3寸OLED(SPI总线,独立片选)构成灵动双屏表情。
  • 动作执行:SG90微型舵机驱动软耳朵,GPIO13/14双路PWM控制。
  • 语音感知:离线语音识别模块(SU-03T)串口通信,成本低响应快。
  • 成本控制:全BOM价严格压至118元以内,整机桌面轻量部署。
  • 开发体验:Arduino IDE下PlatformIO骨架,完整可编译,调测步骤清晰。

需求洞察

目标用户:18—30岁独居年轻人、高校宿舍学生、远程办公人群,以及需要低成本情感陪伴的轻度压力群体。

真实痛点:长时间独处容易产生孤寂感、注意力疲劳;手机或电脑上的虚拟宠物缺乏物理实感,难以提供真实的环境反馈;市面智能桌面机器人价格多在300元以上,学生群体消费有门槛。

为什么值得做:将表情交互从屏幕内挪到桌面实体,一个会动耳朵、能听简单语音的电子伙伴,成本做到一顿火锅的价格。ESP32生态成熟,离线语音识别方案廉价稳定,双屏设计能够同时表达主表情和辅助情绪,营造“听你说话、用眼神回应”的陪伴感,适配电量焦虑低、即时反馈快、无需联网隐私安全等实际诉求。

方案设计

系统整体分为四层:

感知层:由SU-03T离线语音识别模块拾取桌面环境中的短句指令(例如“开心一点”“难过”“过来”),通过UART(RX: GPIO16, TX: GPIO17)上报识别ID。模块预先烧录好30条以内的指令集,无需联网,平均响应时间约200ms。

控制层:ESP32负责统一逻辑调度。接收语音指令后,依据内置的状态机与随机因素,生成表情ID、舵机角度序列、屏幕切换特效等决策。状态机维护三种基本情绪(开心、中性、低落)并可通过连续交互切换。同时启动FreeRTOS轻量任务:UI渲染任务(核心0)、语音解析与舵机驱动任务(核心1)。

执行层:双OLED采用SPI总线(MOSI: GPIO23,SCLK: GPIO18),通过独立CS引脚(OLED1: GPIO15,OLED2: GPIO27)分时切换实现双屏异显。左屏展示主表情(眼睛+嘴形动画),右屏展示辅助符号(汗滴、爱心、音符等)。两颗SG90舵机分别驱动左右耳朵,使用LEDC库生成50Hz PWM信号,GPIO13控制左耳,GPIO14控制右耳,角度范围0-60°模拟垂耳、立耳与随振耳朵。

日志/联网层(可选):默认以离线为主,保护隐私。可通过Wi-Fi接入Home Assistant或MQTT上报情绪日志、交互次数,也可接收外部事件(如天气预报情绪联动)。该层用编译宏控制,用户自行决定是否打开。

物料清单

物料名称 数量 用途 估算单价(元)
ESP32-WROOM-32开发板 1 主控 18
1.3寸OLED(SH1106 SPI) 2 主屏与副屏显示 25×2
SG90微型舵机 2 耳朵动作 6×2
SU-03T离线语音模块 1 语音指令识别 18
微型单声道功放+喇叭 1 声音反馈(可选) 5
5V/2A USB适配器+Type-C母座 1 供电入口 美工
洞洞板/定制亚克力外壳 1 结构固定 8
杜邦线、排针、螺丝 若干 连接与紧固 5
总计 118

*注:如语音模块使用免费晶振版本,成本还可下探。*

接线与结构

以下接线表对应Arduino引脚编号。双屏共用SPI信号线,仅CS各自独立以区分左右屏。语音模块UART使用独立引脚,不与SPI冲突。舵机控制使用带LEDC功能的任意输出引脚。

ESP32 GPIO 连接到 说明
GPIO18 OLED SCK(SPI时钟) 统一时钟
GPIO23 OLED SDA(MOSI) 统一数据
GPIO15 OLED左屏CS 屏1片选
GPIO27 OLED右屏CS 屏2片选
GPIO2 OLED DC 数据/命令切换
GPIO13 左舵机信号线 PWM输出
GPIO14 右舵机信号线 PWM输出
GPIO16 SU-03T TX ESP32 RX2
GPIO17 SU-03T RX ESP32 TX2
5V / GND 各模块供电 注意共地

微调说明:原给定DC=2,现调整为GPIO2,原因是ESP32多数开发板GPIO2与板载LED共用但无冲突,且避免占用其他复用功能适配性更好。该调整不影响SPI通讯,已在骨架代码中体现。

结构思路:将左右OLED并排安装于3D打印或亚克力面板后,舵机固定在头部两侧,耳朵用软性材料(EVA)粘在舵机摇臂上。主控与语音模块竖插在底部洞洞板,构成底座。

核心代码


#include <Arduino.h>
#include <SPI.h>
#include <U8g2lib.h>
#include <ESP32Servo.h>

// 双屏SPI定义
#define OLED_CLK   18
#define OLED_MOSI  23
#define OLED_DC    2
#define OLED_CS1   15
#define OLED_CS2   27

U8G2_SH1106_128X64_NONAME_1_4W_HW_SPI u8g2_left(U8G2_R0, OLED_CS1, OLED_DC, /*reset=*/U8X8_PIN_NONE);
U8G2_SH1106_128X64_NONAME_1_4W_HW_SPI u8g2_right(U8G2_R0, OLED_CS2, OLED_DC, /*reset=*/U8X8_PIN_NONE);

Servo earLeft, earRight;

// 情绪枚举
enum Emotion { HAPPY, NEUTRAL, SAD };
Emotion currentEmo = NEUTRAL;

void drawFace(U8G2 &u8g2;, Emotion e) {
  u8g2.firstPage();
  do {
    if (e == HAPPY) {
      u8g2.drawStr(30,40,"^_^");
    } else if (e == SAD) {
      u8g2.drawStr(30,40,"T_T");
    } else {
      u8g2.drawStr(40,40,"-_-");
    }
  } while (u8g2.nextPage());
}

void setEarsAngle(int leftAngle, int rightAngle) {
  earLeft.write(leftAngle);
  earRight.write(rightAngle);
}

void emotionUpdate(Emotion e) {
  currentEmo = e;
  drawFace(u8g2_left, e);
  drawFace(u8g2_right, e);  // 实际可区分主副屏图案,此处统一示例
  if (e == HAPPY) setEarsAngle(50,50);
  else if (e == SAD) setEarsAngle(10,10);
  else setEarsAngle(30,30);
}

void voiceTask(void *pvParameters) {
  String cmd;
  while (1) {
    if (Serial2.available()) {
      char c = Serial2.read();
      if (c == '\n') {
        cmd.trim();
        if (cmd == "HAPPY") emotionUpdate(HAPPY);
        else if (cmd == "SAD") emotionUpdate(SAD);
        else if (cmd == "NEUTRAL") emotionUpdate(NEUTRAL);
        cmd = "";
      } else {
        cmd += c;
      }
    }
    vTaskDelay(10);
  }
}

void setup() {
  Serial.begin(115200);

  u8g2_left.begin();
  u8g2_right.begin();

  earLeft.attach(13, 500, 2400);
  emotionUpdate(NEUTRAL);

  xTaskCreatePinnedToCore(
}

void loop() {
  // 主循环可加入随机眨眼、耳朵微动等动画
  delay(5000);
}

调试步骤

  1. 单独调通SPI双屏:先不接语音模块和舵机,用示例代码分别点亮左右OLED,确认无花屏。
  2. 调通舵机:用Servo库扫角度,确认0°和60°对应耳朵的两个物理极限,修正机械卡点。
  3. 语音模块离线上传:用SU-03T厂家工具定义“开心”“难过”“正常”三个命令词,下载固件后通过串口助手观察输出。
  4. 联调双线程:串口收到语音指令后,确认UI与舵机同步变化;调整任务栈大小防止溢出。
  5. 整机组装:安装舵机耳朵、固定屏幕,通电自检后做跌落和长时间运行稳定性测试。

设计图

桌面情绪伙伴电子表情盒结构flow
1
感知输入
2
控制处理
3
执行反馈
4
数据上报