> 技术文档 > ESP32移植Openharmony外设篇(10)inmp441麦克风

ESP32移植Openharmony外设篇(10)inmp441麦克风


inmp441麦克风模块

模块简介

INMP441是一款高性能、低功耗的微型电容式MEMS麦克风,采用数字输出,广泛应用于智能手机、平板电脑、智能家居、可穿戴设备等场景。其核心特点包括:

  • 高信噪比(SNR):61 dBA,适合远场和近场语音采集。
  • 数字接口:支持24位I²S输出,可直接连接微控制器或DSP,无需额外编解码器。
  • 低功耗:工作电流仅1.4 mA,适用于电池供电设备。
  • 宽频率响应:60 Hz至15 kHz,覆盖人耳可听范围,音质自然清晰。
  • 小型封装:4.72 mm × 3.76 mm × 1 mm,适合高密度集成。

引脚介绍

INMP441模块的引脚配置如下(不同厂商可能略有差异):

引脚名称

符号

功能描述

电源输入

VDD

供电电压范围1.8V至3.3V,需严格匹配以保障性能。

接地

GND

公共地线,确保电路参考点稳定。

字选择信号

WS

I²S协议中的声道选择信号(左/右),由主机控制时序。

串行时钟

SCK

I²S时钟信号,由主机生成,控制数据传输速率。

数据输出

SD

数字音频数据输出,按I²S协议在时钟边沿传输。

声道选择

L/R

配置麦克风为左声道(低电平)或右声道(高电平),部分模块可能固定此引脚。

这里根据声道选择引脚可以分为单声道采集和双声道采集两种模式:

  • 单声道采集:连接单个INMP441,L/R引脚固定为左声道,仅使用WS信号的左声道周期。
  • 双声道系统:使用两个模块,分别配置为左/右声道,共享SCK和WS信号,通过分时复用SD线传输数据。

工作原理

INMP441基于MEMS电容传感技术:

  1. 声波转换:声波振动导致麦克风膜片与背极板间电容变化。
  2. 信号处理:内置ADC将模拟信号转换为24位数字数据,并通过抗混叠滤波器优化。
  3. 数字输出:通过I²S接口输出数据,主机按协议时序读取。

通信协议(I²S)

INMP441采用标准I²S协议,关键参数如下:

  • 数据格式:24位有符号整数,大端模式传输36。
  • 时序要求
    • WS(LRCLK):周期为64个SCK时钟,低电平传输左声道,高电平传输右声道。
    • SCK(BCLK):主机生成的时钟信号,数据在SCK的上升沿或下降沿被采样(具体取决于配置)。
    • 数据传输:从WS下降沿后的第2个SCK上升沿开始,连续传输24位数据。
  • 主从模式:INMP441为从机,需由微控制器(如STM32、ESP32)作为主机驱动时序

既然模块采用标准的I²S协议通信那就好办了,我们只需要使用ESPIDF官方封装好的I²S库就行!

tips:

这里要注意,我们移植到ESP32上的是ESPIDF V4.3.1,最新版的I²S API有所不同,读者可以根据自己使用的ESPIDF版本修改代码。

这里我们将V4.3.1和最新版的I²S文档都提供给大家:

I2S - ESP32 - — ESP-IDF Programming Guide v4.3.1 documentation

I2S - ESP32 - — ESP-IDF 编程指南 latest 文档

注意事项

  • 电气参数:供电电压需在1.8V–3.3V之间,避免超压损坏模块。
  • 信号完整性
    • SD线下拉:建议在SD引脚接10 kΩ下拉电阻,防止高阻态时误读高电平。
    • 时钟匹配:SCK频率需与主机配置一致,避免数据错位。
  • 声道配置:若使用双麦克风,需通过L/R引脚区分声道,并确保WS信号同步切换。
  • 声学环境:避免强噪声干扰,必要时添加物理隔音或软件滤波。
  • 开发调试
    • 使用示波器验证I²S时序,确保数据对齐。
    • 在代码中处理24位符号扩展,避免数据解析错误。

UDP通信

