Maixduino声源定位听声转头平台:双舵机云台DIY

云天硬件DIY 2026-10-17 10:18:49 34阅读 举报

用一只 Sipeed R6+1 麦克风阵列(6 个环形 + 1 个中心 MEMS 麦克风)在 Maixduino(K210)上做声源方向检测:听到声音从哪来,水平舵机就转头朝向它,环形 LED 同步指出方向。整套「听声 → 定向 → 伺服转头 → LED 指示」的声学伺服闭环,全程只用 MaixPy(MicroPython),可脱离 IDE 上电自跑。

项目简介

用麦克风阵列当「耳朵」、双舵机云台当「脖子」,实现:

  • 声源定位:12 个方向分区 + 加权重心,算出声音的方位角
  • 听声转头:声源在左,水平舵机转向左;在右,转向右
  • 方向指示:环形 12 颗 SK9822 LED 亮蓝灯指向声源,LCD 显示声源热力图
  • 防自激:舵机自己的电机声不会把自己「越叫越动」(高阈值 + 转向后静默锁定期 + 死区)
  • 独立运行:程序存成 main.py 上传 K210 Flash,上电自动跑,不用一直开 IDE

项目特点是「只听方向、只认声音」:不依赖图像标定,闭环由「声源方位角 → 加权重心 → 舵机增量」直接完成。方向数据在串口实时可看,方便标定和调试。

功能特性

  • 开机即定位:麦克风阵列初始化,环形 LED 上电自检
  • 声源定向:12 分区 → 加权重心方位角(比峰值 bin 稳得多)
  • 听声转头:方位角 → 水平舵机角(0~180°,左/右对正)
  • 防抖死区:方向变化 <5° 不转;能量门槛只响应「够响的持续人声」
  • 防自激:舵机转完进入 1.2s 静默锁定,忽略这段时间内的声音
  • 方向指示:环形 LED 蓝灯指方向;LCD 热力图;串口打印 az/pan
  • 独立运行:main.py 存进 Flash,上电自跑

硬件清单

