rk27xx: recording over I2S and HDMA channel 1

pcm-rk27xx.c had no recording at all. Add it for targets with
HAVE_RECORDING: HDMA channel 1 moves the I2S Rx FIFO into the recording
buffer - hdreq 7, fixed source, incrementing destination, 32-bit inc8
slices, the mirror of the playback channel and the setup the Samsung
YP-CP3's original firmware uses. Playback keeps channel 0, so the two can
run together.

- HDMA_ISR holds both channels' masks and flags; playback used to write
  all of it, clearing whatever the other channel had. Each channel now
  updates only its own bits, and INT_HDMA serves each channel's count
  down flag. Both channels start masked and clear.
- Recording cannot mask the shared interrupt, so pcm_rec_lock() defers a
  completed buffer to pcm_rec_unlock() instead.
- In master mode the I2S Rx side runs only while recording: started with
  nothing reading its FIFO it upsets playback. I2S_RXCTL gets the Tx
  frame format, plus bit 24 as the YP-CP3's original firmware sets it.
- audio-rk27xx.c routes inputs through audiohw_set_recsrc() on targets
  that record.

Only partly tested, on a YP-CP3: playback and FM radio still work after
the interrupt changes, and the recording screen's peak meter follows the
microphone, so samples arrive through the DMA. Not yet tested: playing
back a recorded file, recording FM, long recordings, recording while
playing. A known risk: a flag set by the hardware in the few cycles of
the read-modify-write of HDMA_ISR would be lost and stall that channel.
Other rk27xx targets build; their playback was not retested on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Change-Id: Idc64bce8075d0fc628c29767fc33beda4fff4342
This commit is contained in:
Marcin Bukat 2026-09-25 16:16:27 +02:00
parent c922ef98c3
commit a33b0c7360
2 changed files with 211 additions and 28 deletions

View file

@ -29,10 +29,15 @@ static int output_source = AUDIO_SRC_PLAYBACK;
static void select_audio_path(void)
{
#ifdef HAVE_RECORDING
/* the codec routes the source for listening and recording both */
audiohw_set_recsrc(input_source, input_flags & SRCF_RECORDING);
#else
if(input_source == AUDIO_SRC_PLAYBACK)
audiohw_set_monitor(false);
else
audiohw_set_monitor(true);
#endif
}
void audio_input_mux(int source, unsigned flags)

View file

