I am trying to make a version of input_i2s.cpp for Teensy 4 that would allow me to capture all 24 bits from a WM8731 audio codec and shift the bits left by a selectable number of steps before using the resulting 16 MSBs as ordinary 16-bit Teensy audio. The application is an SDR radio and I sometimes receive very weak signals where the MSBs are not exercised, while it would be great to not chop off all of the eight LSBs that are not captured by the ordinary 16-bit Teensy audio routines. (I also use the same codec to drive head phones, but I do not intend to change from 16 bits on the DAC side.)
I have not gotten this to work despite a few days of troubleshooting, reading the existing source code as well as the IMXRT1060 datasheet sections for I2S and DMA. I did put a Serial.println() in the ISR and I can see that it gets called between 1900 and 2000 times and then it does not get called any more. The loop() function in the sketch does get called. At least if I power cycle the board after reprogramming it.
While writing this post, I discovered the https://github.com/WMXZ-EU/microSoundRecorder project where 32-bit audio is captured. It seems to do very similar changes to the DMA setup as I have done, so I think I am on the right track. Although that project is for Teensy 3.6, not 4.0, so some of the changes are not directly transferable.
This is what I have done so far:
In control_wm8731.cpp I changed from 16-bit to 32-bit mode:
This does not seem to break anything as there are plenty of unused bits in the I2S frames and the 16 bits per sample that were sent before remains where they were (MSB aligned), while extra bits are sent in previously unused clock cycles. So this is compatible with the normal audio routines.
The big changes that break things are in my version of input_i2s.cpp (called input_i2s_32.cpp). I made a new class called AudioInputI2S_32 for this purpose.
I also made the same change to ATTR_SSIZE(2) in output_i2s.c. Not sure it is necessary
As mentioned, the symptoms is that the ISR gets called almost 2000 times and then it stops getting called. loop() gets called, but possibly much less frequently. After about 25 minutes of this, the ISR routine gets called about 80 times again and after that nothing more seems to happen.
Here is the code from my new file, input_i2s_32.cpp where most of the changes I described above are located:
It is probably very hard to pinpoint the error from this information, but general suggestions on what more to look into and how to continue troubleshooting would be much appreciated.
I have not gotten this to work despite a few days of troubleshooting, reading the existing source code as well as the IMXRT1060 datasheet sections for I2S and DMA. I did put a Serial.println() in the ISR and I can see that it gets called between 1900 and 2000 times and then it does not get called any more. The loop() function in the sketch does get called. At least if I power cycle the board after reprogramming it.
While writing this post, I discovered the https://github.com/WMXZ-EU/microSoundRecorder project where 32-bit audio is captured. It seems to do very similar changes to the DMA setup as I have done, so I think I am on the right track. Although that project is for Teensy 3.6, not 4.0, so some of the changes are not directly transferable.
This is what I have done so far:
In control_wm8731.cpp I changed from 16-bit to 32-bit mode:
Code:
write(WM8731_REG_INTERFACE, 0x4E); // I2S, 32 bit, MCLK master (was 0x42, 16-bit)
This does not seem to break anything as there are plenty of unused bits in the I2S frames and the 16 bits per sample that were sent before remains where they were (MSB aligned), while extra bits are sent in previously unused clock cycles. So this is compatible with the normal audio routines.
The big changes that break things are in my version of input_i2s.cpp (called input_i2s_32.cpp). I made a new class called AudioInputI2S_32 for this purpose.
- I added the new class as a friend class in AudioOutputI2S so that I could call AudioOutputI2S::config_i2s();
- New constant for the buffer size, which has to be twice as big as before:
Code:const static int32_t I2S_RX_BUFFER_SIZE = 2*AUDIO_BLOCK_SAMPLES; DMAMEM __attribute__((aligned(32))) static uint32_t i2s_rx_buffer[I2S_RX_BUFFER_SIZE];
- Changed SADDR from I2S1_RDR0+2 to I2S1_RDR0+0 since we want all 32 bits. (Not so sure about this change.)
- Change ATTR SSIZE from 1 (16 bits) to 2 (32 bits)
- Increase NBYTES_MLNO from 2 to 4.
- Increase DOFF from 2 to 4.
- Set CITER and BITER_ELINKNO to buffer size divided by 4 instead of 2.
- Add the bit shifting (right shift by 16 to initially get the same result as before) in the do/while loop in the isr.
- Removed the slave support since I do not intend to use it.
I also made the same change to ATTR_SSIZE(2) in output_i2s.c. Not sure it is necessary
As mentioned, the symptoms is that the ISR gets called almost 2000 times and then it stops getting called. loop() gets called, but possibly much less frequently. After about 25 minutes of this, the ISR routine gets called about 80 times again and after that nothing more seems to happen.
Here is the code from my new file, input_i2s_32.cpp where most of the changes I described above are located:
Code:
#include <Arduino.h>
#include "input_i2s_32.h"
#include "output_i2s.h"
const static int32_t I2S_RX_BUFFER_SIZE = 2*AUDIO_BLOCK_SAMPLES; // PM, 2*32 bits per stereo sample
//const static int32_t I2S_RX_BUFFER_SIZE = AUDIO_BLOCK_SAMPLES; // PM, 2*16 bits per stereo sample
DMAMEM __attribute__((aligned(32))) static uint32_t i2s_rx_buffer[I2S_RX_BUFFER_SIZE]; // PM
audio_block_t * AudioInputI2S_32::block_left = NULL;
audio_block_t * AudioInputI2S_32::block_right = NULL;
uint16_t AudioInputI2S_32::block_offset = 0;
bool AudioInputI2S_32::update_responsibility = false;
DMAChannel AudioInputI2S_32::dma(false);
void AudioInputI2S_32::begin(void)
{
dma.begin(true); // Allocate the DMA channel first
//block_left_1st = NULL;
//block_right_1st = NULL;
// TODO: should we set & clear the I2S_RCSR_SR bit here?
AudioOutputI2S::config_i2s();
#if defined(KINETISK)
CORE_PIN13_CONFIG = PORT_PCR_MUX(4); // pin 13, PTC5, I2S0_RXD0
dma.TCD->SADDR = (void *)((uint32_t)&I2S0_RDR0 + 2);
dma.TCD->SOFF = 0;
dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1);
dma.TCD->NBYTES_MLNO = 4; // PM
dma.TCD->SLAST = 0;
dma.TCD->DADDR = i2s_rx_buffer;
dma.TCD->DOFF = 4; // PM
dma.TCD->CITER_ELINKNO = sizeof(i2s_rx_buffer) / 4; // PM
dma.TCD->DLASTSGA = -sizeof(i2s_rx_buffer);
dma.TCD->BITER_ELINKNO = sizeof(i2s_rx_buffer) / 4; // PM
dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR;
dma.triggerAtHardwareEvent(DMAMUX_SOURCE_I2S0_RX);
I2S0_RCSR |= I2S_RCSR_RE | I2S_RCSR_BCE | I2S_RCSR_FRDE | I2S_RCSR_FR;
I2S0_TCSR |= I2S_TCSR_TE | I2S_TCSR_BCE; // TX clock enable, because sync'd to TX
#elif defined(__IMXRT1062__)
CORE_PIN8_CONFIG = 3; //1:RX_DATA0
IOMUXC_SAI1_RX_DATA0_SELECT_INPUT = 2; // Use pin D8 on Teensy 4 as I2S ADCDATA
dma.TCD->SADDR = (void *)((uint32_t)&I2S1_RDR0 + 0); // 2 -> 0 PM
dma.TCD->SOFF = 0; // 0: always read from the same address
dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(2) | DMA_TCD_ATTR_DSIZE(1); // SSIZE(1) -> SSIZE(2) PM
dma.TCD->NBYTES_MLNO = 4; // 2 -> 4, read 4 bytes at a time PM
dma.TCD->SLAST = 0;
dma.TCD->DADDR = i2s_rx_buffer;
dma.TCD->DOFF = 4; // Destination offset, 2 -> 4, read 4 bytes at a time PM
dma.TCD->CITER_ELINKNO = sizeof(i2s_rx_buffer) / 4; // 2 -> 4 PM
dma.TCD->DLASTSGA = -sizeof(i2s_rx_buffer);
dma.TCD->BITER_ELINKNO = sizeof(i2s_rx_buffer) / 4; // 2 -> 4 PM
dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR;
dma.triggerAtHardwareEvent(DMAMUX_SOURCE_SAI1_RX);
I2S1_RCSR = I2S_RCSR_RE | I2S_RCSR_BCE | I2S_RCSR_FRDE | I2S_RCSR_FR;
#endif
update_responsibility = update_setup();
dma.enable();
dma.attachInterrupt(isr);
}
void AudioInputI2S_32::isr(void)
{
uint32_t daddr, offset;
const int32_t *src, *end; // int16 -> int32 PM
int16_t *dest_left, *dest_right;
audio_block_t *left, *right;
static int32_t count = 0; // PM debug
#if defined(KINETISK) || defined(__IMXRT1062__)
daddr = (uint32_t)(dma.TCD->DADDR);
dma.clearInterrupt();
Serial.print("isr"); // PM debug
if (daddr < (uint32_t)i2s_rx_buffer + sizeof(i2s_rx_buffer) / 2) {
// DMA is receiving to the first half of the buffer
// need to remove data from the second half
src = (int32_t *)&i2s_rx_buffer[I2S_RX_BUFFER_SIZE/2]; // int16 -> int32 PM
end = (int32_t *)&i2s_rx_buffer[I2S_RX_BUFFER_SIZE]; // int16 -> int32 PM
if (AudioInputI2S_32::update_responsibility) AudioStream::update_all();
} else {
// DMA is receiving to the second half of the buffer
// need to remove data from the first half
src = (int32_t *)&i2s_rx_buffer[0]; // 16 -> 32 PM
end = (int32_t *)&i2s_rx_buffer[I2S_RX_BUFFER_SIZE/2]; // 16 -> 32 PM
}
left = AudioInputI2S_32::block_left;
right = AudioInputI2S_32::block_right;
if (left != NULL && right != NULL) {
offset = AudioInputI2S_32::block_offset;
if (offset <= AUDIO_BLOCK_SAMPLES/2) {
dest_left = &(left->data[offset]);
dest_right = &(right->data[offset]);
AudioInputI2S_32::block_offset = offset + AUDIO_BLOCK_SAMPLES/2;
arm_dcache_delete((void*)src, sizeof(i2s_rx_buffer) / 2);
do {
// Copy and convert to 16-bit values
*dest_left++ = (*src)>>16; // PM
src++; // PM
*dest_right++ = (*src)>>16; // PM
src++; // PM
} while (src < end);
}
}
Serial.print(" exit ISR "); // PM debug
Serial.println(count++);
#endif
}
void AudioInputI2S_32::update(void)
{
audio_block_t *new_left=NULL, *new_right=NULL, *out_left=NULL, *out_right=NULL;
// allocate 2 new blocks, but if one fails, allocate neither
new_left = allocate();
if (new_left != NULL) {
new_right = allocate();
if (new_right == NULL) {
release(new_left);
new_left = NULL;
}
}
__disable_irq();
if (block_offset >= AUDIO_BLOCK_SAMPLES) {
// the DMA filled 2 blocks, so grab them and get the
// 2 new blocks to the DMA, as quickly as possible
out_left = block_left;
block_left = new_left;
out_right = block_right;
block_right = new_right;
block_offset = 0;
__enable_irq();
// then transmit the DMA's former blocks
transmit(out_left, 0);
release(out_left);
transmit(out_right, 1);
release(out_right);
//Serial.print(".");
} else if (new_left != NULL) {
// the DMA didn't fill blocks, but we allocated blocks
if (block_left == NULL) {
// the DMA doesn't have any blocks to fill, so
// give it the ones we just allocated
block_left = new_left;
block_right = new_right;
block_offset = 0;
__enable_irq();
} else {
// the DMA already has blocks, doesn't need these
__enable_irq();
release(new_left);
release(new_right);
}
} else {
// The DMA didn't fill blocks, and we could not allocate
// memory... the system is likely starving for memory!
// Sadly, there's nothing we can do.
__enable_irq();
}
}
It is probably very hard to pinpoint the error from this information, but general suggestions on what more to look into and how to continue troubleshooting would be much appreciated.