Difference between revisions of "STM32 bit bang PWM"

From Stm32World Wiki
Jump to navigation Jump to search
 
(22 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
[[Category:STM32]][[Category:STM32 Development]][[Category:STM32CubeMX]][[Category:STM32CubeIde]][[Category:STM32 HAL]]{{metadesc|How to bitbang PWM}}
 
[[Category:STM32]][[Category:STM32 Development]][[Category:STM32CubeMX]][[Category:STM32CubeIde]][[Category:STM32 HAL]]{{metadesc|How to bitbang PWM}}
On many of the cheaper [[STM32]] [[:Category:Development_Board|Development Boards]] there is a [[LED]] attached to PC13.  This is perfectly ok if you want to switch it on or off, but PC13 is not attached to any of the timer channels.
+
On many of the cheaper [[STM32]] [[:Category:Development_Board|Development Boards]] there is a [[LED]] attached to PC13.  This is perfectly ok if you want to switch it on or off, but PC13 is not attached to any of the timer channels, so it will not be possible to control the brightness using [[PWM]].
 +
 
 +
Fortunately, while not ideal, it ''is'' possible to bitbang (not to be confused with [[STM32 Bit Banding (or bit-banding)|bit-banding]]) the [[PWM]] in a manner which doesn't require too much computation.  Contrary to [[PWM]] using a [[Timer]] channel, it does require some computation in the [[MCU]].
 +
 
 +
== Video ==
 +
 
 +
Watch on Youtube here: [https://www.youtube.com/watch?v=ZsHR1bQOmzk https://www.youtube.com/watch?v=ZsHR1bQOmzk]
 +
 
 +
{{#ev:youtube|ZsHR1bQOmzk}}
 +
 
 +
== Bit-banding ==
 +
 
 +
[[Bit-banding]] is a feature of all [[STM32]] [[MCU]]s.  Because [[STM32]] are 32-bit [[MCU]]s they got a huge address space, much bigger than what is needed to address available memory.  Each byte of memory has got a dedicated address for each bit, so if a single bit needs to be set or reset, a value of 0 or 1 can be written to that address.  Without digging into details, the following macro will calculate the necessary address.
 +
 
 +
<pre>
 +
#define BITBAND_BIT_ADDR(src_byte_addr, bit)  (((((uint32_t)(src_byte_addr) & 0x000fffff) << 5) | ((uint32_t)(src_byte_addr) & 0xfff00000) | 0x02000000) + (((uint32_t)(bit)) << 2))
 +
</pre>
 +
 
 +
== Code ==
 +
 
 +
=== Timer Settings ===
 +
 
 +
Firstly configure some timer (we use TIM10 here):
 +
 
 +
[[File:Bitbang PWM Timer Setting.png|600px]]
 +
 
 +
We also need to enable the global timer 10 interrupt:
 +
 
 +
[[File:Bitbang PWM Timer Interrupt Setting.png|600px]]
 +
 
 +
=== Implementation ===
 +
 
 +
We use bitbanding to address the bit used for GPIO:
 +
 
 +
<pre>
 +
// The led_bb_bit points to the bitband address for controlling the PC13
 +
uint8_t *led_bb_bit = (uint8_t*) BITBAND_BIT_ADDR(&LED_GPIO_Port->ODR, 13);
 +
</pre>
 +
 
 +
A few more global variables keep track of the counter and pwm value:
 +
 
 +
<pre>
 +
// Variables to run the pwm.  led_pwm_cnt goes from 0-255 and then roll back over to 0.
 +
// led_pwm_val determines the ratio between on and off status of the LED.
 +
uint32_t led_pwm_cnt;
 +
uint8_t led_pwm_val = 0x00;
 +
</pre>
 +
 
 +
Now the actual PWM work can be handled in the interrupt callback:
 +
 
 +
<pre>
 +
// Callback which runs the PWM
 +
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
 +
 
 +
    if (htim->Instance == LED_PWM_TIM) {
 +
 
 +
        // Increase the counter - it will roll over automatically every 256 bit
 +
        ++led_pwm_cnt;
 +
 
 +
        // Switch LED on off or on depending on value of led_pwm_cnt.
 +
        *led_bb_bit = (uint8_t) led_pwm_cnt >= led_pwm_val ? 1 : 0;
 +
 
 +
    }
 +
 
 +
}
 +
</pre>
 +
 
 +
=== Using BSRR ===
 +
 
 +
After implementing the bit banging using bit-banding, I came to realize that this could also be implemented simply by using the BSRR register:
 +
 
 +
<pre>
 +
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
 +
 
 +
    if (htim->Instance == LED_PWM_TIM) {
 +
        ++led_pwm_cnt; // The counter is 8 bit so will wrap around after 255
 +
 
 +
        // Use BSRR to set or reset bit 13 of the LED GPIO port
 +
        LED_GPIO_Port->BSRR = led_pwm_cnt >= led_pwm_val ? GPIO_BSRR_BS13 : GPIO_BSRR_BR13;
 +
    }
 +
 
 +
}
 +
</pre>
 +
 
 +
The GPIO_BSRR_BS13 and GPIO_BSRR_BR13 macros, contain a formula which calculates the values, but as these are most likely optimized by the compiler into constants, this is likely to perform just as well as the bit-banding approach.
 +
 
 +
== Result ==
 +
 
 +
{{#ev:youtube|ZsHR1bQOmzk}}
 +
 
 +
== Performance (or rather: Performance Penalty) ==
 +
 
 +
=== Using a simple loop counter ===
 +
 
 +
The "normal" approach to doing PWM is to use a Timer Channel. Using that approach the CPU core does not have to do any work once the timer is started and no precious CPU cycles are wasted.  Using the Timer to fire an interrupt and doing any computation in the service handler cost cycles.  The big question is, how expensive is this in reality.
 +
 
 +
In order to determine the "cost" I added a few bits and pieces to the main loop:
 +
 
 +
<pre>
 +
    while (1) {
 +
 
 +
        now = HAL_GetTick();
 +
 
 +
        if (now - last_chg >= 10) {
 +
            led_pwm_val += led_pwm_chg;
 +
 
 +
            if (led_pwm_val == 0)
 +
                led_pwm_chg = 1;    // Go up
 +
            if (led_pwm_val == 100)
 +
                led_pwm_chg = -1;    // Go down
 +
 
 +
            last_chg = now;
 +
        }
 +
 
 +
        if (now - last_tick >= 1000) {
 +
 
 +
            DBG("Tick %lu (loop: %lu) val=%d\n", now / 1000, loop_cnt, led_pwm_val);
 +
 
 +
            loop_cnt = 0;
 +
            last_tick = now;
 +
        }
 +
 
 +
        ++loop_cnt;
 +
    }
 +
</pre>
 +
 
 +
The loop_cnt variable will increase every loop and then reset once every second.  This way, the higher the value the less time is wasted in the interrupt service.  Running normally it will print out the following in a STM32F411 running at 100 MHz:
 +
 
 +
<pre>
 +
Tick 10 (loop: 4632748) val=0
 +
</pre>
 +
 
 +
In other words, the loop will be executed 4.6 million times every second.  Disabling the start of the Timer, this changes to:
 +
 
 +
<pre>
 +
Tick 10 (loop: 4758671) val=0
 +
</pre>
 +
 
 +
Which is about 4.8 million times.  In other words, about 2.6 % of the CPU time is being used in the interrupt service.  That is of course a waste but in all honesty it was/is less expensive than I personally would have thought it would be.
 +
 
 +
=== Using a SWD Trace profiler ===
 +
 
 +
Using the SWD trace to profile the execution, we can pretty much verify this observation:
 +
 
 +
<div class="res-img">
 +
[[File:Bitbang PWM Profiling.png|600px]]
 +
</div>
 +
 
 +
This number - around 1.3 % is a tad lower, but that is most likely because the profiler only includes time spend in the actual subroutine but misses out the time spend on pushing registers on the stack and popping them off again when returning.
 +
 
 +
== Miscellaneous Links ==
 +
 
 +
* [https://github.com/STM32World/firmware/tree/master/mcustm32f405_bitbang_pwm Stm32World STM32F405 Example]
 +
* [https://github.com/lbthomsen/blackpill/tree/master/bitbang_pwm Blackpill STM32F411 Example]

Latest revision as of 06:39, 24 October 2024

On many of the cheaper STM32 Development Boards there is a LED attached to PC13. This is perfectly ok if you want to switch it on or off, but PC13 is not attached to any of the timer channels, so it will not be possible to control the brightness using PWM.

Fortunately, while not ideal, it is possible to bitbang (not to be confused with bit-banding) the PWM in a manner which doesn't require too much computation. Contrary to PWM using a Timer channel, it does require some computation in the MCU.

Video

Watch on Youtube here: https://www.youtube.com/watch?v=ZsHR1bQOmzk

Bit-banding

Bit-banding is a feature of all STM32 MCUs. Because STM32 are 32-bit MCUs they got a huge address space, much bigger than what is needed to address available memory. Each byte of memory has got a dedicated address for each bit, so if a single bit needs to be set or reset, a value of 0 or 1 can be written to that address. Without digging into details, the following macro will calculate the necessary address.

#define BITBAND_BIT_ADDR(src_byte_addr, bit)  (((((uint32_t)(src_byte_addr) & 0x000fffff) << 5) | ((uint32_t)(src_byte_addr) & 0xfff00000) | 0x02000000) + (((uint32_t)(bit)) << 2))

Code

Timer Settings

Firstly configure some timer (we use TIM10 here):

Bitbang PWM Timer Setting.png

We also need to enable the global timer 10 interrupt:

Bitbang PWM Timer Interrupt Setting.png

Implementation

We use bitbanding to address the bit used for GPIO:

// The led_bb_bit points to the bitband address for controlling the PC13
uint8_t *led_bb_bit = (uint8_t*) BITBAND_BIT_ADDR(&LED_GPIO_Port->ODR, 13);

A few more global variables keep track of the counter and pwm value:

// Variables to run the pwm.  led_pwm_cnt goes from 0-255 and then roll back over to 0.
// led_pwm_val determines the ratio between on and off status of the LED.
uint32_t led_pwm_cnt;
uint8_t led_pwm_val = 0x00;

Now the actual PWM work can be handled in the interrupt callback:

// Callback which runs the PWM
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {

    if (htim->Instance == LED_PWM_TIM) {

        // Increase the counter - it will roll over automatically every 256 bit
        ++led_pwm_cnt;

        // Switch LED on off or on depending on value of led_pwm_cnt.
        *led_bb_bit = (uint8_t) led_pwm_cnt >= led_pwm_val ? 1 : 0;

    }

}

Using BSRR

After implementing the bit banging using bit-banding, I came to realize that this could also be implemented simply by using the BSRR register:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {

    if (htim->Instance == LED_PWM_TIM) {
        ++led_pwm_cnt; // The counter is 8 bit so will wrap around after 255

        // Use BSRR to set or reset bit 13 of the LED GPIO port
        LED_GPIO_Port->BSRR = led_pwm_cnt >= led_pwm_val ? GPIO_BSRR_BS13 : GPIO_BSRR_BR13;
    }

}

The GPIO_BSRR_BS13 and GPIO_BSRR_BR13 macros, contain a formula which calculates the values, but as these are most likely optimized by the compiler into constants, this is likely to perform just as well as the bit-banding approach.

Result

Performance (or rather: Performance Penalty)

Using a simple loop counter

The "normal" approach to doing PWM is to use a Timer Channel. Using that approach the CPU core does not have to do any work once the timer is started and no precious CPU cycles are wasted. Using the Timer to fire an interrupt and doing any computation in the service handler cost cycles. The big question is, how expensive is this in reality.

In order to determine the "cost" I added a few bits and pieces to the main loop:

    while (1) {

        now = HAL_GetTick();

        if (now - last_chg >= 10) {
            led_pwm_val += led_pwm_chg;

            if (led_pwm_val == 0)
                led_pwm_chg = 1;     // Go up
            if (led_pwm_val == 100)
                led_pwm_chg = -1;    // Go down

            last_chg = now;
        }

        if (now - last_tick >= 1000) {

            DBG("Tick %lu (loop: %lu) val=%d\n", now / 1000, loop_cnt, led_pwm_val);

            loop_cnt = 0;
            last_tick = now;
        }

        ++loop_cnt;
    }

The loop_cnt variable will increase every loop and then reset once every second. This way, the higher the value the less time is wasted in the interrupt service. Running normally it will print out the following in a STM32F411 running at 100 MHz:

Tick 10 (loop: 4632748) val=0

In other words, the loop will be executed 4.6 million times every second. Disabling the start of the Timer, this changes to:

Tick 10 (loop: 4758671) val=0

Which is about 4.8 million times. In other words, about 2.6 % of the CPU time is being used in the interrupt service. That is of course a waste but in all honesty it was/is less expensive than I personally would have thought it would be.

Using a SWD Trace profiler

Using the SWD trace to profile the execution, we can pretty much verify this observation:

Bitbang PWM Profiling.png

This number - around 1.3 % is a tad lower, but that is most likely because the profiler only includes time spend in the actual subroutine but misses out the time spend on pushing registers on the stack and popping them off again when returning.

Miscellaneous Links