当前位置: 首页 > news >正文

RP2040下的I2S Slave Out,PIO状态机(三)

        DS让我一言难尽,因为总是给我整出幺蛾子,总喜欢过度设计或则过度编码。这些都需要你自己去分辨这些是否需要执行。今天我继续说说我如何让I2S Slave Out的状态机运行起来。(实际ADC的输出尚未知,还需要硬件调试)。

        之前我说过我需要一个正弦波输出的效果。因此,我写了一个简单的类,来实现正弦波的创建和输出,采用的是正弦表的方式,代码如下:

#ifndef _SINE_WAVE_
#define _SINE_WAVE_
#include "Common.h"
#define SINE_TABLE_LENGTH 255class 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或则1uint8_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: 1
PIO 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 firstprocess_audio(i2s.input_buffer, i2s.output_buffer, AUDIO_BUFFER_FRAMES);} else {// It is currently inputting the first buffer, so we write to the secondprocess_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 FIFOdma_channel_configure(i2s->dma_ch_out_data,&c,&i2s->pio->txf[i2s->sm_dout],                          // 目标:PIO 状态机的 TX FIFONULL,                                                  // 源地址由控制通道动态设置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);// 初始化输出DMAdma_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;       // fsconfig.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!");}    
}

http://www.dtcms.com/a/320121.html

相关文章:

  • pybind11 的应用
  • (Python)Python爬虫入门教程:从零开始学习网页抓取(爬虫教学)(Python教学)
  • 腾讯iOA技术指南:实现数据防泄漏与高级威胁防护
  • Spark Memory 内存设计的核心组件、对比Flink内存配置
  • langchain入门笔记02:几个实际应用
  • 生信分析进阶16 - 可准确有效地检测血浆 ccf-mtDNA 突变的分析方法
  • DrissionPage实战案例:小红书旅游数据爬取
  • 串口转ADC/PWM 串口转I2C 串口转GPIO工具
  • c++20--std::format
  • 工业相机使用 YOLOv8深度学习模型 及 OpenCV 实现目标检测简单介绍
  • 计算机视觉CS231n学习(4)
  • jmeter要如何做接口测试?
  • python源码是如何运行起来的
  • HTTPS是如何确保网站安全性的?
  • 【Apache Olingo】全面深入分析报告-OData
  • 使用Ollama本地部署DeepSeek、GPT等大模型
  • C++模拟法超超超详细指南
  • 连续最高天数的销售额(动态规划)
  • 如何让keil编译生成bin文件与反汇编文件?
  • 机器学习:线性回归
  • Win10桌面从默认C盘改到D盘
  • 小红书开源多模态视觉语言模型DOTS-VLM1
  • 深入剖析React框架原理:从虚拟DOM到Fiber架构
  • PCA9541调试记录
  • 软考中级【网络工程师】第6版教材 第2章 数据通信基础(下)
  • ansible 操作家族(ansible_os_family)信息
  • 网页中 MetaMask 钱包钱包交互核心功能详解
  • Redis缓存数据库深度剖析
  • ESXI7.0添加标准交换机过程
  • 通过CNN、LSTM、CNN-LSTM及SSA-CNN-LSTM模型对数据进行预测,并进行全面的性能对比与可视化分析