UDP简介
  1. 无连接通信:UDP在传输数据时不需要建立连接,直接将数据包发送出去。这使得UDP的传输效率比TCP更高,因为省去了建立连接所需的时间和资源。
  2. 不可靠传输:UDP不提供可靠性保证,在传输过程中可能会出现数据包丢失、重复、乱序等问题。然而,由于UDP的无连接特点,应用层可以自行处理这些问题,如通过重传机制来恢复丢失的数据包。
  3. 简单轻量:UDP的协议头部较小,只包含源端口、目的端口、长度、校验和以及数据等字段,这使得UDP的数据包结构相对简单且轻量。此外,UDP不进行流量控制和拥塞控制,进一步减少了协议开销。
  4. 低延迟:UDP不需要等待确认,因此可以实现较低的传输延迟。这使得UDP非常适合实时应用场景,如视频、音频、游戏等。

为什么我们使用UDP传输麦克风采集到的数据流?

  1. 实时性要求高:麦克风声音流属于实时音频数据,对延迟的要求非常高。UDP的低延迟特性使其成为传输实时音频数据的理想选择。相比之下,TCP虽然可靠但传输延迟较高,不适合实时音频传输。
  2. 容错性强:对于麦克风声音流来说,偶尔的数据包丢失或乱序并不会对整体音质造成太大影响。因为人耳对音频数据的容错性较强,即使丢失一些数据包也不会导致音频完全中断或失真。这使得UDP的不可靠传输特性在音频传输中变得可接受。
  3. 资源占用少:UDP的协议开销较小,传输效率高。在资源受限的环境中(如嵌入式设备或移动网络),使用UDP可以减少资源占用并提高传输效率。这对于实时音频传输来说非常重要,因为资源占用少意味着可以更快地处理和传输音频数据。

参考代码

inmp441.c
#include #include \"cmsis_os2.h\"#include \"ohos_run.h\"#include \"esp_system.h\"#include \"nvs_flash.h\"#include \"esp_log.h\"#include \"driver/i2s.h\"#include \"driver/gpio.h\"#define INMP_SD GPIO_NUM_16 // 数据引脚#define INMP_SCK GPIO_NUM_17 // 时钟引脚#define INMP_WS GPIO_NUM_18 // 字选择引脚void I2S_Init(void){ // I2S 配置 i2s_config_t i2s_config = { .mode = I2S_MODE_MASTER | I2S_MODE_RX, // 主模式,接收模式 .sample_rate = 16000, // 采样率 44.1kHz .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 24 位数据 .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道(左声道) .communication_format = I2S_COMM_FORMAT_STAND_I2S, // 标准 I2S 格式 .dma_buf_count = 8, // DMA 缓冲区数量 .dma_buf_len = 1024, // 每个 DMA 缓冲区长度 .use_apll = false,  // 不使用 APLL }; // I2S 引脚配置 i2s_pin_config_t pin_config = { .bck_io_num = INMP_SCK, // 时钟引脚 .ws_io_num = INMP_WS, // 字选择引脚 .data_out_num = -1, // 不使用输出引脚 .data_in_num = INMP_SD, // 数据输入引脚 }; // 初始化 I2S 驱动 i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, &pin_config);}esp_err_t inmp441_read_data(void *read_data_buff, size_t size, size_t *bytes_read){ // 读取 I2S 数据 return i2s_read(I2S_NUM_0, read_data_buff, size, bytes_read, portMAX_DELAY);}void inmp441_data_process(uint8_t *data_buff, uint32_t len, int32_t *real_data){ // 处理读取的数据 for (uint16_t i = 0; i < len; i += 3) { *real_data = (data_buff[i] << 16) | (data_buff[i + 1] << 8) | (data_buff[i + 2]); // 拼接 3 个字节 if (*real_data & 0x00800000) { *real_data |= 0xFF000000; // 如果是负数,补全符号位 } }}void inmp441_free(uint8_t *data_buff){ free(data_buff);}

