/**
* Copyright 2022 Colin Lam, Ploopy Corporation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* SPECIAL THANKS TO:
* @miketeachman (github.com/miketeachman)
* @jimmo (github.com/jimmo)
* @dlech (github.com/dlech)
* for their exceptional work on the I2S library for the rp2 port of the
* Micropython project (github.com/micropython/micropython).
*/
#include
#include
#include
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "hardware/gpio.h"
#include "hardware/dma.h"
#include "hardware/irq.h"
#include "ringbuf.h"
#include "i2s.h"
#include "i2s.pio.h"
void i2s_write_init(i2s_obj_t *self) {
self->pio = pio1;
self->pio_program = &i2s_write_program;
self->sm = pio_claim_unused_sm(self->pio, true);
self->prog_offset = pio_add_program(self->pio, self->pio_program);
pio_sm_init(self->pio, self->sm, self->prog_offset, NULL);
pio_sm_config config = pio_get_default_sm_config();
float pio_freq = self->sampling_rate * SAMPLES_PER_FRAME * 32 *
PIO_INSTRUCTIONS_PER_BIT;
float clkdiv = clock_get_hz(clk_sys) / pio_freq;
sm_config_set_clkdiv(&config, clkdiv);
sm_config_set_out_pins(&config, self->sd_pin, 1);
sm_config_set_out_shift(&config, false, true, 32);
sm_config_set_fifo_join(&config, PIO_FIFO_JOIN_TX); // double TX FIFO size
sm_config_set_sideset(&config, 2, false, false);
sm_config_set_sideset_pins(&config, self->sck_pin);
sm_config_set_wrap(&config, self->prog_offset,
self->prog_offset + self->pio_program->length - 1);
pio_sm_set_config(self->pio, self->sm, &config);
uint8_t *rbs = malloc(sizeof(uint8_t) * RINGBUF_LEN_IN_BYTES);
ringbuf_init(&self->ring_buffer, rbs, RINGBUF_LEN_IN_BYTES);
irq_set_exclusive_handler(DMA_IRQ_1, dma_irq_write_handler);
irq_set_enabled(DMA_IRQ_1, true);
gpio_init_i2s(self->pio, self->sm, self->sck_pin, 0, GP_OUTPUT);
gpio_init_i2s(self->pio, self->sm, self->ws_pin, 0, GP_OUTPUT);
gpio_init_i2s(self->pio, self->sm, self->sd_pin, 0, GP_OUTPUT);
dma_configure(self);
pio_sm_set_enabled(self->pio, self->sm, true);
dma_channel_start(self->dma_channel[0]);
}
void gpio_init_i2s(PIO pio, uint8_t sm, uint pin_num, uint8_t pin_val, gpio_dir_t pin_dir) {
uint32_t pinmask = 1 << pin_num;
pio_sm_set_pins_with_mask(pio, sm, pin_val << pin_num, pinmask);
pio_sm_set_pindirs_with_mask(pio, sm, pin_dir << pin_num, pinmask);
pio_gpio_init(pio, pin_num);
}
void dma_irq_write_handler() {
i2s_obj_t *self = &i2s_write_obj;
uint dma_channel;
if (dma_irqn_get_channel_status(1, self->dma_channel[0]))
dma_channel = self->dma_channel[0];
else if (dma_irqn_get_channel_status(1, self->dma_channel[1]))
dma_channel = self->dma_channel[1];
else {
//printf("ERROR write: dma_channel not found");
exit(1);
}
uint8_t *dma_buffer = dma_get_buffer(self, dma_channel);
if (dma_buffer == NULL) {
//printf("ERROR write: dma_buffer not found\n");
exit(1);
}
feed_dma(self, dma_buffer);
dma_irqn_acknowledge_channel(1, dma_channel);
dma_channel_set_read_addr(dma_channel, dma_buffer, false);
}
void dma_configure(i2s_obj_t *self) {
uint8_t num_free_dma_channels = 0;
for (uint8_t ch = 0; ch < NUM_DMA_CHANNELS; ch++) {
if (!dma_channel_is_claimed(ch)) {
num_free_dma_channels++;
}
}
if (num_free_dma_channels < I2S_NUM_DMA_CHANNELS) {
//printf("ERROR: cannot claim 2 DMA channels");
exit(1);
}
for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) {
self->dma_channel[ch] = dma_claim_unused_channel(true);
}
// The DMA channels are chained together. The first DMA channel is used to access
// the top half of the DMA buffer. The second DMA channel accesses the bottom half of the DMA buffer.
// With chaining, when one DMA channel has completed a data transfer, the other
// DMA channel automatically starts a new data transfer.
enum dma_channel_transfer_size dma_size = DMA_SIZE_32;
for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) {
dma_channel_config dma_config = dma_channel_get_default_config(self->dma_channel[ch]);
channel_config_set_transfer_data_size(&dma_config, dma_size);
channel_config_set_chain_to(&dma_config, self->dma_channel[(ch + 1) % I2S_NUM_DMA_CHANNELS]);
uint8_t *dma_buffer = self->dma_buffer + (SIZEOF_HALF_DMA_BUFFER_IN_BYTES * ch);
channel_config_set_dreq(&dma_config, pio_get_dreq(self->pio, self->sm, true));
channel_config_set_read_increment(&dma_config, true);
channel_config_set_write_increment(&dma_config, false);
dma_channel_configure(self->dma_channel[ch],
&dma_config,
(void *)&self->pio->txf[self->sm], // dest = PIO TX FIFO
dma_buffer, // src = DMA buffer
SIZEOF_HALF_DMA_BUFFER_IN_BYTES / (32 / 8),
false);
}
for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) {
dma_irqn_acknowledge_channel(1, self->dma_channel[ch]); // clear pending. e.g. from SPI
dma_irqn_set_channel_enabled(1, self->dma_channel[ch], true);
}
}
// note: first DMA channel is mapped to the top half of buffer, second is mapped to the bottom half
uint8_t *dma_get_buffer(i2s_obj_t *i2s_obj, uint channel) {
for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) {
if (i2s_obj->dma_channel[ch] == channel) {
return i2s_obj->dma_buffer + (SIZEOF_HALF_DMA_BUFFER_IN_BYTES * ch);
}
}
// This should never happen
return NULL;
}
void feed_dma(i2s_obj_t *self, uint8_t *dma_buffer_p) {
// when data exists, copy samples from ring buffer
if (ringbuf_available_data(&self->ring_buffer) >= SIZEOF_HALF_DMA_BUFFER_IN_BYTES) {
for (uint32_t i = 0; i < SIZEOF_HALF_DMA_BUFFER_IN_BYTES; i++)
ringbuf_pop(&self->ring_buffer, &dma_buffer_p[i]);
} else {
// underflow. clear buffer to transmit "silence" on the I2S bus
memset(dma_buffer_p, 0, SIZEOF_HALF_DMA_BUFFER_IN_BYTES);
}
}
uint i2s_stream_write(i2s_obj_t *self, const uint8_t *buf_out, uint size) {
if (size == 0) {
//printf("ERROR: buffer can't be length zero");
exit(1);
}
uint32_t num_bytes_written = copy_userbuf_to_ringbuf(self, buf_out, size);
return num_bytes_written;
}
// TODO maybe we can skip every fourth byte, if we're doing this in 24-bit...
// could save on some processing power
uint32_t copy_userbuf_to_ringbuf(i2s_obj_t *self, const uint8_t *buf_out, uint size) {
uint32_t a_index = 0;
while (a_index < size) {
// copy a byte to the ringbuf when space becomes available
while (ringbuf_push(&self->ring_buffer, buf_out[a_index]) == false) {
;
}
a_index++;
}
return a_index;
}