diff --git a/examples/spi_dac/README.md b/examples/spi_dac/README.md index ff0c1a8e2128a86741e6bbcc7bf7dab072f7dc61..7693f5d14376784a6b91c4c74a94f52fc1892cc3 100644 --- a/examples/spi_dac/README.md +++ b/examples/spi_dac/README.md @@ -1,5 +1,51 @@ # SPI DAC Demo -This example shows how to set up the SPI port with circular DMA and IRQs to do -continuous audio output through a DAC. +This example shows how to set up the SPI port with a timer, circular DMA and +IRQs to do continuous audio output through an inexpensive I2S DAC. + + + +## Theory +The CH32V003 does not have an I2S port which is usually required for driving +audio DACs, but the inexpensive PT8211 stereo audio DAC is very forgiving of +the signal format used to drive it so we can approximate an I2S signal using +one of the CH32V003 on-chip timers to generate the frame sync signal. The SPI +port then provides the bit clock and serial data. + +### Timer setup +TIM1 on the CH32V003 is set up to generate a 48kHz square wave which is output +on GPIO pin PC4 and serves as the frame sync or WS. The timer is configured to +run in center-aligned mode 3 which generates DMA requests on both rising and +falling edges. Channel 4 is configured as the output and the threshold is set +to 50%. + +### SPI setup +SPI1 is configured for 16-bit TX with a bit clock of 48MHz/8 (3MHz) which is +fast enough to clock out 16 bits of data between the edges of the frame sync +signal. Transmit data arrives via DMA, but the SPI port does not control DMA - +that is triggered from the timer above. + +### DMA setup +DMA1 channel 4 is used because that channel is connected to TIM1 Chl 4 output +and is set up in circular mode with both Half-transfer and Transfer-Complete +interrupts. It continuously pulls data out of a 32-word buffer and sends it to +the SPI port when the timer fires. + +### Interrupts +The DMA TC and HT IRQs trigger execution of a buffer fill routine which simply +indexes through a sinewave table at variable rates using fixed-point math to +fill the buffer as requested. This process uses up only about 4% of the +available CPU cycles so there's plenty leftover for foreground processing or +more complex waveform calculations like interpolation or synthesis. ## Use +Connect a PT8211 DAC as follows: +* DAC pin 1 (BCK) - MCU pin 15 (PC5/SCK) +* DAC pin 2 (WS) - MCU pin 14 (PC4/T1CH4) +* DAC pin 3 (DIN) - MCU pin 16 {PC6/MOSI) +* DAC pin 4 (GND) - ground +* DAC pin 5 (VCC) - 3.3V or 5V supply +* DAC pin 6 (LCH) - left channel output +* DAC pin 8 (RCH) - right channel output + +Connect an oscilloscope to the left and right channel outputs and observe +sine and sawtooth waves at different frequencies. diff --git a/examples/spi_dac/oscope.png b/examples/spi_dac/oscope.png new file mode 100644 index 0000000000000000000000000000000000000000..5f1bb342e937763e11fb6447903d830a9d6ec603 Binary files /dev/null and b/examples/spi_dac/oscope.png differ diff --git a/examples/spi_dac/spidac.h b/examples/spi_dac/spidac.h index 1f2c253f3ad57fffa6e7fffa8ca38e5029990021..e8242b9321abaada92d2d6c7bcd513cc15de2c26 100644 --- a/examples/spi_dac/spidac.h +++ b/examples/spi_dac/spidac.h @@ -7,12 +7,13 @@ #define _SPIDAC_H #include <stdint.h> +#include "Sine16bit.h" // uncomment this to enable GPIO diag #define DAC_DIAG // uncomment this to fill the buffer with static test data -#define DAC_STATIC +//#define DAC_STATIC // uncomment this for timer-generated DMA #define DAC_TIMDMA @@ -20,6 +21,7 @@ #define DACBUFSZ 32 uint16_t dac_buffer[DACBUFSZ]; +uint32_t osc_phs[2], osc_frq[2]; /* * initialize SPI and DMA @@ -27,13 +29,20 @@ uint16_t dac_buffer[DACBUFSZ]; void spidac_init( void ) { #ifdef DAC_STATIC + // fill output buffer with diag data uint16_t data = 0xffff; for(int i=0;i<DACBUFSZ;i++) { /* just a full-scale ramp for now */ dac_buffer[i] = data; data >>= 1; - } + } +#else + // init two oscillators + osc_phs[0] = 0; + osc_phs[1] = 0; + osc_frq[0] = 0x01000000; + osc_frq[1] = 0x00400000; #endif // Enable DMA + Peripherals @@ -61,36 +70,11 @@ void spidac_init( void ) SPI1->CTLR1 = SPI_NSS_Soft | SPI_CPHA_1Edge | SPI_CPOL_Low | SPI_DataSize_16b | SPI_Mode_Master | SPI_Direction_1Line_Tx | - SPI_BaudRatePrescaler_32; - -#ifndef DAC_TIMDMA - // SPI generates DMA Req - SPI1->CTLR2 = SPI_CTLR2_TXDMAEN; - //SPI1->HSCR = 1; + SPI_BaudRatePrescaler_16; // enable SPI port SPI1->CTLR1 |= CTLR1_SPE_Set; - //DMA1_Channel3 is for SPI1TX - DMA1_Channel3->PADDR = (uint32_t)&SPI1->DATAR; - DMA1_Channel3->MADDR = (uint32_t)dac_buffer; - DMA1_Channel3->CNTR = DACBUFSZ; - DMA1_Channel3->CFGR = - DMA_M2M_Disable | - DMA_Priority_VeryHigh | - DMA_MemoryDataSize_HalfWord | - DMA_PeripheralDataSize_HalfWord | - DMA_MemoryInc_Enable | - DMA_Mode_Circular | - DMA_DIR_PeripheralDST | - DMA_IT_TC | DMA_IT_HT; - - NVIC_EnableIRQ( DMA1_Channel3_IRQn ); - DMA1_Channel3->CFGR |= DMA_CFGR1_EN; -#else - // enable SPI port - SPI1->CTLR1 |= CTLR1_SPE_Set; - // TIM1 generates DMA Req and external signal // Enable TIM1 RCC->APB2PCENR |= RCC_APB2Periph_TIM1; @@ -112,7 +96,7 @@ void spidac_init( void ) TIM1->PSC = 0x0000; // Auto Reload - sets period to ~47kHz - TIM1->ATRLR = 1023; + TIM1->ATRLR = 499; // Reload immediately TIM1->SWEVGR |= TIM_UG; @@ -124,7 +108,7 @@ void spidac_init( void ) TIM1->CHCTLR2 |= TIM_OC4M_2 | TIM_OC4M_1; // Set the Capture Compare Register value to 50% initially - TIM1->CH4CVR = 512; + TIM1->CH4CVR = 256; // Enable TIM1 outputs TIM1->BDTR |= TIM_MOE; @@ -151,7 +135,6 @@ void spidac_init( void ) NVIC_EnableIRQ( DMA1_Channel4_IRQn ); DMA1_Channel4->CFGR |= DMA_CFGR1_EN; -#endif } /* @@ -159,53 +142,22 @@ void spidac_init( void ) */ void dac_update(uint16_t *buffer) { -} - -#ifndef DAC_TIMDMA -/* - * SPI DMA IRQ Handler - */ -void DMA1_Channel3_IRQHandler( void ) __attribute__((interrupt)); -void DMA1_Channel3_IRQHandler( void ) -{ -#ifdef DAC_DIAG - GPIOD->BSHR = 1; -#endif + int i; - // why is this needed? Can't just direct compare the reg in tests below - volatile uint16_t intfr = DMA1->INTFR; - - if( intfr & DMA1_IT_TC3 ) + // fill the buffer with stereo data + for(i=0;i<DACBUFSZ/2;i+=2) { - // Transfer complete - update 2nd half - dac_update(dac_buffer+DACBUFSZ/2); - - // clear TC IRQ - DMA1->INTFCR = DMA1_IT_TC3; - - GPIOC->BSHR = (1<<1); // NSS 1 - } - - if( intfr & DMA1_IT_HT3 ) - { - // Half transfer - update first half - dac_update(dac_buffer); - - // clear HT IRQ - DMA1->INTFCR = DMA1_IT_HT3; + // right chl + *buffer++ = Sine16bit[osc_phs[0]>>24]; + osc_phs[0] += osc_frq[0]; - GPIOC->BSHR = (1<<(1+16)); // NSS 0 + // left chl + //*buffer++ = Sine16bit[osc_phs[1]>>24]; + *buffer++ = osc_phs[1]>>16; + osc_phs[1] += osc_frq[1]; } - - // clear the Global IRQ - DMA1->INTFCR = DMA1_IT_GL3; - -#ifdef DAC_DIAG - GPIOD->BSHR = 1<<16; -#endif } -#else /* * TIM1CH4 DMA IRQ Handler */ @@ -248,8 +200,4 @@ void DMA1_Channel4_IRQHandler( void ) GPIOD->BSHR = 1<<16; #endif } - -#endif - - #endif