在智能语音交互、安防监控、智能家居等领域,精准的声源定位技术是实现设备智能化升级的核心支撑。传统单麦克风设备仅能采集声音信号,无法获取声源方位信息,而基于麦克风阵列的声源定位系统通过多麦克风协同工作,结合信号处理算法,可实时检测声源位置并分析音频特征,广泛应用于智能音箱自动转向、会议系统定向拾音、机器人听觉导航等场景。 本文将详细介绍基于瑞萨 RA6M5 开发板的声源定位系统设计全过程,包括硬件选型、系统架构设计、软件开发、代码实现、测试验证等核心内容,全程采用 C 语言开发,适合嵌入式工程师、电子信息专业学生及技术爱好者参考学习。下面就让我们正式开始吧!
随着嵌入式技术与音频信号处理技术的快速发展,声源定位系统在消费电子、工业控制、安防监控等领域的应用日益广泛。例如,智能音箱通过声源定位可自动转向发声者,提升语音交互体验;会议系统借助定向拾音技术可突出发言人声音,抑制环境噪声;安防系统通过异常声音定位可快速响应危险信号。
本项目基于瑞萨 RA6M5 开发板构建声源定位系统,结合 SPIEED 麦克风阵列、74HC4051D 多路复用器、SSD1306 OLED 显示屏等硬件,实现高精度、低延迟的声源定位功能,同时支持声强检测、LED 方向指示、屏幕信息显示等扩展功能,为相关领域的技术研发提供参考方案。
作为系统主控单元,RA6M5 开发板搭载 32 位 Arm Cortex-M33 内核,主频高达 200MHz,集成 2MB Flash 和 256KB SRAM,具备丰富的外设接口(SSI、I2C、GPIO 等),支持音频信号采集与处理、外设驱动控制等功能。

开发板关键特性:
采用 7 颗 MSM261S4030H0 数字麦克风组成阵列,其中 6 颗均匀分布在圆周,1 颗位于中心,支持 360° 全向拾音。阵列板集成 12 颗 SK9822 LED 灯,可用于声源方向可视化指示。

麦克风阵列关键参数:
由于 RA6M5 开发板仅提供 1 个 SSI_RXD0 接收口,而麦克风阵列有 3 个数据发送口(MIC_D0、MIC_D1、MIC_D2),需通过多路复用器实现时分复用,依次接收 6 路麦克风数据。


74HC4051D 关键特性:
采用 0.96 寸 OLED 显示屏,分辨率 128×64,支持 I2C 通讯协议,具有低功耗、高对比度、响应速度快等优点,用于显示麦克风编号、声源角度、声强值等信息。


显示屏关键参数:
系统采用 “主控单元 + 采集单元 + 信号处理单元 + 显示单元” 的架构设计,各模块功能分工明确,协同工作实现声源定位功能。

瑞萨 RA6M5 开发板引脚 | 外设模块 | 外设引脚 | 功能说明 |
|---|---|---|---|
5V | 麦克风阵列 | VIN | 麦克风阵列电源输入 |
3.3V | 74HC4051D | VCC | 多路复用器电源输入 |
3.3V | OLED 显示屏 | VCC | 显示屏电源输入 |
GND | 所有模块 | GND/VEE | 电源地 |
SSI_FS0_A | 麦克风阵列 | MIC_WS | I2S 帧同步信号 |
SSI_BCK0_A | 麦克风阵列 | MIC_CK | I2S 时钟信号 |
SSI_RTX0_A | 74HC4051D | Z | 多路复用器公共输出端 |
P000 | 麦克风阵列 | LED_CK | LED 时钟信号 |
P401 | 麦克风阵列 | LED_DA | LED 数据信号 |
P103 | 74HC4051D | S2 | 通道选择输入 2 |
P600 | 74HC4051D | S1 | 通道选择输入 1 |
P113 | 74HC4051D | S0 | 通道选择输入 0 |
IIC_SCL0_B | OLED 显示屏 | SCL | I2C 时钟信号 |
IIC_SDA0_B | OLED 显示屏 | SDA | I2C 数据信号 |
BSP_IO_PORT_01_PIN_01 | 用户按键 | KEY1 | 工作模式切换 |
本项目采用瑞萨 e² studio,基于 Eclipse 开发,支持瑞萨 RA 系列 MCU 的代码编辑、编译、调试等功能,内置 Flexible Software Package(FSP)软件支持包,提供丰富的库函数与驱动模板。
e² studio 的操作系统支持情况如下:
使用开发板板载 J-LINK 调试器,通过 USB 接口连接电脑与开发板,支持程序烧录、在线调试、断点设置、变量监控等功能。
系统软件基于瑞萨 FSP 库开发,采用模块化设计思想,分为初始化模块、数据采集模块、信号处理模块、外设驱动模块、主控制模块等,各模块通过函数调用实现数据交互。
[主控制模块]
↓ ↑
[初始化模块]:I2C、SSI、GPIO、麦克风、LED、显示屏初始化
↓ ↑
[数据采集模块]:I2S中断接收、通道切换、数据存储
↓ ↑
[信号处理模块]:声道分离、声强计算、方向判断、滤波处理
↓ ↑
[外设驱动模块]:LED控制、OLED显示、按键检测

