本篇文章探討Raspberry Pi Pico 使用PIO功能,模擬I2S界面,連接MAX98357A DAC播放Wave聲音檔。 開發界面使用VSCode + C-SDK。
一、PIO與MAX98357A I2S界面對應
根據MAX98357A datasheet可支援16、24、32bit, 8kHz, 16kHz, 32kHz, 44.1kHz,
48kHz, 88.2kHz取樣速率。分成左右兩聲道。
若是檔案為單聲道時,則左右聲道播放相同內容。
根據Datasheet 16與32 bit的時間脈序如下圖所示,因此24bit時為後補0成為32bit再送出給MAX98357A。
根據以上分析,PIO 程式碼分成mono與streo兩段,分別處理。
- 使用pio side-set pin 輸出LRCLK與BCLK。 bit0:BCLK, bit1:LRCLK
- Mono audio
根據上圖每輸出1bit,pio StateMachine使用4 clock cycle。 因為register ISR未使用,因此可用來當scratch register用,第9行先將OSR存在ISR中,第19行相同內容輸出到右聲道。單聲道時autopull=false。
- Stereo audio
PIO 程式開始時將bits per sample存到scratch register y。
二、SDCard code:
使用no-OS-FatFS-SD-SPI-RPi-Pico程式碼。
三、PIO DMA:
將檔案讀取的資料透過DMA直接送PIO StateMachin TX_FIFO中:
四、成果影片
五、原始程式碼:
1. PIO
;===== mono ====== ; 4 pio clock cycles for 1 bit .program i2s_pio_dma_mono .side_set 2 loop1: out pins, 1 side 0b00 [1] jmp x--, loop1 side 0b01 [1] out pins, 1 side 0b10 mov osr, isr side 0b10 ;isr as scratch register, the same output for right channel mov x, y side 0b11 [1] loop2: out pins, 1 side 0b10 [1] jmp x--, loop2 side 0b11 [1] out pins, 1 side 0b00 public entry_point_m: pull side 0b00 mov isr, osr side 0b01 mov x,y side 0b01 ;===== stereo ====== ; 2 pio clock cycles for 1 bit .program i2s_pio_dma_stereo .side_set 2 loop1: out pins, 1 side 0b00 jmp x--, loop1 side 0b01 out pins, 1 side 0b10 mov x, y side 0b11 loop2: out pins, 1 side 0b10 jmp x--, loop2 side 0b11 out pins, 1 side 0b00 public entry_point_s: mov x,y side 0b01
2. main.c
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/pio.h"
#include "sd_card.h"
#include "ff.h"
#include "pio_dma.h"
#include "hardware/clocks.h"
uint8_t f_buf0[4010], f_buf1[4010]; //double buffer
uint8_t *file_ptr = f_buf0;
uint8_t *fifo_ptr = f_buf1;
uint8_t *temp_ptr=NULL;
bool read_flag=true;
uint byte_trans_count=0;
uint MAX_BYTE_TRANS=4000;
struct i2s_pio_dma_param_t {
//for pio
pio_program_t program;
uint8_t bitsPerSample;
uint8_t offset;
uint8_t entry_point;
uint8_t bitInsCycles; // 4 pio clock cycles for mono, 2 pio clocks for stereo
//for dma
int chan_num;
uint8_t dma_size;
} i2s_pio_dma_param;
struct wave_file_header_t {
char blockID[4];
uint32_t totalSize;
char typeHeader[4];
char fmt[4];
uint32_t headerLen;
uint16_t typeOfFormat;
uint16_t numbeOfChannel;
uint32_t sampleRate;
uint32_t byteRate;
uint16_t blockAlign;
uint16_t bitsPerSample;
char dataHeader[4];
uint32_t dataSize;
} wave_file_header;
void trans_wav();
void i2s_pio_init(PIO pio, uint sm, uint sideset_base, uint out_base, uint numberOfChannels, uint bitsPerSample, uint freq) {
uint offset = 0;
pio_sm_config c;
if (numberOfChannels == 1) {
offset = pio_add_program(pio, &i2s_pio_dma_mono_program);
c = i2s_pio_dma_mono_program_get_default_config(offset);
i2s_pio_dma_param.program = i2s_pio_dma_mono_program;
i2s_pio_dma_param.entry_point = i2s_pio_dma_mono_offset_entry_point_m;
sm_config_set_out_shift(&c, false, false, bitsPerSample);
} else {
offset = pio_add_program(pio, &i2s_pio_dma_stereo_program);
c = i2s_pio_dma_stereo_program_get_default_config(offset);
i2s_pio_dma_param.program = i2s_pio_dma_stereo_program;
i2s_pio_dma_param.entry_point = i2s_pio_dma_stereo_offset_entry_point_s;
sm_config_set_out_shift(&c, false, true, bitsPerSample);
}
i2s_pio_dma_param.offset = offset;
i2s_pio_dma_param.bitsPerSample = bitsPerSample;
pio_gpio_init(pio, out_base);
pio_gpio_init(pio, sideset_base);
pio_gpio_init(pio, sideset_base+1);
pio_sm_set_consecutive_pindirs(pio, sm, sideset_base, 2, true);
pio_sm_set_consecutive_pindirs(pio, sm, out_base, 1, true);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
sm_config_set_out_pins(&c, out_base, 1);
sm_config_set_sideset_pins(&c, sideset_base);
float div = clock_get_hz(clk_sys)/freq;
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
}
void i2s_pio_enable(PIO pio, uint sm) {
dma_channel_set_irq1_enabled(i2s_pio_dma_param.chan_num, true);
pio_sm_set_enabled(pio, sm, true);
pio_sm_exec(pio, sm, pio_encode_set(pio_y, i2s_pio_dma_param.bitsPerSample-2));
pio_sm_exec(pio, sm, pio_encode_jmp(i2s_pio_dma_param.offset+i2s_pio_dma_param.entry_point));
}
void i2s_reset(PIO pio, uint sm) {
pio_sm_drain_tx_fifo(pio, sm);
pio_sm_set_enabled(pio, sm, false);
pio_remove_program(pio, &i2s_pio_dma_param.program, i2s_pio_dma_param.offset);
pio_clear_instruction_memory(pio);
dma_channel_unclaim(i2s_pio_dma_param.chan_num);
irq_set_enabled(DMA_IRQ_1,false);
irq_remove_handler(DMA_IRQ_1, trans_wav);
dma_channel_set_irq1_enabled(i2s_pio_dma_param.chan_num, false);
}
void i2s_pio_dma_init(PIO pio, uint sm) {
i2s_pio_dma_param.chan_num = dma_claim_unused_channel(true);
dma_channel_config c = dma_channel_get_default_config(i2s_pio_dma_param.chan_num);
channel_config_set_write_increment(&c, false);
channel_config_set_read_increment(&c, true);
channel_config_set_dreq(&c, pio_get_dreq(pio, sm, true));
channel_config_set_transfer_data_size(&c, i2s_pio_dma_param.dma_size); //DMA_SIZE_8,16,32
dma_channel_acknowledge_irq1(i2s_pio_dma_param.chan_num); // clear channel interrupt(ints1)
dma_channel_set_irq1_enabled(i2s_pio_dma_param.chan_num, true); // enable irq1 (inte1)
irq_add_shared_handler(DMA_IRQ_1, trans_wav, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
irq_set_enabled(DMA_IRQ_1, true);
dma_channel_configure(i2s_pio_dma_param.chan_num, &c, (void*) (PIO0_BASE+PIO_TXF0_OFFSET),
fifo_ptr, MAX_BYTE_TRANS>> i2s_pio_dma_param.dma_size, false); //DMA_SIZE_8 or 16 or 32
}
void trans_wav() {
if (dma_channel_get_irq1_status(i2s_pio_dma_param.chan_num)) {
dma_channel_acknowledge_irq1(i2s_pio_dma_param.chan_num);
temp_ptr = file_ptr;
file_ptr=fifo_ptr;
fifo_ptr=temp_ptr;
dma_channel_set_trans_count(i2s_pio_dma_param.chan_num, byte_trans_count >> i2s_pio_dma_param.dma_size, false);
dma_channel_set_read_addr(i2s_pio_dma_param.chan_num, fifo_ptr,true);
read_flag=true;
if (byte_trans_count < MAX_BYTE_TRANS) {
dma_channel_set_irq1_enabled(i2s_pio_dma_param.chan_num, false);
}
}
}
void i2s_init() {
i2s_pio_dma_param.bitInsCycles=2;
if (wave_file_header.numbeOfChannel == 1) i2s_pio_dma_param.bitInsCycles=4;
uint freq = wave_file_header.sampleRate*i2s_pio_dma_param.bitInsCycles*wave_file_header.bitsPerSample*2;
i2s_pio_dma_param.dma_size = wave_file_header.bitsPerSample >> 4; // DMA_SIZE_8, 16, 32
i2s_pio_dma_init(pio0, 0);
i2s_pio_init(pio0, 0, 16, 18, wave_file_header.numbeOfChannel,wave_file_header.bitsPerSample, freq);
i2s_pio_enable(pio0, 0);
}
void playWave(char* fn) {
printf("%s\n", fn);
FRESULT fr;
uint br;
FIL pfile;
fr = f_open(&pfile, fn, FA_READ);
if (fr != FR_OK) {
printf("open file error\n");
return;
}
f_read(&pfile, &wave_file_header, 44, &br);
if (fr != FR_OK) {
printf("open file error\n");
return;
}
i2s_init();
f_read(&pfile, fifo_ptr, MAX_BYTE_TRANS, &byte_trans_count);
read_flag=true;
dma_channel_set_trans_count(i2s_pio_dma_param.chan_num, byte_trans_count >> i2s_pio_dma_param.dma_size, false);
dma_channel_set_read_addr(i2s_pio_dma_param.chan_num, fifo_ptr,true);
while (1) {
if (read_flag) {
read_flag=false;
f_read(&pfile, file_ptr, MAX_BYTE_TRANS, &byte_trans_count);
if (byte_trans_count < MAX_BYTE_TRANS) {
f_close(&pfile);
i2s_reset(pio0,0);
break;
}
}
}
}
int main()
{
FRESULT fr;
FATFS fs;
stdio_init_all();
if (!sd_init_driver()) {
printf("SD Initial Error\n");
return 0;
}
fr = f_mount(&fs, "0:", 1);
if (fr != FR_OK) {
printf("bool read_flag=true;mount error\n");
return 0;
}
playWave(demo1.wav");
playWave("demo2.wav");
while(true) {
tight_loop_contents();
}
return 0;
}
3. CMakeLists.txt
# Generated Cmake Pico project file
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/your-path/pico/pico-sdk")
set(PICO_BOARD pico CACHE STRING "Board type")
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0")
message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()
project(pio_dma C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable. Default name is the project name, version 0.1
add_executable(pio_dma main.c )
pico_set_program_name(pio_dma "pio_dma")
pico_set_program_version(pio_dma "0.1")
pico_enable_stdio_uart(pio_dma 1)
pico_enable_stdio_usb(pio_dma 0)
# Add the standard library to the build
target_link_libraries(pio_dma
pico_stdlib)
# Add the standard include files to the build
target_include_directories(pio_dma PRIVATE
${CMAKE_CURRENT_LIST_DIR}
${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required
)
# Add any user requested libraries
target_link_libraries(pio_dma
hardware_dma
hardware_pio
FatFs_SPI
)
# Tell CMake where to find other source code
add_subdirectory(FatFs_SPI build)
pico_add_extra_outputs(pio_dma)
# Enable usb output, disable uart output
pico_enable_stdio_usb(${PROJECT_NAME} 0)
pico_enable_stdio_uart(${PROJECT_NAME} 1)
Hi, this is very useful. Can you provide the git repository for the code? I am unable to replicate the code using the information provided in the blog. It would be really helpful if you could provide the repository link.
回覆刪除