DS让我一言难尽,因为总是给我整出幺蛾子,总喜欢过度设计或则过度编码。这些都需要你自己去分辨这些是否需要执行。今天我继续说说我如何让I2S Slave Out的状态机运行起来。(实际ADC的输出尚未知,还需要硬件调试)。
之前我说过我需要一个正弦波输出的效果。因此,我写了一个简单的类,来实现正弦波的创建和输出,采用的是正弦表的方式,代码如下:
#ifndef _SINE_WAVE_
#define _SINE_WAVE_
#include "Common.h"
#define SINE_TABLE_LENGTH 255
class CSineWave
{
private:
uint32_t frame_per_second;
uint8_t bit_depth;
uint32_t sine_freq;
int32_t sine_table[SINE_TABLE_LENGTH]; // 正弦波数据表
uint32_t phase_accumulator; // DDS相位累加器
uint32_t phase_increment; // 相位增量
uint32_t call_count;
bool initialized = false;
__attribute__((aligned(8))) pio_i2s i2s_inst;
public:
CSineWave();
~CSineWave();
// 生成正弦波表(预计算优化)
bool GenerateSineTable();
void HandleDmaIrq();
bool Initiate(uint8_t out_pin, uint32_t fps, uint8_t bit_per_frame, uint32_t sine_frequency);
void DebugOutput();
};
#endif
创建正弦表的C++部分如下:
// 生成正弦波表(预计算优化)
bool CSineWave::GenerateSineTable()
{
if( bit_depth == 0 ) {
return false;
}
const double amplitude = static_cast<double>( ( uint32_t(1) << (bit_depth - 1) ) - 1 );
for( uint32_t i = 0; i < SINE_TABLE_LENGTH; ++ i )
{
sine_table[i] = static_cast<int32_t>( std::round(
amplitude * sin( 2.0 * M_PI * static_cast<double>(i) / SINE_TABLE_LENGTH ) ) );
}
return true;
}
创建正弦表非常简单,其实就是准备一个数组,然后通过位深来计算最大值,然后再把一个完整的周期分为255个点,通过sin函数一个一个计算这些点的值。
好了,接下来需要生成DMA回调的时候的代码,我让DS生成这个代码,因为我当时没有看到那个开源的工程里面有一个例子,所以我对如何写这个回调函数没有任何思路,我想的是让DS先写一个出来再改。。。这个是地狱的开始。下面是DS给我生成的代码:
void CSineWave::HandleDmaIrq()
{
// 1. 清除DMA中断标志
dma_hw -> ints0 = 1u << i2s_inst.dma_ch_out_data;
// 2. 获取当前传输完成的缓冲区索引,0或则1
uint8_t completed_buffer = (
dma_hw -> ch[i2s_inst.dma_ch_out_data].al1_read_addr - // al1_read_addr 是最后读取完成的地址
reinterpret_cast<uintptr_t>(i2s_inst.output_buffer) ) /
( STEREO_BUFFER_SIZE * sizeof(int32_t) );
// 3. 计算下一个缓冲区的填充位置
uint8_t next_buffer = 1 - completed_buffer;
int32_t * fill_target = i2s_inst.output_buffer + ( next_buffer * STEREO_BUFFER_SIZE );
// 4. 填充新缓冲区(立体声)
for( uint32_t i = 0; i < AUDIO_BUFFER_FRAMES; ++ i )
{
// DDS相位累加(32位累加器,高8位作为索引)
const uint32_t index = (phase_accumulator >> 24) & 0xFF;
phase_accumulator += phase_increment;
// 写入立体声数据(左右声道相同)
fill_target[2*i] = sine_table[index]; // 左声道
fill_target[2*i + 1] = sine_table[index]; // 右声道
}
// 5. 重新激活DMA通道
dma_channel_set_write_addr(
i2s_inst.dma_ch_out_data,
i2s_inst.out_ctrl_blocks[next_buffer],
true
);
// 5. 重新使能中断(防止意外关闭)[4](@ref)
dma_channel_set_irq1_enabled(i2s_inst.dma_ch_out_data, true);
call_count ++;
}
从逻辑上来讲,它的做法大致正确,清除标志,然后判断是那个缓冲区被读写,然后决定下一个缓冲区是那个,然后写入数据,激活DMA通道,然后重新启用中断。对此,我不太熟悉DMA的工作流程,就沿用了他的办法。然后,这DMA中断调用一次,就再也不回调了!
这一折腾,就是折腾了我一天的时间。我检查了所有的代码,我对此有猜想:1. 状态机没有执行;2. 中断处理有问题;
我一个一个的排查。首先是做了一个检查,检查这个PIO状态机是否被执行。我在OutputDebug调试函数内添加了几行代码:
Serial.printf( "call: %d\n", call_count );
call_count = 0;
if( pio_sm_is_tx_fifo_full(pio0, i2s_inst.sm_dout) ) {
Serial.println("PIO FIFO Started!");
}
检查中断回调处理了几次,然后检查PIO FIFO是否启用,结果果不其然,PIO没有执行。我想了半天,还是决定暂时不用DS,自己用Slave bidi 的函数做样本,一行一行的对代码。然后,我就发现两个地方,DS之前给我生成的代码漏掉了。
第一个,它在设置好DMA之后,没有启用DMA传输。它漏掉了这一行代码。导致DMA没有被启动。
dma_channel_start(i2s->dma_ch_out_ctrl);
第二个,没有设置PIO状态机的时钟!这个才是致命的。我仔细对比了Slave bidi 的代码,发现哪怕是外部输入时钟,PIO的状态机也启用了时钟,因为Pio也需要时钟来驱动处理外部的信号,而且这个时钟频率必须远远大于外部时钟。我于是直接用MCU的全部时钟频率来驱动这个状态机,虽然不是特别有必要。
// 启用时钟
pio_sm_set_clkdiv_int_frac(pio, i2s->sm_dout, 1, 0);
开启时钟之后,我再次执行,发现日志有输出了:
call: 1PIO FIFO Started!
至少证明PIO已经开始运行了,这是一个好消息,这个过程非常困难,几乎花了我大半天的时间,但是目前调用依旧没有什么变化,执行一次之后,就再也不会执行。在我一筹莫展之际,我突然想起来,是否可以找找这个库的例子看看?我又跑到Github上找到这个工程,这次让我直接看到了一个example。我简直被这个例子闪瞎了眼睛。直接下载打开看,我看到了他的处理方式:
static void dma_i2s_in_handler(void) {
/* We're double buffering using chained TCBs. By checking which buffer the
* DMA is currently reading from, we can identify which buffer it has just
* finished reading (the completion of which has triggered this interrupt).
*/
if (*(int32_t**)dma_hw->ch[i2s.dma_ch_in_ctrl].read_addr == i2s.input_buffer) {
// It is inputting to the second buffer so we can overwrite the first
process_audio(i2s.input_buffer, i2s.output_buffer, AUDIO_BUFFER_FRAMES);
} else {
// It is currently inputting the first buffer, so we write to the second
process_audio(&i2s.input_buffer[STEREO_BUFFER_SIZE], &i2s.output_buffer[STEREO_BUFFER_SIZE], AUDIO_BUFFER_FRAMES);
}
dma_hw->ints0 = 1u << i2s.dma_ch_in_data; // clear the IRQ
}
我看完彻底无语,哪儿有那么复杂!只需要根据最后读取的地址来判断那个缓冲区需要输入数据,然后填充数据,最后重置一下中断标志即可!这个DS简直给我整了个大活。
我自己重写了这段代码,然后再次上机测试。这次终于成功了。折腾一天,如果不是DS给我添乱,也许早就完成了代码调试。所以,奉劝各位,哪怕是再小心,对DS生成的代码也要保持100%的怀疑!
最后我把最终的代码放在下面,供大家参考。
static void dma_out_double_buffer_init(pio_i2s* i2s, void (*dma_handler)(void))
{
// 仅初始化输出通道
i2s->dma_ch_out_ctrl = dma_claim_unused_channel(true);
i2s->dma_ch_out_data = dma_claim_unused_channel(true);
i2s->out_ctrl_blocks[0] = i2s->output_buffer;
i2s->out_ctrl_blocks[1] = &i2s->output_buffer[STEREO_BUFFER_SIZE];
// 输出控制通道
dma_channel_config c = dma_channel_get_default_config(i2s->dma_ch_out_ctrl);
channel_config_set_read_increment(&c, true); // 读取地址递增(遍历地址数组)
channel_config_set_write_increment(&c, false); // 写入地址固定(目标为数据通道的触发器)
channel_config_set_ring(&c, false, 3); // 禁用地址回环
channel_config_set_transfer_data_size(&c, DMA_SIZE_32); // 传输单位:32位
dma_channel_configure(i2s->dma_ch_out_ctrl, &c,
&dma_hw->ch[i2s->dma_ch_out_data].al3_read_addr_trig, // 目标:数据通道的地址触发器
i2s->out_ctrl_blocks, // 源:缓冲区地址数组
1, // 传输次数(每次触发传输一个地址)
false); // 不立即启动
// 输出数据通道
c = dma_channel_get_default_config(i2s->dma_ch_out_data);
channel_config_set_read_increment(&c, true);
channel_config_set_write_increment(&c, false);
channel_config_set_chain_to(&c, i2s->dma_ch_out_ctrl); // 链接到控制通道
channel_config_set_dreq(&c, pio_get_dreq(i2s->pio, i2s->sm_dout, true)); // 触发源:PIO TX FIFO
dma_channel_configure(i2s->dma_ch_out_data,
&c,
&i2s->pio->txf[i2s->sm_dout], // 目标:PIO 状态机的 TX FIFO
NULL, // 源地址由控制通道动态设置
STEREO_BUFFER_SIZE, // 单次传输数据量(一个缓冲区大小)
false);
// 设置DMA中断
dma_channel_set_irq0_enabled(i2s->dma_ch_out_data, true);
irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
irq_set_enabled(DMA_IRQ_0, true);
dma_channel_start(i2s->dma_ch_out_ctrl);
}
// 在文件末尾添加新函数
void i2s_program_start_out_slave(PIO pio, const i2s_config* config, void (*dma_handler)(void), pio_i2s* i2s)
{
if (((uint32_t)i2s & 0x7) != 0) {
panic("pio_i2s argument must be 8-byte aligned!");
}
uint offset = 0;
i2s->pio = pio;
i2s->sm_mask = 0;
i2s->sm_dout = pio_claim_unused_sm(pio, true);
i2s->sm_mask |= (1u << i2s->sm_dout);
// 添加从机输出程序
offset = pio_add_program(pio, &i2s_slave_out_program);
i2s_out_slave_init(pio, i2s->sm_dout, offset, config->dout_pin, config->bit_depth);
// 启用时钟
pio_sm_set_clkdiv_int_frac(pio, i2s->sm_dout, 1, 0);
// 初始化输出DMA
dma_out_double_buffer_init(i2s, dma_handler);
// 开启状态机
pio_enable_sm_mask_in_sync(i2s->pio, i2s->sm_mask);
}
#include "PicoSineWave.h"
// 静态实例指针用于中断处理
static CSineWave * s_pSineWaveInstance = NULL;
// DMA 中断处理函数
void dma_handler(void)
{
if( s_pSineWaveInstance ) {
s_pSineWaveInstance -> HandleDmaIrq();
}
}
CSineWave::CSineWave() :
bit_depth(0),frame_per_second(0),sine_freq(0),phase_accumulator(0),phase_increment(0),call_count(0)
{
memset( sine_table, 0, sizeof(sine_table) );
}
CSineWave::~CSineWave()
{
}
// 生成正弦波表(预计算优化)
bool CSineWave::GenerateSineTable()
{
if( bit_depth == 0 ) {
return false;
}
const double amplitude = static_cast<double>( ( uint32_t(1) << (bit_depth - 1) ) - 1 );
for( uint32_t i = 0; i < SINE_TABLE_LENGTH; ++ i )
{
sine_table[i] = static_cast<int32_t>( std::round(
amplitude * sin( 2.0 * M_PI * static_cast<double>(i) / SINE_TABLE_LENGTH ) ) );
}
return true;
}
void CSineWave::HandleDmaIrq()
{
// 1. 获取当前活动的控制块地址
int32_t** ctrl_addr = (int32_t**) dma_hw -> ch[i2s_inst.dma_ch_out_ctrl].read_addr;
int32_t* active_ctrl_block = *ctrl_addr;
// 2. 确定要填充的缓冲区
int32_t* fill_target = NULL;
if( active_ctrl_block == i2s_inst.out_ctrl_blocks[0] ) {
fill_target = i2s_inst.out_ctrl_blocks[1]; // 当前正在使用缓冲区0 → 填充缓冲区1
} else {
fill_target = i2s_inst.out_ctrl_blocks[0]; // 当前正在使用缓冲区1 → 填充缓冲区0
}
// 3. 填充新缓冲区(立体声)
for( uint32_t i = 0; i < AUDIO_BUFFER_FRAMES; ++ i )
{
const uint32_t index = (phase_accumulator >> 24) & 0xFF;
phase_accumulator += phase_increment;
// 写入立体声数据(左右声道相同)
fill_target[2*i] = sine_table[index]; // 左声道
fill_target[2*i + 1] = sine_table[index]; // 右声道
}
// 3. 重新使能中断
dma_hw->ints0 |= 1u << i2s_inst.dma_ch_out_data;
call_count++;
}
bool CSineWave::Initiate(uint8_t out_pin, uint32_t fps, uint8_t bit_per_frame, uint32_t sine_frequency)
{
s_pSineWaveInstance = this;
frame_per_second = fps;
bit_depth = bit_per_frame;
sine_freq = sine_frequency;
// 计算系统时钟频率(立体声 ×2)
uint32_t sck = frame_per_second * bit_depth * 2;
// 计算相位增量(N=32位)
phase_increment = static_cast<uint32_t>(
(static_cast<uint64_t>(sine_frequency) << 32) / sck
);
GenerateSineTable();
i2s_config config;
config.fs = frame_per_second; // fs
config.bit_depth = bit_depth; // 位深
config.dout_pin = out_pin; // 数据输出引脚
config.din_pin = 0; // 数据输入引脚
config.clock_pin_base = 0; // 没使用
config.sck_enable = false; // 禁用 SCK(外部提供时钟)
config.sck_pin = 0; // 没使用
config.sck_mult = 0; // 没使用
i2s_program_start_out_slave( pio0, &config, dma_handler, &i2s_inst );
Serial.printf("sine ok.\n");
return true; // 明确返回成功
}
void CSineWave::DebugOutput()
{
Serial.printf( "call: %d\n", call_count );
call_count = 0;
if( pio_sm_is_tx_fifo_full(pio0, i2s_inst.sm_dout) ) {
Serial.println("PIO FIFO Started!");
}
}