udp_test.c
/* * Copyright (c) 2022 Hunan OpenValley Digital Industry Development Co., Ltd. * Licensed under the Apache License, Version 2.0 (the \"License\"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an \"AS IS\" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */#include #include \"securec.h\"#include \"cmsis_os2.h\"#include \"ohos_run.h\"#include \"lwip/sockets.h\"#include \"lwip/ip_addr.h\"#include \"wifi_device.h\"#include \"esp_system.h\"#include \"nvs_flash.h\"#include \"esp_log.h\"#include \"driver/gpio.h\"#define OPEN_WIFI_NAME \"test\"#define SERVER_IP \"192.168.31.238\"#define SERVER_PORT 8080#define OD_DELAY_1000 1000#define OD_DELAY_100 100#define RECV_LEN 511#define STACK_SIZE 4096#define PRIORITY 25osThreadId_t wifi_test_id = NULL;/* * Copyright (c) 2022 Hunan OpenValley Digital Industry Development Co., Ltd. * Licensed under the Apache License, Version 2.0 (the \"License\"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an \"AS IS\" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */#define SYS_DELAY_TICKS 200#define TASK_PRIO 25void udp_connect_wifi(int *sock){ struct sockaddr_in client_addr; char recv_data[512] = {0}; int recv_data_len; WifiConnect(OPEN_WIFI_NAME); printf(\"start wifi_test test\\r\\n\"); while (1) { // 创建UDP socket *sock = socket(AF_INET, SOCK_DGRAM, 0); if (*sock < 0) { printf(\"Socket error\\n\"); osDelay(OD_DELAY_100); continue; } // 设置服务器地址结构 memset_s(&client_addr, sizeof(client_addr), 0, sizeof(client_addr)); client_addr.sin_family = AF_INET; client_addr.sin_port = htons(SERVER_PORT); client_addr.sin_addr.s_addr = inet_addr(SERVER_IP); printf(\"try connect to server \" SERVER_IP \":%d\\n\", SERVER_PORT); if (connect(*sock, (struct sockaddr *)&client_addr, sizeof(client_addr)) == -1) { printf(\"Connect error\\n\"); closesocket(*sock); osDelay(OD_DELAY_1000); continue; } else { printf(\"UDP connected! \\n\"); break; } }}void udp_send_mes(int sock, char *message, int message_len){ send(sock, message, message_len, 0);}void udp_close(int sock){ closesocket(sock);}
wifi_connect.c
/* * Copyright (c) 2022 Hunan OpenValley Digital Industry Development Co., Ltd. * Licensed under the Apache License, Version 2.0 (the \"License\"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an \"AS IS\" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */#include #include #include #include \"securec.h\"#include \"cmsis_os2.h\"#include \"ohos_init.h\"#include \"wifi_device.h\"#include \"lwip/ip4_addr.h\"#include \"lwip/netif.h\"#include \"lwip/netifapi.h\"#include \"wifi_error_code.h\"#define DEF_TIMEOUT 15#define ONE_SECOND 1#define SELECT_WLAN_PORT \"wlan0\"#define SELECT_WIFI_SECURITYTYPE WIFI_SEC_TYPE_OPEN#define STD_TIMEZONE_OFFSET (+8)#define OD_DELAY_100 100#define OD_DELAY_200 200static int g_staScanSuccess = 0;static int g_ConnectSuccess = 0;static int ssid_count = 0;static WifiErrorCode wifi_error;static WifiEvent g_wifiEventHandler = {0};static int wifi_sta_init_state = 0;int sock_fd;int addr_length;const int timeZone = 8;static void WiFiInit(void);static void WaitScanResult(void);static int WaitConnectResult(void);static void OnWifiScanStateChangedHandler(int state, int size);static void OnWifiConnectionChangedHandler(int state, WifiLinkedInfo *info);static void OnHotspotStaJoinHandler(StationInfo *info);static void OnHotspotStateChangedHandler(int state);static void OnHotspotStaLeaveHandler(StationInfo *info);void DisableWIFI(void){ DisableWifi();}static void OnHotspotStaJoinHandler(StationInfo *info){ (void)info; printf(\"STA join AP\\n\"); return;}static void OnHotspotStaLeaveHandler(StationInfo *info){ (void)info; printf(\"HotspotStaLeave:info is null.\\n\"); return;}static void OnHotspotStateChangedHandler(int state){ printf(\"HotspotStateChanged:state is %d.\\n\", state); return;}static void WiFiInit(void){ printf(\"\\r\\n\"); g_wifiEventHandler.OnWifiScanStateChanged = OnWifiScanStateChangedHandler; g_wifiEventHandler.OnWifiConnectionChanged = OnWifiConnectionChangedHandler; g_wifiEventHandler.OnHotspotStaJoin = OnHotspotStaJoinHandler; g_wifiEventHandler.OnHotspotStaLeave = OnHotspotStaLeaveHandler; g_wifiEventHandler.OnHotspotStateChanged = OnHotspotStateChangedHandler; wifi_error = RegisterWifiEvent(&g_wifiEventHandler); if (wifi_error != WIFI_SUCCESS) { printf(\"register wifi event fail!\\r\\n\"); } else { printf(\"register wifi event succeed!\\r\\n\"); }}static void OnWifiScanStateChangedHandler(int state, int size){ (void)state; if (size > 0) { ssid_count = size; g_staScanSuccess = 1; } return;}static int result;int WifiConnect(const char *ssid, const char *psk){ WifiScanInfo *info = NULL; unsigned int size = WIFI_SCAN_HOTSPOT_LIMIT; static struct netif *g_lwip_netif = NULL; WifiDeviceConfig select_ap_config = {0}; osDelay(OD_DELAY_200); printf(\"\\r\\n\"); WiFiInit(); if (EnableWifi() != WIFI_SUCCESS) { printf(\"EnableWifi failed, wifi_error = %d\\n\", wifi_error); return -1; } if (IsWifiActive() == 0) { printf(\"Wifi station is not actived.\\n\"); return -1; } info = malloc(sizeof(WifiScanInfo) * WIFI_SCAN_HOTSPOT_LIMIT); if (info == NULL) { printf(\"faild to create wifiscanInfo.\\n\"); return -1; } do { ssid_count = 0; g_staScanSuccess = 0; Scan(); WaitScanResult(); wifi_error = GetScanInfoList(info, &size); } while (g_staScanSuccess != 1); strcpy_s(select_ap_config.ssid, sizeof(select_ap_config.ssid), ssid); printf(\"[%s][%s] \\r\\n\", select_ap_config.ssid, select_ap_config.preSharedKey); select_ap_config.securityType = SELECT_WIFI_SECURITYTYPE; if (AddDeviceConfig(&select_ap_config, &result) == WIFI_SUCCESS) { if (ConnectTo(result) == WIFI_SUCCESS && WaitConnectResult() == 1) { printf(\"WiFi connect succeed!\\r\\n\"); wifi_sta_init_state = 1; } } osDelay(OD_DELAY_100); return 0;}static int WaitConnectResult(void){ int ConnectTimeout = DEF_TIMEOUT; while (ConnectTimeout > 0) { sleep(1); ConnectTimeout--; if (g_ConnectSuccess == 1) { printf(\"WaitConnectResult:wait success[%d]s\\n\", (DEF_TIMEOUT - ConnectTimeout)); break; } } if (ConnectTimeout  0) { g_ConnectSuccess = 1; printf(\"callback function for wifi connect\\r\\n\"); } else { g_ConnectSuccess = 0; printf(\"connect wifi_error, please check password, state:%d, try connect again\\r\\n\", state); esp_wifi_connect(); } return;}static void WaitScanResult(void){ int scanTimeout = DEF_TIMEOUT; while (scanTimeout > 0) { sleep(ONE_SECOND); scanTimeout--; if (g_staScanSuccess == 1) { printf(\"WaitScanResult:wait success[%d]s\\n\", (DEF_TIMEOUT - scanTimeout)); break; } } if (scanTimeout <= 0) { printf(\"WaitScanResult:timeout!\\n\"); }}
main.c
#include #include \"cmsis_os2.h\"#include \"ohos_run.h\"#include \"esp_system.h\"#include \"nvs_flash.h\"#include \"esp_log.h\"#include \"esp_rom_sys.h\"// 每次读取20ms的数据#define DATA_LEN 1024 // 1500-28(IP/UDP头)int socket = -1;void LLM_Init(){ I2S_Init(); udp_connect_wifi(&socket);}void LLM_Test(){ uint8_t *read_data_buff = (uint8_t *)malloc(sizeof(uint8_t) * DATA_LEN); size_t len; uint16_t packet_counter = 0; LLM_Init(); while (1) { int ret = inmp441_read_data(read_data_buff, DATA_LEN, &len); if (ret == ESP_OK && len > 0) { // 发送音频数据 udp_send_mes(socket, read_data_buff, len); } } udp_close();}void LLM_Task(void){ osThreadAttr_t attr; attr.name = \"llm_task\"; attr.attr_bits = 0U; attr.cb_mem = NULL; attr.cb_size = 0U; attr.stack_mem = NULL; attr.stack_size = 4096; attr.priority = osPriorityNormal; if (osThreadNew(LLM_Test, NULL, &attr) == NULL) { printf(\"[Inmp441Test] Failed to create LLM_Test!\\n\"); }}OHOS_APP_RUN(LLM_Task);