部件型号/参数数量
主控Maixduino(Sipeed,基于 K210:RISC-V 双核 400MHz + FPU,自带 5" LCD)1
麦克风阵列Sipeed R6+1(6 环形 + 1 中心 MSM261S4030H0 MEMS,环形 12×SK9822 LED,10 脚 FPC)1
执行器双舵机云台:水平 pan 180°、俯仰 tilt 180°(只用 90~180°)2
连线杜邦线/排针若干
供电阵列 5V(同 LED);舵机独立 5V 电源 + GND 共地—

接线要点(本实机制坑后的正确接法):阵列信号直连 K210 的 GPIO,不是那排 A0~A5 丝印(A0~A5 是 ESP32 的脚,不是 K210 的)。本机实测有效配置:

信号引脚信号引脚
MIC_D0GPIO24MIC_BCKGPIO23
MIC_D1GPIO22LED_DATGPIO12
MIC_D2GPIO15LED_CLKGPIO11
MIC_D3GPIO32pan 舵机GPIO13
MIC_WSGPIO21tilt 舵机GPIO14

系统方案与原理

1. 为什么「峰值 bin」不稳,要用加权重心:MIC_ARRAY.get_dir() 返回 12 个分区(对应环形 360°)。单帧峰值 bin 在噪声下会 0↔8↔2↔3 乱跳(本机实测:同一个方向峰值 bin 到处跳)。改用加权重心(circular mean):

方位角 az = atan2( Σ v[i]·sin(2πi/12), Σ v[i]·cos(2πi/12) )  (0~360°)

峰值只取「哪个 bin 最响」做粗参考,加权重心给出连续稳定的方位角,再加一个 EMA 平滑(0.7 旧 + 0.3 新),方向从 180 这种散点收敛到很稳的值。

2. 面板竖放是「平面阵列」:左/右好测,正前最弱:R6+1 六个麦克风排在一个竖直圆上。平面阵列对同一平面内(左/右)的方向分辨最好,对正前(垂直面板方向)天生很弱。实测标定出左右两个稳定锚点:

正左 AZ ≈ 67    正右 AZ ≈ 258   (两者约相差 180°,符合「左/右互为反方向」)

所以这套平台只负责左/右转头(配你左右两侧的麦克风列),竖直面板正前弱是平面阵列固有特性。

3. 方位角 → 舵机角:把 az 相对 AZ_LEFT 归一化成横向位置 lat(0=左、180=右),再映射到水平舵机角并夹在安全区间:

lat = ((az - AZ_LEFT) % 360)     # 取 ≤180 的弧
pan = 90 + (lat/180 - 0.5) × 2 × 70    # 夹在 [0,180]

4. 防自激:为什么必须「高阈值 + 锁定 + 死区」:最初舵机一转,它自己的电机声被阵列听到 → 又触发转向 → 越转越响。三个措施一起破回路:

  • 能量门槛 MIN_ENERGY:只响应「够响的持续人声」,挡掉环境/舵机小幅噪声
  • 转向后静默锁定期 LOCKOUT_MS:舵机转完 1.2s 内忽略麦克风——自己的声音被「堵在门外」
  • 方向死区 DEADBAND:方向变化 <5° 不动,不抖

程序实现(完整可直接运行)

from Maix import MIC_ARRAY as mic
from machine import PWM
import time, math

# ===== 舵机(本机:pan=IO13 全量程,tilt=IO14 只用90~180)=====
pan = PWM(0, freq=50, duty=0, pin=13)  # 水平
tilt = PWM(1, freq=50, duty=0, pin=14) # 俯仰
PAN_LIM, TILT_LIM = (0, 180), (90, 180)
def angle2duty(deg): return 2.5 + deg/180.0*10.0   # 0~180° -> 2.5~12.5%占空比
def set_pan(deg):  pan.duty(angle2duty(max(PAN_LIM[0], min(PAN_LIM[1], deg))))
def set_tilt(deg): tilt.duty(angle2duty(max(TILT_LIM[0], min(TILT_LIM[1], deg))))

# ===== 麦克风阵列(本机实测有效引脚)=====
mic.init(i2s_d0=24, i2s_d1=22, i2s_d2=15, i2s_d3=32,
         i2s_ws=21, i2s_sclk=23,
         sk9822_dat=12, sk9822_clk=11)

# ===== 标定:左/右方位锚点 =====
AZ_LEFT, AZ_RIGHT = 67, 258
PAN_GAIN = 70      # 转头灵敏度
MIN_ENERGY = 8     # 能量门槛(挡环境声/舵机声)
LOCKOUT_MS = 1200  # 舵机转完静默锁定期(防自激)
DEADBAND = 5       # 方向死区

def az_deg(d):     # 加权重心方位角 0~360°
    x = y = 0.0
    for i in range(12):
        x += d[i]*math.cos(2*math.pi*i/12)
        y += d[i]*math.sin(2*math.pi*i/12)
    return (math.atan2(y, x)*180/math.pi + 360) % 360

def az_to_pan(az): # 方位角 -> 水平舵机角
    raw = (az - AZ_LEFT) % 360
    lat = raw if raw <= 180 else 360 - raw   # 0=左, 180=右
    a = 90 + round((lat/180 - 0.5)*2*PAN_GAIN)
    return max(PAN_LIM[0], min(PAN_LIM[1], a))

set_pan(90); set_tilt(90)   # 回中:水平90、俯仰90(平视)
pan_deg, ema = 90.0, 180.0
lock_until = 0

while True:
    now = time.ticks_ms()
    if now < lock_until:    # 静默锁定期:忽略,照常读避免卡缓冲
        mic.get_map()
        continue
    d = mic.get_dir(mic.get_map())
    if max(d) >= MIN_ENERGY:       # 只对"够响"持续声响应
        az = az_deg(d)
        diff = (az - ema + 540) % 360 - 180   # 循环平滑
        ema = (ema + diff*0.3) % 360
        t = az_to_pan(ema)
        if abs(t - pan_deg) >= DEADBAND:       # 死区消抖 + 慢速趋近
            pan_deg = (pan_deg*0.5 + t*0.5)
            set_pan(pan_deg)
            lock_until = time.ticks_ms() + LOCKOUT_MS   # 转完锁定防自激
        mic.set_led(d, (0, 0, 255))             # 环形 LED 指方向(蓝)
        print("az", round(az,1), "pan", round(pan_deg))
    time.sleep_ms(120)

独立运行:MaixPy IDE 里 File → Save to device 存成 main.py,上电即自动运行,脱离 IDE。

调试实录与踩坑

  1. 固件里根本没有 mic 模块(最坑):v0.6.0 的普通/精简/with_lvgl 全系固件都没编声源定位的 mic_array,必须用 v0.6.2 标准完整版才有 MIC_ARRAY,也才支持 MaixPy IDE。
  2. 模块名是大写的:是 from Maix import MIC_ARRAY as mic,不是小写 from maix import mic_array。写小写就是 ImportError: no module named 'maix'。
  3. A0~A5 丝印在 ESP32 上,不在 K210 上:那排 A 引脚是 ESP32 的地盘,外接麦克风阵列不该往那接,要直连 K210 的 GPIO(本机 24/22/15/32/21/23、LED 12/11)。
  4. init 一直返回 True、方向却全 0:mic.init(...) 只是「逻辑上把 GPIO 配成 I2S」,不检查物理上是否真接对了脚。接错脚时 init 照样成功、get_map 也有图,但 get_dir 恒 0。判断「接没接对」要看数据是不是随声源变,不能只看 init。
  5. 舵机信号没路由到脚上的隐藏坑:machine.PWM(ch, freq=50, duty=0) 创建后舵机不动——因为没传 pin=,PWM 信号去了默认脚。正确是 PWM(0, freq=50, duty=0, pin=13),pin= 参数把通道路由到目标 GPIO。duty 量程是 0~100 百分比。
  6. 没接电测舵机会误判「死区」:一度以为俯仰有死区、0~180 不到底,其实是不小心没给舵机供电在测。接电后 pan/tilt 全行程扫描都顺畅——先确认供电再做死区结论。
  7. 死区误判的教训:怀疑某个舵机「有死区/不转」时,用「好用的通道去驱那个引脚、再用另一通道去驱好用的引脚」做交叉隔离,别急着改代码。

实测效果与数据

  • 方位角:同一方向连续测,峰值 bin 在 0↔8↔2↔3 乱跳,加权重心 az 稳定在 70±10 / 258±12 两个簇,标定可靠
  • 转向:左喊 → 水平舵机转左,右喊 → 转右(反向就调换 AZ_LEFT/AZ_RIGHT)
  • 防自激:加 MIN_ENERGY=8 + LOCKOUT_MS=1200 + DEADBAND=5 后,舵机自身电机声不再触发自己

听声转头演示

下一步改进

  • 真正「只认人声」:当前 DOA 只认方向、不认「是不是人声」。两条路:① 换带 speech/关键词唤醒的固件,喊口令(如「小智」)才转头——最可靠;② 用 audio 做音高(Voiced)检测,基频 85~300Hz 才响应——不换固件但较凑合
  • 竖直面板正前方向弱:加前后遮挡/改立体阵列增强水平朝向分辨
  • 视觉联动:麦克风阵判定方位 → 二哈 2 视觉细瞄 → 双舵机(甚至六舵机机械臂)锁定目标

安全提醒:舵机用独立 5V 供电 + GND 共地,别用 K210 逻辑电(两舵一起转电流瞬时可达 1A+);上电前把舵机手动回中位。所用引脚/标定锚点均为本机实测示例,请按你的安装与麦克风朝向微调。


作者:云天
链接:https://www.aiddithome.com/p/9c5b402dbc27a.html
本作遵循 [CC BY 4.0 署名] 许可,版权归作者所有。若存在违规行为,您可以点击 “举报”。


登录 后发表评论
3条评论
触底反弹的Tony
1楼 · 19小时前

防自激那个1.2s锁定太关键了,不然舵机一转身自己的电机声又触发转头,越转越响,这个坑踩过的人才懂

阿伟不emo
2楼 · 19小时前

用麦克风阵列做听声转头这个思路很妙,12分区再加加权重心确实比单看峰值bin稳,峰值bin那种乱跳的坑我复制过,还是这个算法扛噪

老张装机
3楼 · 19小时前

最坑的还是固件,普通包根本没有mic模块,必须v0.6.2完整版才有MIC_ARRAY,不看这篇光配个环境就得装错好几回