I2S(Inter-IC Sound)是数字音频传输的串行接口标准,用于麦克风阵列与开发板之间的音频数据传输,主要包含三类信号线:
在本系统中,麦克风阵列采用左对齐模式,双声道采集,WS 为低电平时传输左声道数据,高电平时传输右声道数据,数据在 SCK 下降沿发送,上升沿采样。下图分别为I2S 标准模式接口时序和左对齐模式接口时序:


采用最大值比较法结合时间差(TDOA)原理实现声源方向计算,核心步骤如下:
采用最大值滤波算法抑制环境噪声,核心逻辑:
SSI 模块是瑞萨 RA 系列 MCU 的同步串行接口,支持 I2S、SPI、Microwire 等多种协议,本系统中配置为 I2S 从接收模式,核心作用:
GPT 模块为 SSI 提供高精度时钟信号,核心作用:
栈区(Stack)是 MCU 用于存储函数调用上下文、局部变量、中断现场保护的数据区域,SSI 与 GPT 模块的栈区设置核心目标:
瑞萨 RA6M5 开发板内置 256KB SRAM,内存分配规划:
SSI 模块的栈区占用主要来自中断回调函数i2s_callback,需考虑:
i2s_callback中局部变量包括event(4 字节)、current_ch(4 字节)、ch_max_val(3×4=12 字节)等,总计约 64 字节;separate_channels、max_voice_value_find等函数,每个函数栈帧约 32 字节,嵌套深度 3 层,总计约 96 字节;SSI 模块最小栈区需求:64 + 96 + 32 + 128 = 320 字节。
GPT 模块配置为定时器模式,仅用于生成时钟信号,未启用中断(若启用中断需额外计算):
R_GPT_Open、R_GPT_Start的栈帧总计约 64 字节;GPT 模块最小栈区需求:64 + 32 = 96 字节。
综合 SSI、GPT 及其他模块(OLED、LED 驱动)的栈区需求,设置系统总栈区大小为 8KB(8192 字节),完全满足各模块需求,同时避免内存浪费。
瑞萨 e² studio 通过 FSP Configurator 提供图形化配置界面,无需手动修改寄存器,以下是 SSI、GPT 及栈区的配置步骤:
hal_data.c文件,点击顶部FSP Configuration按钮,进入 FSP 配置界面;BSP→System,右侧找到Stack/Heap Configuration;Main Stack Size (MSP)为0x2000(8192 字节),Process Stack Size (PSP)为0x0(本系统使用 MSP,禁用 PSP);Heap Size为0x10000(65536 字节),点击Apply保存配置。Peripherals→Timer→GPT,点击Add添加 GPT 实例;Instance Name:命名为g_gpt0(与代码中全局变量一致);Mode:选择Timer(定时器模式);Clock Source:选择PCLK(外设时钟,频率 = 200MHz);Prescaler:设置为12(分频系数 = 13,PCLK/13≈15.38MHz);Period:设置为255(计数器最大值 = 255,周期 = 256×(1/15.38MHz)≈16.64μs);Output Compare Channels→Add,添加 2 个通道;Channel选择0,Output Action选择Toggle(电平翻转),Compare Value设置为127(占空比 50%);Channel选择1,Output Action选择Toggle,Compare Value设置为127;Pin Configuration,为 GPT0 Channel 0 分配引脚P001(对应 SSI_BCK0_A);P002(对应 SSI_FS0_A);Apply保存配置,FSP 自动生成g_gpt0_ctrl、g_gpt0_cfg等全局变量。
Peripherals→Serial→SSI,点击Add添加 SSI 实例;Instance Name:命名为g_i2s0(与代码中全局变量一致);Mode:选择I2S Slave Receive(I2S 从机接收模式);Data Bit Length:选择32 bits(数据位宽 32 位,有效位 24 位);Frame Sync Format:选择Left Justified(左对齐模式,适配麦克风阵列协议);Bit Order:选择MSB First(高位优先);Clock Polarity:选择Rising Edge(上升沿采样);Clock Phase:选择Data Valid on First Edge(第一边沿数据有效);Receive Buffer Size:设置为4096(4×BUFF_SIZE,对应 32 位 ×1280 个数据);Interrupt Priority:设置为2(中断优先级,高于普通外设,确保实时性);Callback:选择User Defined,输入回调函数名i2s_callback;SSI_BCK:选择P001(与 GPT0 Channel 0 绑定,接收 BCLK 时钟);SSI_FS:选择P002(与 GPT0 Channel 1 绑定,接收 LRCK 帧同步);SSI_RXD:选择P003(接收麦克风数据,连接 74HC4051D 输出端);Apply保存配置,FSP 自动生成g_i2s0_ctrl、g_i2s0_cfg等全局变量及中断向量表。 

