CH32 Oscillator

From Stm32World Wiki
Jump to navigation Jump to search

In order to get to know the CH32V307 MCU better I decided to create a dual oscillator using the DACs.


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).


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.


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

The final thing that need to be enabled is the Interrupt from the DMA.

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

By now, timer, dma and the dac is fully configured and will run off of the buffer defined. Unfortunately, the buffer is filled with zeros, so we need to fill some data into it.

The Interrupt handler will be called twice during the run of the full buffer. The full buffer got 96 elements, so the interrupt will be generated after 48 values have been transferred to the DAC, and then again after the full 96.

__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) {
        update_dac_buffer (&dac_buffer[BUFFER_SIZE]);
        DMA_ClearITPendingBit (DMA2_IT_TC3);
    else if (DMA_GetITStatus (DMA2_IT_HT3) != RESET) {
        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);


After the full transfer we can update the second half of the buffer and after the halfway point we can update the first.

The actual calculation becomes quite simple. Rather than calculating based on frequency the frequency is turned into an angular speed (in angular change per sample), so we simply add this value to the total value at each sample. We also make sure the angle never get above 2 * PI (full circle) by subtracting 2PI when it does get too large.

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));

More Videos

Dual oscillator slightly out of sync (250 Hz and 249.9 Hz):

X-Y plot:

Miscellaneous Links