CH32 Oscillator
In order to get to know the CH32V307 MCU better I decided to create a dual oscillator using the DACs.
Timer
A timer will be responsible for the sample frequency. The timer will be configured to run at 48000 Hz and it will generate an Update event for each clock.
void Timer4_Init (void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = { 0 }; RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM4, ENABLE); TIM_TimeBaseStructInit (&TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Period = 3000 - 1; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit (TIM4, &TIM_TimeBaseStructure); TIM_SelectOutputTrigger (TIM4, TIM_TRGOSource_Update); TIM_Cmd (TIM4, ENABLE); }
A Period of 3000 will give us our desired 48000 Hz (144000000 / 3000 = 48000).
DMA
Setting the values for the DAC 48000 times each second would require a lot of CPU overhead. It is much better to use DMA for this purpose. We allocate a buffer:
#define BUFFER_SIZE 48 uint32_t dac_buffer[2 * BUFFER_SIZE];
In this case we set the buffer size to 48 but we allocate a buffer twice that size so that we can update each half while the other half is being used b the DAC.
void Dac_Dma_Init (void) { DMA_InitTypeDef DMA_InitStructure = { 0 }; RCC_AHBPeriphClockCmd (RCC_AHBPeriph_DMA2, ENABLE); DMA_StructInit (&DMA_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &(DAC->RD12BDHR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) &dac_buffer[0]; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 2 * BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init (DMA2_Channel3, &DMA_InitStructure); DMA_ITConfig (DMA2_Channel3, DMA_IT_TC, ENABLE); DMA_ITConfig (DMA2_Channel3, DMA_IT_HT, ENABLE); DMA_Cmd (DMA2_Channel3, ENABLE); }
As can be seen we allocate the buffer to the DMA as a circular buffer incrementing the address for each sample.
We also configure the DMA to generate an interrupt at half time (DMA_IT_HT) and transfer complete. That way we can update the first half of the buffer after we have received the half time interrupt and the second half of the buffer after the transfer is complete.
DAC
The CH32V307 has gone one DAC with two possible output channels. We'll run both of those channels simultaneously.
void Dual_Dac_Init (void) { GPIO_InitTypeDef GPIO_InitStructure = { 0 }; DAC_InitTypeDef DAC_InitType = { 0 }; // Make sure the APB busses are clocked RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd (RCC_APB1Periph_DAC, ENABLE); // Configure PA4 and PA5 for analog output GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init (GPIOA, &GPIO_InitStructure); GPIO_SetBits (GPIOA, GPIO_Pin_4); // Throw a debug pulse out on PA6 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init (GPIOA, &GPIO_InitStructure); // DAC convertion triggered by Timer 4 DAC_InitType.DAC_Trigger = DAC_Trigger_T4_TRGO; DAC_InitType.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; DAC_InitType.DAC_OutputBuffer = DAC_OutputBuffer_Enable; DAC_Init (DAC_Channel_1, &DAC_InitType); DAC_Init (DAC_Channel_2, &DAC_InitType); DAC_Cmd (DAC_Channel_1, ENABLE); DAC_Cmd (DAC_Channel_2, ENABLE); DAC_DMACmd (DAC_Channel_1, ENABLE); DAC_DMACmd (DAC_Channel_2, ENABLE); DAC_SetDualChannelData (DAC_Align_12b_R, 0x123, 0x321); }
First, the GPIOs are initialized. The conversion trigger will be the event from Timer4.
DMA Interrupt
void DMA_Interrupt_Init () { /*Configuration interrupt priority*/ NVIC_InitTypeDef NVIC_InitStructure = { 0 }; NVIC_InitStructure.NVIC_IRQChannel = DMA2_Channel3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //Seeing priority NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //Response priority NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //Enable NVIC_Init (&NVIC_InitStructure); }
Oscillator Calculation
__attribute__((interrupt("WCH-Interrupt-fast"))) void DMA2_Channel3_IRQHandler () { // To time the ISR throw debug out hi GPIO_WriteBit (GPIOA, GPIO_Pin_6, Bit_SET); if (DMA_GetITStatus (DMA2_IT_TC3) != RESET) { ++full_count; update_dac_buffer (&dac_buffer[BUFFER_SIZE]); DMA_ClearITPendingBit (DMA2_IT_TC3); } else if (DMA_GetITStatus (DMA2_IT_HT3) != RESET) { ++half_count; update_dac_buffer (&dac_buffer[0]); DMA_ClearITPendingBit (DMA2_IT_HT3); } // Finally toggle debug out low again GPIO_WriteBit (GPIOA, GPIO_Pin_6, Bit_RESET); }
static inline void update_dac_buffer (uint32_t *buffer_address) { for (uint8_t sample = 0; sample < BUFFER_SIZE; ++sample) { for (uint8_t oscillator = 0; oscillator < 2; ++oscillator) { osc[oscillator].last_value = osc[oscillator].amplitude * sinf (osc[oscillator].angle); osc[oscillator].angle += osc[oscillator].angle_per_sample; // rotate if (osc[oscillator].angle > M_TWOPI) osc[oscillator].angle -= M_TWOPI; // roll over } buffer_address[sample] = (((uint16_t) (MID_POINT + MID_POINT * osc[1].last_value)) << 16) | ((uint16_t) (MID_POINT + MID_POINT * osc[0].last_value)); } }