声源定位·听声转头平台:Maixduino + 6+1麦克风阵列 + 双舵机云台
用一只 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,不是那排 A0A5 丝印(A0A5 是 ESP32 的脚,不是 K210 的!)。本机实测有效配置:
MIC_D0 → GPIO24 MIC_D1 → GPIO22 MIC_D2 → GPIO15 MIC_D3 → GPIO32
MIC_WS → GPIO21 MIC_BCK→ GPIO23 LED_DAT → GPIO12 LED_CLK → GPIO11
舵机: pan = GPIO13 (全0~180) tilt = GPIO14 (只用90~180)



四、系统方案与原理
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。
六、调试实录与踩坑
- 固件里根本没有 mic 模块(最坑):v0.6.0 的普通/精简/with_lvgl 全系固件都没编声源定位的
mic_array。必须用 v0.6.2 标准完整版固件才有MIC_ARRAY,也才支持 MaixPy IDE。 - 模块名是大写的:是
from Maix import MIC_ARRAY as mic,不是小写from maix import mic_array。写小写就是ImportError: no module named 'maix'。 - A0~A5 丝印在 ESP32 上,不在 K210 上:最初的迷茫全来自这里——那排 A 引脚是 ESP32 的地盘,外接麦克风阵列根本不该往那接,要直连 K210 的 GPIO(本机 24/22/15/32/21/23、LED 12/11)。
- init 一直返回 True、方向却全 0:
mic.init(...)只是"逻辑上把 GPIO 配成 I2S",不检查物理上是否真接了对的脚。接错脚时 init 照样"成功"、get_map 也有图,但get_dir恒 0。判断"接没接对"要看数据是不是随声源变,不能只看 init。 - 舵机信号没路由到脚上一个隐藏坑:
machine.PWM(ch, freq=50, duty=0)创建后舵机不动——因为没传pin=,PWM 信号去了默认脚。正确是PWM(0, freq=50, duty=0, pin=13),pin=参数把通道路由到目标 GPIO(实测打印[MAIXPY]PWM: channel=0, freq=50, duty=5.00)。duty 量程是 0~100 百分比。 - 没接电测舵机会误判"死区":一度以为俯仰有死区、0~180 不到底,其实是不小心没给舵机供电在测。接电后 pan/tilt 分别用通道 0/1 全行程扫描都顺畅——先确认供电再做死区结论。
- 死区误判的教训:怀疑某个舵机"有死区/不转"时,用"好用的通道去驱那个引脚、再用另一通道去驱好用的引脚"做交叉隔离,别急着改代码。
七、实测效果与数据
- 方位角:同一方向连续测,峰值 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视觉细瞄 → 双舵机(甚至六舵机机械臂)锁定目标,参考本系列另一篇《二哈2 视觉跟随机器人》。
安全提醒:舵机用独立 5V 供电 + GND 共地,别用 K210 逻辑电(两舵一起转电流瞬时可达 1A+);上电前把舵机手动回中位。所用引脚/标定锚点均为本机实测示例,请按你的安装与麦克风朝向微调。



