CH32V003 Programming: How to use ADC

It’s been a couple of weeks since I released a tutorial on CH32V003. I have recently developed a feature-rich development board for CH32V003, and the prototypes arrived last week, I got very busy testing it and developing the demo firmware. This firmware will go along with the board. If you are interested in knowing about that board, check here.

Coming back to the ADC on CH32V003, there is one 10-bit SAR ADC block on the MCU. It’s a general-purpose ADC, so we can’t expect too much precision, but it should be decent enough for general use.

In real-world applications, ADC is used to sense analogue voltage from various sensors, battery voltage, current, etc.

CH32V003 Programming: How to use ADC 1

Some notes about ADC on CH32V003:

  • The resolution is 10-bit, and the analogue input sensing range is 0 to VDD.
  • A total of 10 channels are there: 8 external channels(AI0 – AI7) + 2 internal channels (VREF, 2 level calibration voltages). Please read more about it in the Reference Manual.
  • Support external delayed triggering is available
  • VDD = 2.7~5.5V: Power supply for some I/O pins and internal voltage regulator (VDD performance decreases if less than 2.9V when using ADC).

Internal Reference Voltage for ADC & Typical Sampling Time

CH32V003 Programming: How to use ADC 2

Can we use external VREF if some applications need more precision?

As per the datasheet, it is not possible to have an external voltage reference for the ADC on CH32V003. VRef internal is connected to ADC Channel 8, which is an internal channel, this helps you to read this channel if you want to measure the internal VREF in your application.

CH32V003 ADC feature Snapshot from the datasheet

The product is embedded with a 10-bit analogue/digital converter (ADC) that shares up to eight external channels and two internal channel samples, with programmable channel sampling times for single, continuous, sweep or intermittent conversion.

Provides an analogue watchdog function that allows very accurate monitoring of one or more selected channels for monitoring channel signal voltages.

Supports external event-triggered transitions with trigger sources, including internal signals from the on-chip timer and external pins. Support for using DMA operation. Supports external trigger delay function.

When this function is enabled, the controller delays the trigger signal according to the configured delay time when an external trigger edge is generated, and the ADC conversion is triggered as soon as the delay time is reached.

Hardware for ADC Testing

The circuit is quite simple, just a 10K potentiometer connected to the MCU’s D4 pin.

CH32V003 Programming: How to use ADC 3

Discontinuous Single Sample ADC read Operation

In this article, I will share how you can configure one Analog input pin to read analogue voltage. So, basically, you configure the ADC, start the conversion, and will get an interrupt once the conversion is completed. Then, you read the value. It’s a single sample ADC read operation.

For configuration, three things are required

  • Set the GPIO Pin for the Analog Input
  • Configure the ADC
  • Configure the Interrupt

To configure the GPIO for ADC input at D4, which has an A7 channel connected, you need to code as shown below:

    ADC_InitTypeDef ADC_InitStructure = {0};
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    NVIC_InitTypeDef NVIC_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div8);

    // ADC I/P on D4 (A7)
    GPIO_InitStructure.GPIO_Pin = ANALOG1_PIN; 
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(ANALOG1_PORT, &GPIO_InitStructure);

Now, to configure the ADC to read the Pin D4/A7 in a discontinuous sensing mode with no external trigger(it’s a software trigger mode) and just a single channel to read, the following code needs to be written.

    ADC_DeInit(ADC1);
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigInjecConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

The following code is to configure the sequence length. As we have only one channel, we will use 1. Then, channel 7 is linked to ADC data register 1.

In this example, we have used ADC_SampleTime_241Cycles for ADC pin sampling, more sampling time will give a better averaged reading as you are giving more time to connect and hold the circuit of ADC to charge the capacitor.

    ADC_InjectedSequencerLengthConfig(ADC1, 1);
    ADC_InjectedChannelConfig(ADC1, ADC_Channel_7, 1, ADC_SampleTime_241Cycles);
    ADC_ExternalTrigInjectedConvCmd(ADC1, DISABLE);