BUILD.gn

# Copyright (c) 2022 Hunan OpenValley Digital Industry Development Co., Ltd.# Licensed under the Apache License, Version 2.0 (the \"License\");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an \"AS IS\" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.import(\"//kernel/liteos_m/liteos.gni\")module_name = get_path_info(rebase_path(\".\"), \"name\")kernel_module(module_name){ sources = [ \"udp_test.c\", \"wifi_connect.c\", \"inmp441.c\", \"main.c\" ] include_dirs = [ \"//drivers/hdf_core/framework/include/platform/\", \"//drivers/hdf_core/framework/include/utils/\", \"//drivers/hdf_core/framework/support/platform/include/adc\", \"//drivers/hdf_core/adapter/khdf/liteos_m/osal/include/\", \"//drivers/hdf_core/framework/include/core/\", \"//drivers/hdf_core/framework/include/osal/\", \"//drivers/hdf_core/interfaces/inner_api/utils\", \"//device/soc/esp/esp32/components/driver/include\", \"//device/soc/esp/esp32/components/esp_adc_cal/include\", \"//drivers/hdf_core/framework/support/platform/include/gpio\",  \"//device/soc/esp/esp32/components/driver/esp32/include\", \"//foundation/communication/wifi_lite/interfaces/wifiservice\", \"//device/board/esp/esp32/liteos_m/hals/driver/wifi_lite\",  \"//device/soc/esp/esp32/components/esp_wifi/include\", \"//device/soc/esp/esp32/components/esp_event/include\", \"//device/soc/esp/esp32/components/esp_netif/include\", \"//device/soc/esp/esp32/components/tcpip_adapter/include\", \"//device/soc/esp/esp32/components/spi_flash/sim/stubs/freertos/include\", \"//device/soc/esp/esp32/components/osal/include/esp_osal\", \"//device/soc/esp/esp32/components/driver/include/driver\", \"//device/soc/esp/esp32/components/hal/include/hal\" ]}

