[i=s] 本帖最后由 沈夜 于 2025-10-22 04:35 编辑 [/i]
本来想挑战一下自己,从零开始写一个完整的NES游戏机。前几天刚搞定USB手柄的接入,满心欢喜。今天打算测试声音播放,结果发现手头上的开发板不支持ES8388……瞬间感觉天塌了😭
一路走来真的不容易,一步步学了:
🎮 按键的使用
🖥️ 屏幕驱动
📦 NES模拟器的移植
🗂️ 文件管理系统
🔌 GPIO的使用
🕹️ USB Host的驱动
🎵 声音播放
就差临门一脚了,没想到硬件居然不支持……只能自己写了💪


✅ 一、硬件连接(重点)
MAX98357A 的接口如下(常见模块引脚标号):
| MAX98357A 引脚 |
连接到 BL618 |
说明 |
| DIN (SD) |
I2S_DO(GPIO15) |
I²S 数据输出 |
| BCLK |
I2S_BCLK(GPIO20) |
I²S 时钟 |
| LRC (LRCLK) |
I2S_FS(GPIO13) |
帧时钟 |
| GND |
GND |
电源地 |
| VIN |
3.3V |
电源正 |
| SD_MODE |
拉高(使能) |
可以固定拉 3.3V |
| GAIN0 / GAIN1 |
可选 |
控制音量,默认悬空即可 |
注意:MAX98357A 不需要 I²C;只要 I²S 信号和电源正确,就会自动工作。
✅ 二、软件部分:最简 I²S + DMA 播放 demo
以下代码可直接在 BL618 上播放嵌入的 PCM 数据(一段短音频)。(示例中用一个固定数组代替文件;后面我会告诉你如何换成音频文件数据)
工程基础要求:
- SDK 为 BouffaloLab 官方 BL618(如
bl_mcu_sdk)
- 已包含驱动:
bflb_i2s, bflb_dma, bflb_gpio, bflb_mtimer
#include "board.h"
#include "bflb_i2s.h"
#include "bflb_gpio.h"
#include "bflb_dma.h"
#include "bflb_mtimer.h"
/* 简单单声道16bit PCM 音频(示例数据, 16KHz 采样率) */
extern const int16_t sound_sample[];
extern const uint32_t sound_sample_len;
struct bflb_device_s *i2s0;
struct bflb_device_s *dma0_ch0;
static struct bflb_dma_channel_lli_pool_s tx_llipool[20];
volatile uint8_t dma_done_flag = 0;
void dma_tx_isr(void *arg)
{
dma_done_flag = 1;
printf("DMA TX Done\r\n");
}
/* GPIO Function: setup pins for I2S0 */
void i2s_gpio_init(void)
{
struct bflb_device_s *gpio = bflb_device_get_by_name("gpio");
/* I2S FS -> GPIO13 */
bflb_gpio_init(gpio, GPIO_PIN_13, GPIO_FUNC_I2S |
GPIO_ALTERNATE | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_1);
/* I2S DO -> GPIO15 */
bflb_gpio_init(gpio, GPIO_PIN_15, GPIO_FUNC_I2S |
GPIO_ALTERNATE | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_1);
/* I2S BCLK -> GPIO20 */
bflb_gpio_init(gpio, GPIO_PIN_20, GPIO_FUNC_I2S |
GPIO_ALTERNATE | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_1);
}
/* Init I2S and DMA */
void i2s_dma_init(void)
{
struct bflb_i2s_config_s i2s_cfg = {
.bclk_freq_hz = 16000 * 16 * 2, /* 16bit, stereo frame */
.role = I2S_ROLE_MASTER,
.format_mode = I2S_MODE_LEFT_JUSTIFIED,
.channel_mode = I2S_CHANNEL_MODE_NUM_1,
.frame_width = I2S_SLOT_WIDTH_16,
.data_width = I2S_SLOT_WIDTH_16,
.fs_offset_cycle = 0,
.tx_fifo_threshold = 0,
.rx_fifo_threshold = 0,
};
struct bflb_dma_channel_config_s tx_config = {
.direction = DMA_MEMORY_TO_PERIPH,
.src_req = DMA_REQUEST_NONE,
.dst_req = DMA_REQUEST_I2S_TX,
.src_addr_inc = DMA_ADDR_INCREMENT_ENABLE,
.dst_addr_inc = DMA_ADDR_INCREMENT_DISABLE,
.src_burst_count = DMA_BURST_INCR1,
.dst_burst_count = DMA_BURST_INCR1,
.src_width = DMA_DATA_WIDTH_16BIT,
.dst_width = DMA_DATA_WIDTH_16BIT,
};
i2s0 = bflb_device_get_by_name("i2s0");
bflb_i2s_init(i2s0, &i2s_cfg);
bflb_i2s_link_txdma(i2s0, true);
dma0_ch0 = bflb_device_get_by_name("dma0_ch0");
bflb_dma_channel_init(dma0_ch0, &tx_config);
bflb_dma_channel_irq_attach(dma0_ch0, dma_tx_isr, NULL);
struct bflb_dma_channel_lli_transfer_s tx_trans = {
.src_addr = (uint32_t)sound_sample,
.dst_addr = (uint32_t)DMA_ADDR_I2S_TDR,
.nbytes = sound_sample_len,
};
uint32_t num = bflb_dma_channel_lli_reload(dma0_ch0, tx_llipool,
sizeof(tx_llipool) / sizeof(tx_llipool[0]),
&tx_trans, 1);
bflb_dma_channel_lli_link_head(dma0_ch0, tx_llipool, num);
}
int main(void)
{
board_init();
printf("BL618 + MAX98357A I2S DMA Sound Demo\r\n");
i2s_gpio_init();
i2s_dma_init();
bflb_i2s_feature_control(i2s0, I2S_CMD_DATA_ENABLE,
I2S_CMD_DATA_ENABLE_TX);
printf("Start DMA transmit\r\n");
bflb_dma_channel_start(dma0_ch0);
while (1) {
if (dma_done_flag) {
dma_done_flag = 0;
printf("Playback finished\r\n");
bflb_mtimer_delay_ms(1000);
/* 重新播放 */
bflb_dma_channel_start(dma0_ch0);
}
}
}
🔊 简单的声音数据(示例)
你可以放在另一个 sound_data.c 文件中:
#include <stdint.h>
const int16_t sound_sample[] = {
0, 5000, 10000, 5000, 0, -5000, -10000, -5000,
0, 5000, 10000, 5000, 0, -5000, -10000, -5000,
/* 这里可以放更多采样点,用音频编辑器导出 PCM16 格式 */
};
const uint32_t sound_sample_len = sizeof(sound_sample);
编译烧录后,你将听到简单的「嗡嗡声」。
如果 MAX98357A 输出到喇叭,上电后应该能听见。
✅ 三、播放声音文件(WAV 文件)
如果你想播放「真实声音」:
- 用音频软件(如 Audacity)导出:
- 单声道(Mono)
- 采样率 16 000 Hz
- 16 bit PCM
- 生成
.wav,然后去掉 WAV 文件头(前 44 字节);
- 把纯 PCM 数据转成数组(可以用 Python 脚本生成);
- 替换上面的
sound_sample[]。
或者更高级的:
- 存在 SPI Flash 或 SD 卡上;
- 从文件系统中读取 PCM 数据;
- 分段用 DMA 送到 I²S → 即支持连续播放(需要环形 DMA 或 double buffer)。
✅ 四、总结
| 功能 |
状态 |
| 不需要 I²C |
✅ |
| 可驱动 MAX98357A |
✅ |
| DMA 连续播放 |
✅ |
| 可嵌入 PCM 声音文件 |
✅ |
| 可进一步扩展成 WAV 播放器 |
✅ |