From a33b0c7360649daa2dca59172c89c9f610683745 Mon Sep 17 00:00:00 2001 From: Marcin Bukat Date: Fri, 25 Sep 2026 16:16:27 +0200 Subject: [PATCH] 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 Change-Id: Idc64bce8075d0fc628c29767fc33beda4fff4342 --- firmware/target/arm/rk27xx/audio-rk27xx.c | 5 + firmware/target/arm/rk27xx/pcm-rk27xx.c | 234 +++++++++++++++++++--- 2 files changed, 211 insertions(+), 28 deletions(-) diff --git a/firmware/target/arm/rk27xx/audio-rk27xx.c b/firmware/target/arm/rk27xx/audio-rk27xx.c index 6307274af2..231b87d883 100644 --- a/firmware/target/arm/rk27xx/audio-rk27xx.c +++ b/firmware/target/arm/rk27xx/audio-rk27xx.c @@ -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) diff --git a/firmware/target/arm/rk27xx/pcm-rk27xx.c b/firmware/target/arm/rk27xx/pcm-rk27xx.c index df4b6d11b6..f37eb194b0 100644 --- a/firmware/target/arm/rk27xx/pcm-rk27xx.c +++ b/firmware/target/arm/rk27xx/pcm-rk27xx.c @@ -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 */