代码调试

运行代码发现缺少i2s.h中定义的api函数,但是可以通过右键跳转找到函数的定义,而且能在components中找到i2s.h这个库,说明已经将其移植过来了,那到底是为什么编译提示函数未定义呢?这就跟openharmony的编译过程有关了,简单来说,移植来的库需要在BUILD.gn文件中参与编译才能生效。

我的思路是先检查工程文件的BUILD.gn中是否将移植的外设库进行了包含

发现已经包含了外设库的路径,那么再看看外设库的BUILD.gn文件是否将i2s的驱动进行了编译.

使用Ctrl+F搜索关键字i2s,果然在BUILD.gn文件中发现driver/i2s.c这行被注释掉,我们尝试去掉注释再编译。

发现还是会有报错:

我们跳转过去后,尝试取消esp_pm.h的依赖,注释掉之后又出现了一个新的报错:

在Ubuntu中grep 搜索一下这个变量,发现它是用来打印提示信息的,应该删掉这一段代码也不影响。

我们这里直接将其注释掉,发现程序能成功运行了!

我们可以通过网络助手NetAssist进行UDP协议的网络调试

这相当于监听本机上的所有1234端口的数据(这里需要与ESP32代码中udp的端口设置一致)

数据传输这边没问题,我们还可以编写一个python的上位机用来将数据流保存成wav文件实现录音功能!