CH32V003F4P6 - Timers and PWM
In this article, I continue my tutorial series with timers, mainly focusing on the PWM function. I have already talked about the basics of this CH32V003-series microcontroller and showed some examples of controlling the GPIO pins and communicating with the chip via USART. This example expands on these previous exercises, and I will go deeper by explaining some basic stuff using timers. Timers can do more than just “timing”, so make sure you watch the video and read the article!
Introduction to timers
First, let’s talk about the hardware specific things. In this article I will use the chip’s advanced-control timer module (ADTM). This is a 16-bit timer with a lot of fancy features. It can count upwards (incremental) and downwards (decremental), it can be used for generating PWM signals, pulses, it can be used with incremental encoders…etc. But it is better to show these features through examples, so let’s see the details.
Let’s understand very quickly how the timers work and how they generate the PWM signal. The timer is basically a counter. The counter counts up (or down) once for every clock pulse. A 16-bit timer can count up to 2^16 -1 = 65535, and a 32-bit timer can count up to 2^32 - 1 = 4294967295 before overflowing. To generate a PWM signal we need to know the main clock frequency of the MCU which is 48 MHz in this case. The period of this frequency is about 20.83 ns.
Also, we need to know the parameters of the PWM signal: the period (frequency) and the duty cycle. Let’s say 10 Hz with a 50% duty cycle.
Now we have to translate these numbers to the “language of registers”. There are two registers, the auto reload register (ARR) and the capture compare register (CCR) which are used to generate the PWM signal. Basically, CCR says how long the PWM on the output should stay high from the starting of the counter (CNT) and ARR says how long the timer should count from the starting of the counter. There’s actually a formula to determine the output frequency:
We know that the system frequency is 48 MHz, or 4.8x10^-7 Hz. We need to find an ideal combination of PRSC and ARR values to get the 48 MHz down to 10 Hz. We can see that 10 Hz is 4.8 millionth part of the system frequency. So the total product of the denominator should be 4.8 million.
I usually like to decrease the system frequency first by using a rather large prescaler value. Let’s set it to 48000. Since the formula contains a “+1” term, we need to enter 47999. Now, 4.8M divided by 48k is 100. So, the ARR must be set to 100. However, the ARR term also has a “+1”, so we need to enter 99. This would result in 10 Hz frequency.
But this is only half of the work, we need to determine CCR in order to get the desired duty cycle. The duty cycle (%) can be obtained by the following formula:
Since we already know ARR (99) and the duty cycle (50%), we just need to calculate CCR. It is easily visible that we must set it to 50 because (ARR + 1 = 99+1 = 100), so 100 * 50/100 is 50.
One more thing that is good to know is that the GPIO pin that is used for the PWM generation must be defined as a push-pull (PP) output with an alternate function (AF). Otherwise, you won’t see the output.
void initializeTimerPWM(uint16_t PRSC, uint16_t ARR, uint16_t CCR) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_TIM1, ENABLE); GPIO_InitTypeDef GPIO_InitStructure = {0}; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_30MHz; GPIO_Init(GPIOD, &GPIO_InitStructure ); TIM_Cmd( TIM1, DISABLE ); TIM_OCInitTypeDef TIM_OCInitStructure={0}; TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure={0}; TIM_TimeBaseInitStructure.TIM_Period = ARR; TIM_TimeBaseInitStructure.TIM_Prescaler = PRSC; TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit( TIM1, &TIM_TimeBaseInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = CCR; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init( TIM1, &TIM_OCInitStructure ); TIM_CtrlPWMOutputs(TIM1, ENABLE ); TIM_OC1PreloadConfig( TIM1, TIM_OCPreload_Enable ); TIM_ARRPreloadConfig( TIM1, ENABLE ); TIM_Cmd( TIM1, ENABLE ); }
Let’s go a little fancy and let’s recycle the code from the previous USART tutorial where we could receive characters from the computer and process them. Here, we take a step further and we do a bit more. Since we need three parameters, PRSC, ARR and CCR to set the PWM up, let’s receive these numbers via USART and pass them to the timer’s config function.
Parsing the data from USART is relatively simple. We need to put the received characters into a char buffer, then we need to use the sscanf() function to fetch the three numbers from it. Just to make sure, I print the parsed values on the terminal at the end.
You might ask, why did you start with the parsing before showing how to fetch the data from the USART and put it in a buffer? I have a solid reason for this. The above-described parsing function is called inside the function which polls the USART for new data. Therefore, it is a good idea to declare the parsing first and then the polling, otherwise the compiler will complain. The polling function is simple, each time a new character is available on the serial, we put it in the buffer. Once an end line character arrives, the buffer is finalized and the parsing function is called so the three parameters can be fetched from the buffer.
void parse_USART() { int num1 = 0, num2 = 0, num3 = 0; sscanf(usart_buffer, "%d %d %d", &num1, &num2, &num3); prescaler = num1; autoReloadRegister = num2; captureComparRegister = num3; printf("Parsed Numbers: %d, %d, %d\n", prescaler, autoReloadRegister, captureComparRegister); }
void pollUSART() { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) { char received_char = USART_ReceiveData(USART1); if (received_char == '\n') { usart_buffer[buffer_index] = '\0'; parse_USART(); buffer_index = 0; } else { if (buffer_index < BUFFER_SIZE - 1) { usart_buffer[buffer_index++] = received_char; } } } }
Non-blocking blinky
Now we can generate a PWM signal and manipulate its properties. This is already a big leap because many hardware can be driven by a PWM signal. Speed regulation of DC motors, brightness control of LEDs, driving of CCDs and so on…
But, let’s do another basic example which is an important cornerstone of microcontroller projects. We already covered the basic blinky example where a set of LEDs were toggled with a simple delay. I mentioned there already that this is not an ideal way of doing things because during the duration of the delay, the CPU is not able to do anything else, it is “locked” in the delay. It would be much more efficient to turn the toggle the LED, start counting and when the desired time passed, toggle the LED again. And in the meantime, the microcontroller can do other things because it is not blocked by the blocking delay.
The steps are simple, we need to initialize the timer, poll the counter in the while(1) loop and when the expected time has passed, we toggle the LED and reset the counter. Before you wonder why I am not using interrupts is because I will have a separate article just on interrupts, I don’t want to introduce interrupts just yet.
Regarding the timing, it requires a little different logic than the PWM generation, but it is nearly the same. We don’t have CCR here, but only a prescaler and the ARR. So, what we need to do is the following: first, we need to “tame” the system clock (48 Mhz) to a lower frequency by using the prescaler. I used 9600 (9599+1) which means that the timer will tick at 4800000000/9600 = 5000 Hz. This means that the “tick period” is 1/5000 Hz or 200 us. Now let’s select a delay first, let’s say 2 seconds. 2 s / 200 us = 10000, so we need to count 10000 ticks to get 2 s of elapsed time. So, we can simply write an if() condition that constantly polls the timer’s counter and if the counter is greater or equal to 10000, we can toggle the GPIO pin, and reset the timer in order to restart counting up to 2 seconds. Also, now that we know how many ticks we want to count, we can define the ARR value. We need to be able to count up to 10000, so technically, this value can be used, but basically anything larger than this and less than 65536 can be used, so I picked 20000. So, technically, the timer could count up to 20000 before going back to zero, but the code will reset the timer to zero already at 10000.
If you look closely, you can also see that this signal is actually a PWM signal with 50% duty cycle. The pin is ON for 2 seconds and then OFF for two seconds. So, the total period is 4 s or 0.25 Hz.
To prove/test that the CPU is not blocked during polling, we can define a global variable that can act as a counter, and increment it in the while(1) loop together with the timer’s polling function. Then, we can continuously print these numbers on the serial terminal. As expected, the terminal is flooded with numbers and when the timer polling occurs, the message is printed on the terminal and then the numbers keep flooding the terminal until another message.
void initializeTimerDelay() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); TIM_Cmd(TIM1, DISABLE ); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure={0}; TIM_TimeBaseInitStructure.TIM_Period = 19999; TIM_TimeBaseInitStructure.TIM_Prescaler = 9599; TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStructure); TIM_Cmd(TIM1, ENABLE); }
void pollTimer(uint16_t elapsedTicks) { if (TIM_GetCounter(TIM1) >= elapsedTicks) { GPIO_WriteBit(GPIOD, GPIO_Pin_2, (GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_2) == Bit_SET) ? Bit_RESET : Bit_SET); TIM_SetCounter(TIM1, 0); printf("Time occurred\n"); } }
If you found this content useful, please consider joining my YouTube channel’s membership or leaving a donation.
Also, please consider using my affiliate links when buying relevant gadgets.