Then, we need to enable the interrupt so that after starting the ADC measurement, it will trigger as soon as the conversion is completed

    NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

Finally, basic ADC calibration and Enable the ADC.


    ADC_Calibration_Vol(ADC1, ADC_CALVOL_50PERCENT);
    ADC_ITConfig(ADC1, ADC_IT_JEOC, ENABLE);
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);

    while (ADC_GetResetCalibrationStatus(ADC1))
        ;

    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1))
        ;

All the above code should be called in a function, say, ADCConfiig(). this function will be called before the while loop or before starting the ADC conversion. See the whole function code below:

void ADCConfig(void)
{
    ADC_InitTypeDef ADC_InitStructure = {0};
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    NVIC_InitTypeDef NVIC_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div8);

    GPIO_InitStructure.GPIO_Pin = ANALOG1_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(ANALOG1_PORT, &GPIO_InitStructure);

    ADC_DeInit(ADC1);
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigInjecConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_InjectedSequencerLengthConfig(ADC1, 1);
    ADC_InjectedChannelConfig(ADC1, ADC_Channel_7, 1, ADC_SampleTime_241Cycles);
    ADC_ExternalTrigInjectedConvCmd(ADC1, DISABLE);

    NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    ADC_Calibration_Vol(ADC1, ADC_CALVOL_50PERCENT);
    ADC_ITConfig(ADC1, ADC_IT_JEOC, ENABLE);
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);

    while (ADC_GetResetCalibrationStatus(ADC1))
        ;

    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1))
        ;

}

To initiate the ADC conversion, you need to call the ADC_SoftwareStartInjectedConvCmd() function:

ADC_SoftwareStartInjectedConvCmd(ADC1, ENABLE);

To capture the interrupt once the conversion is complete, the following code is required for the IRQ handling.

I have made a Flag variable high and nothing else to avoid being in the interrupt function for too long.

The rest of the processing I did in the main code.

void ADC1_IRQHandler() __attribute__((interrupt("WCH-Interrupt-fast")));

/**

 * The function ADC1_IRQHandler handles the interrupt for ADC1 and prints the value of the injected

 * conversion.

 */

void ADC1_IRQHandler()
{

    if (ADC_GetITStatus(ADC1, ADC_IT_JEOC) == SET)
    {
        adcFlag = 1;

        ADC_ClearITPendingBit(ADC1, ADC_IT_JEOC);

    }

}

The main code will look like this, where we will read ADC reading and further process it to convert it to real-world voltage level.

if(adcFlag ==1)
        {
          adcReading = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1);
          adcReading = (adcReading * 325)/100; // Converting to real voltage w.r.t. VCC 3.3V, 0.985 multiplier factor is used with 3.3V for calibration. 330 * 0.985 = 325. This was as per my board, you might need to do it with different multiplier value.
          adcFlag = 0;
        }

CH32V003 ADC Testing

I did some testing with 10-bit ADC of CH32V003 on my development board, and these are the results:

  • – 3rd Col: without compensation
  • – 5th Col: with compensation of 0.985 multiplier

You can see the readings are within +-10mV.

CH32V003 Programming: How to use ADC 4

These readings were taken at 24.5 degrees centigrade room temperature and in an ideal development board.

In your real application use case, noise will be there, and ambient temperature may vary, so accuracy will also change accordingly. Please be mindful about it.

Some images of the board and ADC reading on OLED Display.

CH32V003 Programming: How to use ADC 5
CH32V003 ADC reading 2.002V
CH32V003 Programming: How to use ADC 6
CH32V003 ADC reading 2.996V

That’s all about ADC for now.

I will try to update this article with more details, such as how to trigger ADC externally, automatically scan multiple channels, etc.

I hope you found this useful and was easy to follow.


I am currently working as an embedded systems design consultant helping companies build custom embedded products and develop test automation solutions for their PCB.

If you have any feedback about the blog, you can share it in the comments below or contact me directly.


Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.