@ -31,6 +31,30 @@
static int locked = 0;
/* HDMA_ISR holds the interrupt masks (bits 8-13) and flags (bits 0-5) of
* both channels - channel 0 plays, channel 1 records - so each channel
* updates only its own bits. A flag is cleared by writing 0. */
#define HDMA_CH0_ISR_BITS ((1<<12) | (1<<10) | (1<<9) | \
(1<<4) | (1<<2) | (1<<0))
#define HDMA_CH1_ISR_BITS ((1<<13) | (1<<11) | (1<<8) | \
(1<<5) | (1<<3) | (1<<1))
/* running: only the page count down interrupt unmasked, flags clear */
#define HDMA_CH0_ISR_RUN ((1<<12) | (1<<9))
#define HDMA_CH1_ISR_RUN ((1<<13) | (1<<8))
/* stopped: all masked, flags clear */
#define HDMA_CH0_ISR_STOP ((1<<12) | (1<<10) | (1<<9))
#define HDMA_CH1_ISR_STOP ((1<<13) | (1<<11) | (1<<8))
/* count down to zero */
#define HDMA_CH0_DONE (1<<2)
#define HDMA_CH1_DONE (1<<3)
static void hdma_isr_update(unsigned long bits, unsigned long val)
{
int old = disable_irq_save();
HDMA_ISR = (HDMA_ISR & ~bits) | val;
restore_irq(old);
}
/* Mask the DMA interrupt */
static void sink_lock(void)
{
@ -56,7 +80,7 @@ static void sink_unlock(void)
static void sink_dma_stop(void)
{
HDMA_CON0 = 0x00;
HDMA_ISR = 0x00;
hdma_isr_update(HDMA_CH0_ISR_BITS, HDMA_CH0_ISR_STOP);
locked = 1;
}
@ -74,18 +98,9 @@ static void hdma_i2s_transfer(const void *addr, size_t size)
* (zero based)
*/
HDMA_ISR = ((1<<13) | /* mask ch1 accumulation overflow irq */
(1<<12) | /* mask ch0 accumulation overflow irq */
(1<<11) | /* mask ch1 page count down irq */
(0<<10) | /* UNMASK ch0 page count down irq */
(1<<9) | /* mask ch0 transfer irq */
(1<<8) | /* mask ch1 transfer irq */
(0<<5) | /* clear ch1 accumulation overflow flag */
(0<<4) | /* clear ch0 accumulation overflow flag */
(0<<3) | /* clear ch1 count down to zero flag */
(0<<2) | /* clear ch0 count down to zero flag */
(0<<1) | /* clear ch1 active flag */
(0<<0)); /* clear ch0 active flag */
/* mask ch0 accumulation overflow and transfer irqs, unmask its page
* count down irq, clear its flags */
hdma_isr_update(HDMA_CH0_ISR_BITS, HDMA_CH0_ISR_RUN);
HDMA_ISCNT0 = 0x07; /* slice size in transfer size units (zero base) */
@ -114,6 +129,18 @@ static void sink_dma_start(const void *addr, size_t size)
hdma_i2s_transfer(addr, size);
}
#define I2S_FORMAT ((1<<16) | /* LRCK/SCLK = 64 */ \
(4<<8) | /* MCLK/SCLK = 4 */ \
(1<<4) | /* 16bit samples */ \
(0<<3) | /* stereo */ \
(0<<1) | /* I2S IF */ \
I2S_MASTER)
#ifdef CODEC_SLAVE
#define I2S_MASTER (1<<0) /* master mode */
#else
#define I2S_MASTER (0<<0) /* slave mode */
#endif
static void i2s_init(void)
{
#if defined(HAVE_RK27XX_CODEC)
@ -140,15 +167,10 @@ static void i2s_init(void)
/* configure I2S module */
I2S_IER = 0; /* disable all i2s interrupts */
I2S_TXCTL = (1<<16) | /* LRCK/SCLK = 64 */
(4<<8) | /* MCLK/SCLK = 4 */
(1<<4) | /* 16bit samples */
(0<<3) | /* stereo */
(0<<1) | /* I2S IF */
#ifdef CODEC_SLAVE
(1<<0); /* master mode */
#else
(0<<0); /* slave mode */
I2S_TXCTL = I2S_FORMAT;
#ifdef HAVE_RECORDING
/* bit 24 as the original firmware of the Samsung YP-CP3 sets it */
I2S_RXCTL = I2S_FORMAT | (1<<24);
#endif
/* the fifo is 16x32bits according to my tests
@ -231,6 +253,10 @@ static void sink_dma_init(void)
INTC_IMR |= IRQ_ARM_HDMA;
INTC_IECR |= IRQ_ARM_HDMA;
/* both channels stopped, with the HDMA clocked for the write */
SCU_CLKCFG &= ~CLKCFG_HDMA;
HDMA_ISR = HDMA_CH0_ISR_STOP | HDMA_CH1_ISR_STOP;
audiohw_preinit();
i2s_init();
@ -245,17 +271,46 @@ static void sink_set_freq(uint16_t freq)
audiohw_set_frequency(freq);
}
#ifdef HAVE_RECORDING
static void rec_dma_done(void);
static volatile int rec_locked = 0;
static volatile bool rec_pending = false;
#endif
/* audio DMA ISR called when chunk from callers buffer has been transfered */
void INT_HDMA(void)
{
const void *start;
size_t size;
unsigned long isr = HDMA_ISR;
if (pcm_play_dma_complete_callback(PCM_DMAST_OK, &start, &size))
if (isr & HDMA_CH0_DONE)
{
hdma_i2s_transfer(start, size);
pcm_play_dma_status_callback(PCM_DMAST_STARTED);
const void *start;
size_t size;
hdma_isr_update(HDMA_CH0_DONE, 0);
if (pcm_play_dma_complete_callback(PCM_DMAST_OK, &start, &size))
{
hdma_i2s_transfer(start, size);
pcm_play_dma_status_callback(PCM_DMAST_STARTED);
}
}
#ifdef HAVE_RECORDING
if (isr & HDMA_CH1_DONE)
{
hdma_isr_update(HDMA_CH1_DONE, 0);
if (rec_locked)
{
rec_pending = true;
}
else
{
rec_dma_done();
}
}
#endif
}
struct pcm_sink builtin_pcm_sink = {
@ -279,4 +334,127 @@ struct pcm_sink builtin_pcm_sink = {
/****************************************************************************
** Recording DMA transfer
**/
/* TODO */
#ifdef HAVE_RECORDING
/* HDMA channel 1 from the I2S Rx FIFO. Playback and recording share the
* HDMA interrupt, so locking recording cannot mask it; a buffer completed
* while locked is handed over at unlock instead. */
/* I2S_OPR while playing only, and while recording too. In master mode the
* Rx side runs only while recording: started with nothing reading its FIFO
* it upsets playback. */
#ifdef CODEC_SLAVE
#define I2S_OPR_START_TX (1<<1)
#define I2S_OPR_START_RX (1<<0)
#else
#define I2S_OPR_START_TX 0 /* not used in slave mode */
#define I2S_OPR_START_RX 0
#endif
#define I2S_OPR_PLAY ((0<<6) | /* HDMA Req1 enable */ \
(1<<5) | /* HDMA Req2 disable */ \
(0<<4) | /* Req1 for Tx fifo */ \
(1<<3) | /* Req2 for Rx fifo */ \
I2S_OPR_START_TX)
#define I2S_OPR_RECORD ((I2S_OPR_PLAY & ~(1<<5)) | I2S_OPR_START_RX)
static void hdma_i2s_rec_transfer(void *addr, size_t size)
{
SCU_CLKCFG &= ~CLKCFG_HDMA; /* enable HDMA clock */
/* the DMA writes the buffer: no line of it may be written back over
* the samples later */
commit_discard_dcache_range(addr, size);
HDMA_ISRC1 = (uint32_t)&I2S_RXR; /* i2s rx fifo */
HDMA_IDST1 = (uint32_t)addr; /* destination address */
HDMA_ICNT1 = (uint16_t)((size>>2) - 1); /* number of dma transactions
* of transfer size bytes
* (zero based)
*/
hdma_isr_update(HDMA_CH1_ISR_BITS, HDMA_CH1_ISR_RUN);
HDMA_ISCNT1 = 0x07; /* slice size in transfer size units (zero base) */
HDMA_IPNCNTD1 = 0x01; /* page count */
HDMA_CON1 = ((0<<23) | /* page mode */
(1<<22) | /* slice mode */
(1<<21) | /* DMA enable */
(1<<18) | /* generate interrupt */
(0<<16) | /* on-the-fly is not supported by rk27xx */
(5<<13) | /* transfer mode inc8 */
(7<<9) | /* external hdreq from i2s rx */
(1<<7) | /* fixed source address */
(0<<5) | /* increment destination address */
(2<<3) | /* transfer size = 32bits word */
(0<<1) | /* command of software DMA (not relevant) */
(1<<0)); /* hardware trigger DMA mode */
}
static void rec_dma_done(void)
{
void *start;
size_t size;
if (pcm_rec_dma_complete_callback(PCM_DMAST_OK, &start, &size))
{
hdma_i2s_rec_transfer(start, size);
pcm_rec_dma_status_callback(PCM_DMAST_STARTED);
}
}
void pcm_rec_lock(void)
{
int old = disable_irq_save();
++rec_locked;
restore_irq(old);
}
void pcm_rec_unlock(void)
{
int old = disable_irq_save();
if (--rec_locked == 0 && rec_pending)
{
rec_pending = false;
rec_dma_done();
}
restore_irq(old);
}
void pcm_rec_dma_stop(void)
{
HDMA_CON1 = 0x00;
hdma_isr_update(HDMA_CH1_ISR_BITS, HDMA_CH1_ISR_STOP);
rec_pending = false;
I2S_OPR = I2S_OPR_PLAY;
}
void pcm_rec_dma_start(void *addr, size_t size)
{
pcm_rec_dma_stop();
I2S_OPR = I2S_OPR_PLAY | (1<<16); /* reset Rx */
I2S_OPR = I2S_OPR_RECORD;
hdma_i2s_rec_transfer(addr, size);
}
void pcm_rec_dma_init(void)
{
pcm_rec_dma_stop();
}
void pcm_rec_dma_close(void)
{
pcm_rec_dma_stop();
}
const void * pcm_rec_dma_get_peak_buffer(void)
{
/* where the DMA writes now */
return (const void *)(HDMA_CDST1 & ~3);
}
#endif /* HAVE_RECORDING */