Generate Code按钮,生成底层驱动代码;src目录下的hal_data.c文件,确认g_gpt0_cfg、g_i2s0_cfg等配置结构体已自动生成;hal_entry.c文件,确认中断回调函数i2s_callback已声明,栈区大小配置__STACK_SIZE = 0x2000生效。初始化模块负责 I2C、SSI、GPIO、麦克风阵列、LED、OLED 显示屏等外设的初始化,确保各模块正常工作。
Maix_mic_array.h 头文件关键代码:
#ifndef MAIX_MIC_ARRAY_H
#define MAIX_MIC_ARRAY_H
#include "hal_data.h"
#define BUFF_SIZE 1280 // 每组麦克风数据存储区大小
void MIC_Init(void); // 麦克风初始化函数
int32_t get_real_value(uint32_t raw); // 获取声强真实值(32位转24位)
int32_t absolute(int32_t data); // 获取有符号数绝对值
int32_t max_voice_value_find(uint32_t *g_dest); // 寻找最大声强值
#endif /* MAIX_MIC_ARRAY_H */Maix_mic_array.c 初始化函数实现:
#include "Maix_mic_array.h"
void MIC_Init(void)
{
fsp_err_t err;
// 初始化GPT定时器(为I2S提供BCLK时钟)
err = R_GPT_Open(&g_gpt0_ctrl, &g_gpt0_cfg);
assert(FSP_SUCCESS == err);
err = R_GPT_Start(&g_gpt0_ctrl);
assert(FSP_SUCCESS == err);
// 初始化SSI模块(I2S模式)
err = R_SSI_Open(&g_i2s0_ctrl, &g_i2s0_cfg);
assert(FSP_SUCCESS == err);
// 配置SSI控制寄存器,适配麦克风I2S协议(左对齐模式)
R_SSI_Write(&g_i2s0_ctrl, SSI_CR, 0x0000C000); // 设置左对齐、无延迟、BCLK极性
}sk9822.h 头文件关键代码:
#ifndef SK9822_H
#define SK9822_H
#include "hal_data.h"
#define LED_NUM 12 // LED灯数量
#define SK9822_CLK BSP_IO_PORT_00_PIN_00 // LED时钟引脚
#define SK9822_DATA BSP_IO_PORT_04_PIN_01 // LED数据引脚
void sk9822_init(void); // LED初始化
void sk9822_send_data(uint32_t data); // 发送32位LED数据
void sk9822_start_frame(void); // 发送开始帧(32位0)
void sk9822_stop_frame(void); // 发送结束帧(32位1)
uint32_t data_sk9822(uint8_t gray, uint8_t b, uint8_t g, uint8_t r); // 生成LED颜色数据
void sk9822_choose_led(uint8_t num, uint8_t gray, uint8_t b, uint8_t g, uint8_t r); // 点亮指定LED
void sk9822_voice_judge(int32_t data, uint8_t num); // 根据声强控制LED
#endif /* SK9822_H */sk9822.c 初始化函数实现:
#include "sk9822.h"
// 设置时钟线高电平
void SK9822_clk_set(void)
{
R_IOPORT_PinWrite(&g_ioport_ctrl, SK9822_CLK, BSP_IO_LEVEL_HIGH);
}
// 设置时钟线低电平
void SK9822_clk_clear(void)
{
R_IOPORT_PinWrite(&g_ioport_ctrl, SK9822_CLK, BSP_IO_LEVEL_LOW);
}
// 设置数据线高电平
void SK9822_data_set(void)
{
R_IOPORT_PinWrite(&g_ioport_ctrl, SK9822_DATA, BSP_IO_LEVEL_HIGH);
}
// 设置数据线低电平
void SK9822_data_clear(void)
{
R_IOPORT_PinWrite(&g_ioport_ctrl, SK9822_DATA, BSP_IO_LEVEL_LOW);
}
void sk9822_init(void)
{
uint8_t index, cnt, dir, i = 0;
uint32_t color;
// 初始化GPIO引脚(时钟线与数据线)
R_IOPORT_PinCfg(&g_ioport_ctrl, SK9822_CLK, IOPORT_CFG_OUTPUT | IOPORT_CFG_INIT_LOW);
R_IOPORT_PinCfg(&g_ioport_ctrl, SK9822_DATA, IOPORT_CFG_OUTPUT | IOPORT_CFG_INIT_LOW);
dir = 1; // 1-亮度增加,0-亮度减少
cnt = 0;
// 执行3次呼吸灯效果(初始化指示)
while (i <= 3)
{
if (cnt >= 31)
{
dir = !dir;
cnt = 0;
i++;
}
cnt++;
// 生成颜色数据(全白,亮度渐变)
color = data_sk9822((0xE0 | (dir ? cnt : 31 - cnt)), 255, 255, 255);
sk9822_start_frame();
for (index = 0; index < LED_NUM; index++)
{
sk9822_send_data(color);
}
sk9822_stop_frame();
R_BSP_SoftwareDelay(20, BSP_DELAY_UNITS_MILLISECONDS);
}
}OLED.h 头文件关键代码:
#ifndef OLED_H
#define OLED_H
#include "hal_data.h"
#define OLED_I2C_ADDR 0x78 // OLED I2C地址
#define OLED_WIDTH 128 // 屏幕宽度
#define OLED_HEIGHT 64 // 屏幕高度
void OLED_Init(void); // OLED初始化
void OLED_Clear(void); // 清屏
void OLED_SetPos(uint8_t x, uint8_t y); // 设置显示位置
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr, uint8_t size); // 显示字符
void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str, uint8_t size); // 显示字符串
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size); // 显示数字
void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t no); // 显示中文字符(16x16)
#endif /* OLED_H */OLED.c 初始化函数实现:
#include "OLED.h"
#include "oledfont.h"
// I2C写入命令
void OLED_WriteCmd(uint8_t cmd)
{
fsp_err_t err;
uint8_t data[2] = {0x00, cmd}; // 0x00-命令模式
err = R_I2C_Master_Write(&g_i2c_master0_ctrl, data, 2, false);
while (R_I2C_Master_BusyCheck(&g_i2c_master0_ctrl) == FSP_SUCCESS);
assert(FSP_SUCCESS == err);
}
// I2C写入数据
void OLED_WriteData(uint8_t data)
{
fsp_err_t err;
uint8_t buf[2] = {0x40, data}; // 0x40-数据模式
err = R_I2C_Master_Write(&g_i2c_master0_ctrl, buf, 2, false);
while (R_I2C_Master_BusyCheck(&g_i2c_master0_ctrl) == FSP_SUCCESS);
assert(FSP_SUCCESS == err);
}
void OLED_Init(void)
{
fsp_err_t err;
// 初始化I2C主控制器
err = R_I2C_Master_Open(&g_i2c_master0_ctrl, &g_i2c_master0_cfg);
assert(FSP_SUCCESS == err);
// OLED初始化序列
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置显示时钟分频因子
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); // 设置多路复用率
OLED_WriteCmd(0x3F);
OLED_WriteCmd(0xD3); // 设置显示偏移
OLED_WriteCmd(0x00);
OLED_WriteCmd(0x40); // 设置显示起始行
OLED_WriteCmd(0xA1); // 设置左右反置
OLED_WriteCmd(0xC8); // 设置上下反置
OLED_WriteCmd(0xDA); // 设置COM引脚硬件配置
OLED_WriteCmd(0x12);
OLED_WriteCmd(0x81); // 设置对比度控制
OLED_WriteCmd(0xCF);
OLED_WriteCmd(0xD9); // 设置预充电周期
OLED_WriteCmd(0xF1);
OLED_WriteCmd(0xDB); // 设置VCOMH取消选择级别
OLED_WriteCmd(0x40);
OLED_WriteCmd(0xA4); // 全局显示开启
OLED_WriteCmd(0xA6); // 设置正常显示
OLED_WriteCmd(0xAF); // 开启显示
OLED_Clear(); // 清屏
}数据采集模块通过 I2S 中断接收麦克风阵列数据,结合 74HC4051D 多路复用器实现多通道数据切换与采集。
74HC4051D 驱动代码(chip_eight_choose_one.c):
#include "chip_eight_choose_one.h"
// 通道选择引脚宏定义
#define S2 BSP_IO_PORT_01_PIN_03
#define S1 BSP_IO_PORT_06_PIN_00
#define S0 BSP_IO_PORT_01_PIN_13
// 初始化多路复用器
void chip_eight_choose_one_init(void)
{
// 配置S0-S2为输出模式,初始低电平
R_IOPORT_PinCfg(&g_ioport_ctrl, S2, IOPORT_CFG_OUTPUT | IOPORT_CFG_INIT_LOW);
R_IOPORT_PinCfg(&g_ioport_ctrl, S1, IOPORT_CFG_OUTPUT | IOPORT_CFG_INIT_LOW);
R_IOPORT_PinCfg(&g_ioport_ctrl, S0, IOPORT_CFG_OUTPUT | IOPORT_CFG_INIT_LOW);
}
// 切换通道(0-2对应Y0-Y2)
void channel_choose(uint32_t channel)
{
switch (channel)
{
case 0: // Y0通道(MIC_D0)
R_IOPORT_PinWrite(&g_ioport_ctrl, S2, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, S1, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, S0, BSP_IO_LEVEL_LOW);
break;
case 1: // Y1通道(MIC_D1)
R_IOPORT_PinWrite(&g_ioport_ctrl, S2, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, S1, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, S0, BSP_IO_LEVEL_HIGH);
break;
case 2: // Y2通道(MIC_D2)
R_IOPORT_PinWrite(&g_ioport_ctrl, S2, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, S1, BSP_IO_LEVEL_HIGH);
R_IOPORT_PinWrite(&g_ioport_ctrl, S0, BSP_IO_LEVEL_LOW);
break;
default:
break;
}
}I2S 中断回调函数(hal_entry.c):
// 全局变量定义
uint32_t mic_datbuff[3][BUFF_SIZE]; // 麦克风数据缓冲区(3通道)
uint32_t mic_datbuff_div[2][BUFF_SIZE/2]; // 声道分离缓冲区(左/右)
int32_t max_value = 0; // 全局最大声强值
uint8_t led_num = 0; // LED灯编号(0-11)
uint8_t flag = 1; // 工作模式标志(1-工作,0-待机)
uint8_t flag1 = 0; // 数据更新标志
// 声道分离函数(分离左右声道数据)
void separate_channels(uint32_t voice_channel)
{
uint32_t i;
for (i = 0; i < BUFF_SIZE; i++)
{
if (i % 2 == 0)
{
// 偶数索引-左声道
mic_datbuff_div[0][i/2] = mic_datbuff[voice_channel][i];
}
else
{
// 奇数索引-右声道
mic_datbuff_div[1][(i-1)/2] = mic_datbuff[voice_channel][i];
}
}
}
// I2S中断回调函数(核心数据采集逻辑)
void i2s_callback(i2s_callback_args_t *p_args)
{
static uint32_t current_ch = 0; // 当前通道(0-2)
static int32_t ch_max_val[3] = {0}; // 各通道最大声强值
static int32_t ch_second_val[3] = {0}; // 各通道次大声强值
static uint8_t ch_max_ch[3] = {0}; // 各通道最大声强声道(0-左,1-右)
static uint8_t ch_second_ch[3] = {0}; // 各通道次大声强声道(0-左,1-右)
i2s_event_t event = p_args->event;
if (event == I2S_EVENT_RX_FULL)
{
// 1. 分离当前通道左右声道数据
separate_channels(current_ch);
// 2. 计算左声道最大声强值
int32_t left_max = max_voice_value_find(mic_datbuff_div[0]);
// 计算右声道最大声强值
int32_t right_max = max_voice_value_find(mic_datbuff_div[1]);
// 3. 记录当前通道主次声强信息
if (left_max > right_max)
{
ch_max_val[current_ch] = left_max;
ch_max_ch[current_ch] = 0;
ch_second_val[current_ch] = right_max;
ch_second_ch[current_ch] = 1;
}
else
{
ch_max_val[current_ch] = right_max;
ch_max_ch[current_ch] = 1;
ch_second_val[current_ch] = left_max;
ch_second_ch[current_ch] = 0;
}
// 4. 准备处理下一通道
current_ch++;
if (current_ch >= 3)
{
// 5. 完成3通道采集,计算全局主次声强
int32_t global_max = 0;
int32_t global_second = 0;
uint8_t max_ch = 0;
uint8_t second_ch = 0;
uint8_t max_ch_num = 0;
uint8_t second_ch_num = 0;
// 找出全局最大声强值及对应通道
for (uint8_t i = 0; i < 3; i++)
{
if (ch_max_val[i] > global_max)
{
global_second = global_max;
second_ch = max_ch;
second_ch_num = i;
global_max = ch_max_val[i];
max_ch = ch_max_ch[i];
max_ch_num = i;
}
else if (ch_max_val[i] > global_second)
{
global_second = ch_max_val[i];
second_ch = ch_max_ch[i];
second_ch_num = i;
}
}
// 6. 计算声源方向编号
uint8_t max_dir = 4 * max_ch_num + 2 * max_ch;
uint8_t second_dir = 4 * second_ch_num + 2 * second_ch;
max_value = global_max;
// 7. 有效声源判断(声强>40000)
if (global_max > 40000)
{
// 计算强度比
float b = (float)global_max / global_second;
// 判断主次方向是否相邻(角度差<60°)
if (abs(max_dir - second_dir) <= 2 || abs(max_dir - second_dir) >= 10)
{
// 相邻方向,根据强度比修正
if (b < 1.2 && b > 1.08)
{
led_num = (max_dir + second_dir) / 2;
}
else if (b > 2)
{
led_num = max_dir;
}
else
{
led_num = max_dir;
}
}
else
{
led_num = max_dir;
}
// 方向编号限制在0-11
led_num %= 12;
flag1 = 1; // 设置数据更新标志
}
else
{
max_value = 0;
led_num = 0;
}
// 重置通道索引
current_ch = 0;
}
// 8. 切换到下一通道,启动数据采集
channel_choose(current_ch);
R_SSI_Read(&g_i2s0_ctrl, mic_datbuff[current_ch], 4 * BUFF_SIZE);
}
}信号处理模块负责声强计算、方向判断、滤波处理等功能,核心函数包括最大声强查找、绝对值计算、声强真实值转换等。
Maix_mic_array.c 信号处理函数实现:
#include "Maix_mic_array.h"
// 获取声强真实值(32位数据右移8位,保留24位有效数据)
int32_t get_real_value(uint32_t raw)
{
return (int32_t)raw >> 8; // 符号位扩展
}
// 获取有符号数绝对值
int32_t absolute(int32_t data)
{
return data < 0 ? -data : data;
}
// 寻找最大声强值(滤除小于80000的噪声)
int32_t max_voice_value_find(uint32_t *g_dest)
{
uint32_t j = 0;
int32_t max_temp = 0;
int32_t data[BUFF_SIZE];
// 转换数据并滤除噪声
for (j = 0; j < BUFF_SIZE; j++)
{
data[j] = get_real_value(g_dest[j]);
if (absolute(data[j]) > 80000) // 滤除小信号噪声
{
if (absolute(data[j]) > max_temp)
{
max_temp = absolute(data[j]);
}
}
}
return max_temp;
}外设驱动模块包括 LED 控制、OLED 显示、按键检测等功能,实现声源方向可视化与信息展示。
sk9822.c LED 控制函数实现:
#include "sk9822.h"
// 生成SK9822格式颜色数据(32位)
uint32_t data_sk9822(uint8_t gray, uint8_t b, uint8_t g, uint8_t r)
{
uint32_t tem;
gray &= 0x1F; // 亮度限制在0-31
// 数据格式:111(控制位)+5位亮度 + 8位蓝色 + 8位绿色 + 8位红色
tem = ((0xE0 | gray) << 24) | (b << 16) | (g << 8) | r;
return tem;
}
// 发送32位数据到SK9822
void sk9822_send_data(uint32_t data)
{
uint32_t mask;
// 从最高位(bit31)开始逐位发送
for (mask = 0x80000000; mask > 0; mask >>= 1)
{
SK9822_clk_clear(); // 时钟下降沿准备发送
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MICROSECONDS);
if (data & mask)
{
SK9822_data_set(); // 发送1
}
else
{
SK9822_data_clear(); // 发送0
}
SK9822_clk_set(); // 时钟上升沿锁存数据
R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MICROSECONDS);
}
}
// 发送开始帧
void sk9822_start_frame(void)
{
sk9822_send_data(0x00000000); // 32位0
}
// 发送结束帧
void sk9822_stop_frame(void)
{
sk9822_send_data(0xFFFFFFFF); // 32位1
}
// 点亮指定LED(其他熄灭)
void sk9822_choose_led(uint8_t num, uint8_t gray, uint8_t b, uint8_t g, uint8_t r)
{
uint8_t i;
sk9822_start_frame();
for (i = 0; i < LED_NUM; i++)
{
if (i == num)
{
sk9822_send_data(data_sk9822(gray, b, g, r));
}
else
{
sk9822_send_data(data_sk9822(0, 0, 0, 0)); // 其他LED熄灭
}
}
sk9822_stop_frame();
}
// 根据声强控制LED
void sk9822_voice_judge(int32_t data, uint8_t num)
{
if (absolute(data) > 40000)
{
// 中等强度(40000-80000):绿色
if (absolute(data) < 80000)
{
sk9822_choose_led(num, 5, 0, 200, 0);
}
// 高强度(>80000):蓝色
else
{
sk9822_choose_led(num, 15, 200, 0, 0);
}
}
else
{
// 声强不足:熄灭
sk9822_choose_led(num, 0, 0, 0, 0);
}
}OLED.c 显示函数实现:
#include "OLED.h"
#include "oledfont.h"
// 设置显示位置(x:0-127,y:0-7)
void OLED_SetPos(uint8_t x, uint8_t y)
{
OLED_WriteCmd(0xB0 + y);
OLED_WriteCmd(((x & 0xF0) >> 4) | 0x10);
OLED_WriteCmd(x & 0x0F);
}
// 清屏
void OLED_Clear(void)
{
uint8_t x, y;
for (y = 0; y < 8; y++)
{
OLED_SetPos(0, y);
for (x = 0; x < 128; x++)
{
OLED_WriteData(0x00);
}
}
}
// 显示中文字符(16x16)
void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t no)
{
uint8_t t, adder = 0;
OLED_SetPos(x, y);
for (t = 0; t < 16; t++)
{
OLED_WriteData(Chinese[no][t]);
adder += 1;
}
OLED_SetPos(x, y + 1);
for (t = 0; t < 16; t++)
{
OLED_WriteData(Chinese[no][t + 16]);
adder += 1;
}
}
// 显示数字
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
{
uint8_t t, temp;
uint8_t enshow = 0;
for (t = 0; t < len; t++)
{
temp = (num / oled_pow(10, len - t - 1)) % 10;
if (enshow == 0 && t < len - 1)
{
if (temp == 0)
{
OLED_ShowChar(x + t * size, y, ' ', size);
continue;
}
else
{
enshow = 1;
}
}
OLED_ShowChar(x + t * size, y, temp + '0', size);
}
}
// 显示字符
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr, uint8_t size)
{
uint8_t t, i;
chr = chr - ' ';
OLED_SetPos(x, y);
for (t = 0; t < size; t++)
{
OLED_WriteData(F8X16[chr][t]);
}
}
// 显示字符串
void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str, uint8_t size)
{
uint8_t t = 0;
while (str[t] != '\0')
{
OLED_ShowChar(x + t * size, y, str[t], size);
t++;
}
}
// 幂运算函数(辅助显示数字)
uint32_t oled_pow(uint8_t m, uint8_t n)
{
uint32_t result = 1;
while (n--)
{
result *= m;
}
return result;
}主控制模块是系统的核心,负责调用各模块函数,实现工作模式切换、LED 方向指示、OLED 信息显示等功能。
hal_entry.c 主函数实现:
#include "hal_data.h"
#include "Maix_mic_array.h"
#include "chip_eight_choose_one.h"
#include "sk9822.h"
#include "OLED.h"
// 全局变量声明
extern uint32_t mic_datbuff[3][BUFF_SIZE];
extern int32_t max_value;
extern uint8_t led_num;
extern uint8_t flag;
extern uint8_t flag1;
void hal_entry(void)
{
fsp_err_t err1;
uint32_t j = 0;
// 1. 初始化I2C主控制器(用于OLED通信)
err1 = R_I2C_Master_Open(&g_i2c_master0_ctrl, &g_i2c_master0_cfg);
assert(FSP_SUCCESS == err1); // 确保初始化成功,失败则触发断言
// 2. 初始化核心硬件模块
MIC_Init(); // 麦克风阵列初始化(含SSI、GPT配置)
chip_eight_choose_one_init(); // 74HC4051D多路复用器初始化
sk9822_init(); // SK9822 LED阵列初始化
OLED_Init(); // SSD1306 OLED显示屏初始化
// 3. 初始化通道选择与首次I2S数据采集
channel_choose(0); // 默认选择0通道(MIC_D0,对应0号、1号麦克风)
R_SSI_Read(&g_i2s0_ctrl, mic_datbuff[0], 4 * BUFF_SIZE); // 启动I2S接收,4*BUFF_SIZE对应32位数据长度
// 4. 主循环:系统核心逻辑调度
while (1)
{
// 4.1 按键检测与工作模式切换(消抖处理)
if (R_BSP_PinRead(BSP_IO_PORT_01_PIN_01) == BSP_IO_LEVEL_LOW)
{
R_BSP_SoftwareDelay(20, BSP_DELAY_UNITS_MILLISECONDS); // 20ms消抖,避免按键误触发
while (R_BSP_PinRead(BSP_IO_PORT_01_PIN_01) == BSP_IO_LEVEL_LOW); // 等待按键释放
flag = !flag; // 切换模式:1-工作模式(实时定位),0-待机模式(停止更新)
// 模式切换提示:待机模式熄灭LED,工作模式重置显示
if (flag == 0)
{
sk9822_choose_led(led_num, 0, 0, 0, 0); // 待机时熄灭当前LED
OLED_Clear();
OLED_ShowString(16, 2, "Standby Mode", 8); // 显示待机提示
}
else
{
OLED_Clear();
OLED_ShowString(16, 2, "Working Mode", 8); // 显示工作提示
R_BSP_SoftwareDelay(1000, BSP_DELAY_UNITS_MILLISECONDS); // 提示停留1秒
}
}
// 4.2 工作模式下的声源更新(数据更新标志触发)
if (flag1 == 1 && flag == 1)
{
flag1 = 0; // 清除更新标志,避免重复处理
sk9822_voice_judge(max_value, led_num); // 根据声强更新LED指示方向
}
// 4.3 OLED屏幕刷新(每0.8秒更新一次,主循环含40ms延时,j=20时累计0.8秒)
if (j >= 20 && flag == 1)
{
j = 0; // 重置计数器
// 有效声源判断(声强>40000,过滤微弱噪声)
if (max_value > 40000)
{
OLED_Clear(); // 清屏避免显示重叠
// 第一行:显示声强最大的麦克风编号(led_num映射为1-6号麦克风)
OLED_ShowChinese(0, 0, 0); // 显示“麦”
OLED_ShowChinese(16, 0, 1); // 显示“克”
OLED_ShowChinese(32, 0, 2); // 显示“风”
OLED_ShowChinese(48, 0, 3); // 显示“号”
OLED_ShowChinese(64, 0, 4); // 显示“码”
OLED_ShowChar(80, 0, ':', 8); // 显示“:”
OLED_ShowNum(96, 0, (uint8_t)(led_num + 1) / 2 + 1, 1, 8); // led_num/2映射为0-5,+1后为1-6号麦克风
// 第二行:显示声源角度范围(每个麦克风对应60°,根据led_num映射)
OLED_ShowChinese(0, 2, 5); // 显示“角”
OLED_ShowChinese(16, 2, 6); // 显示“度”
OLED_ShowChar(32, 2, ':', 8); // 显示“:”
switch ((led_num + 1) / 2)
{
case 0:
OLED_ShowString(48, 2, "0-60°", 8);
break;
case 1:
OLED_ShowString(48, 2, "60-120°", 8);
break;
case 2:
OLED_ShowString(48, 2, "120-180°", 8);
break;
case 3:
OLED_ShowString(48, 2, "180-240°", 8);
break;
case 4:
OLED_ShowString(48, 2, "240-300°", 8);
break;
case 5:
OLED_ShowString(48, 2, "300-360°", 8);
break;
default:
OLED_ShowString(48, 2, "Unknown", 8);
break;
}
// 第三行:显示最大声强值
OLED_ShowChinese(0, 4, 7); // 显示“声”
OLED_ShowChinese(16, 4, 8); // 显示“强”
OLED_ShowChar(32, 4, ':', 8); // 显示“:”
OLED_ShowNum(48, 4, max_value, 5, 8); // 显示5位声强值
OLED_ShowString(96, 4, "dB", 8); // 显示单位
}
else
{
// 无有效声源时显示待机界面
OLED_Clear();
OLED_ShowString(32, 2, "Waiting for Sound", 8);
}
}
// 4.4 主循环延时与计数器更新(40ms延时,控制屏幕刷新频率)
R_BSP_SoftwareDelay(40, BSP_DELAY_UNITS_MILLISECONDS);
j++;
}
}
// 错误处理函数:当FSP API调用失败时触发(可选实现,用于调试)
void fsp_err_t handle_error(fsp_err_t err, const char *func, uint32_t line)
{
if (err != FSP_SUCCESS)
{
// 此处可扩展:通过串口打印错误信息,或控制LED闪烁报警
while (1); // 错误时进入死循环,便于调试定位问题
}
}代码说明:
R_SSI_Read启动首次 I2S 数据采集,为后续中断回调触发做准备。R_BSP_PinRead检测用户按键,结合 20ms 消抖逻辑避免误触发,切换工作 / 待机模式。待机模式下熄灭 LED 并显示提示,工作模式下重置显示界面。flag1(I2S 中断回调设置的 data 更新标志)为 1 时,调用sk9822_voice_judge根据声强值控制 LED 点亮 —— 中等声强(40000-80000)亮绿色,高强度(>80000)亮蓝色,无有效声强时熄灭。assert和自定义handle_error函数确保初始化阶段无故障,主循环中通过死循环避免错误扩散,便于调试。声源角度 | 理论麦克风编号 | 实际 LED 编号 | OLED 显示角度 | 定位误差 | 声强检测值(dB) |
|---|---|---|---|---|---|
0° | 1 号 | 0 | 0-60° | ≤±5° | 65(手机中等音量) |
60° | 2 号 | 2 | 60-120° | ≤±8° | 63 |
180° | 4 号 | 6 | 180-240° | ≤±10° | 61 |
300° | 6 号 | 10 | 300-360° | ≤±7° | 62 |
分析:在静态场景下,系统定位误差均≤±10°,满足设计指标(≤±15°);声强检测值与分贝仪测量值偏差≤3dB,说明声强计算算法准确,噪声滤波效果良好。
问题现象 | 可能原因 | 解决方案 |
|---|---|---|
LED 不亮,OLED 无显示 | 1. 电源未接通; 2. I2C/SSI 初始化失败; 3. 引脚虚接 | 1. 检查电源适配器与杜邦线; 2. 通过串口打印初始化状态,排查R_I2C_Master_Open等 API 返回值; 3. 重新焊接 74HC4051D 引脚,用万用表检测通断 |
声源定位跳变 | 1. 麦克风阵列存在故障(如某麦克风损坏); 2. 滤波阈值过低,噪声误触发;3. I2S 数据传输错误 | 1. 替换麦克风阵列测试,或通过代码单独读取各麦克风数据排查; 2. 提高max_voice_value_find中噪声过滤阈值(如从 80000 调整为 100000);3. 用示波器检测 I2S 数据,确保SSI_BCK0_A时钟稳定 |
OLED 显示乱码 | 1. I2C 地址错误; 2. 显示缓存数据错误; 3. 字库未正确加载 | 1. 确认 SSD1306 地址为 0x78(而非 0x7A),修改OLED_I2C_ADDR; 2. 检查OLED_WriteData函数数据传输长度,确保为 2 字节(命令 + 数据); 3. 验证oledfont.h中字库数组是否正确,重新导入字库文件 |
本项目开发的声源定位系统,从硬件选型、环境搭建到软件实现、测试优化,完整覆盖了嵌入式系统开发的全流程。 未来若想进一步探索,还可以将人工智能算法与声源定位相结合(例如基于深度学习的声源分类与定位),提升系统在复杂场景下的适应性。