PWM for LCD backlight with STM32F4

I needed a PWM power control for my LCD screen connected on the Cortex M4 (STM32F407).  The backlight of the LCD was to bright at 3.3V and a simple solution would be to add a resistor in series with the backlight LEDs.  But such a solution would lock the backlight at the same power forever, thus an obvious solution: you could try to fiddle with a variable resistor (potentiometer).  However a more elegant solution is to use PWM.  PWM stands for Pulse-Width Modulation, a technique that controls the width of a pulse.  The technique is commonly used to control the power supplied to electrical devices, in my case the backlight of a LCD.

When using PWM, you need to consider the switching frequency, which has to be much higher than what affects the load.  In my case the frequency has to be high enough so that human eye doesn’t notice any flickering the LCD screen caused by a too low frequency in the backlight LEDs.  The duty cycle will define the ‘ON’ time of the LEDs and so it will steer the power transmitted to the LEDs; 0% would be fully OFF and 100% fully ON (in my case the opposite, I’ll explain later why).

The PWM signal will be generated by the timers and output comparators of the STM32F407.  The I/O pin emitting the PWM signal will be connected through a P-Channel MOSFET with low gate charge and low gate voltage (AO3401).  PWM-LCD-BacklightI’m using a P-Channel because the mass on the LCD is common and only the common anode of the LEDs is accessible.  The AO3401 operates at VGS = -2.5V, which is what I want, since my the I/O on my µC delivers 3.3V and I won’t be needing a protection resistor at the gate.  All other specification of the MOSFET are well within range for powering the backlight LEDs.

This setup means that a logic ‘0’ at the gate of the MOSFET would turn on the backlight and a logic ‘1’ would turn it off.  Thus my PWM signal is inverted, a duty cycle of 100% would turn the backlight OFF and 0% would turn it ON.

On the STM32F407 I’m using Timer3 and Output Comparator Channel 1.  According to the datasheet this output can be linked to the ports PA6, PC6 or PB4; in my case I’ll be using PC6.  I chose a switching frequency of 1 kHz, this frequency is high enough (even at low duty cycle) to avoid flickering of the LCD.  This frequency is achieved by scaling the timer as follows:

#define TIM_PRESCALER SystemCoreClock / 2 / ILI9325_LCD_BACKLIGHT_PWM_FREQ / 1000

The duty cycle is obtained by setting a corresponding value in the Output Comparator’s register.  Since the prescaler of timer and the period need exactly 1000 counts for 1 cycle, any value between 0 and 1000 can be used for setting the duty cycle.

Below the complete code to configure the PWM on Timer3, Channel 1 on the STM32F4:

static void ili9325_config_pwm(void) {
	GPIO_InitTypeDef gpio_C; // GPIOC structure
	TIM_TimeBaseInitTypeDef TIM_TimeBase; // Time Base structure
	TIM_OCInitTypeDef TIM3_OC; // Output Compare structure

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE); // Clocking GPIOC (AHB1 = 168MHz)
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // Clocking TIM3  (APB1 = 42MHz)

	gpio_C.GPIO_Pin = GPIO_Pin_6; // Ch.1 (PC6)
	gpio_C.GPIO_Mode = GPIO_Mode_AF; // PWM is an alternative function
	gpio_C.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz
	gpio_C.GPIO_OType = GPIO_OType_PP; // Push-pull
	gpio_C.GPIO_PuPd = GPIO_PuPd_UP; // Pulling-up
	GPIO_Init(GPIOC, &gpio_C); // Initializing GPIOC structure
	GPIO_PinAFConfig(GPIOC, GPIO_PinSource6, GPIO_AF_TIM3); // Routing TIM3 output to PC6

	TIM_TimeBase.TIM_Prescaler = TIM_PRESCALER - 1;
	TIM_TimeBase.TIM_CounterMode = TIM_CounterMode_Up; // Upcounting configuration
	TIM_TimeBase.TIM_Period = TIM_PERIOD - 1;
	TIM_TimeBase.TIM_ClockDivision = 0;
	TIM_TimeBase.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM3, &TIM_TimeBase); // Initializing Time Base structure
	TIM_Cmd(TIM3, ENABLE); // Ready, Set, Go!

	TIM3_OC.TIM_OCMode = TIM_OCMode_PWM1; // Edge-aligned mode
	TIM3_OC.TIM_OutputState = TIM_OutputState_Enable; // Enabling the Output Compare state
	TIM3_OC.TIM_OCPolarity = TIM_OCPolarity_Low; // Regular polarity (low will inverse it)
	TIM3_OC.TIM_Pulse = TIM_PERIOD / 2; // Output Compare 1 reg value (default at 50%)
	TIM_OC1Init(TIM3, &TIM3_OC); // Initializing Output Compare 1 structure
	TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Disable); // Disabling Ch.1 Output Compare preload

The duty cycle can be set by means of the following method:

void ili9325_set_backlight(uint8_t percentage) {
	TIM3->CCR1 = percentage * (TIM_PERIOD / 100); // Output Compare 1 reg value

If all settings are correct, you should obtain the following output on the PWM output pin (PC6) for a duty cycle of 40%:

PWMThis signal puts my LCD’s backlight at exactly 60% of its power.


Tagged , , , . Bookmark the permalink.

31 Responses to PWM for LCD backlight with STM32F4

  1. Gianfranco Schwengle says:

    Hi, I am trying to get my LCD to do something, however I am not having any succes. Tried the blue red green ribbon example with a LCD32RT. Failed. Do you possibly know or have any tutorial on the LCD? First time with the coocox and stm32f4.

    • Patrick says:

      Right now I don’t have a tutorial ready and I really don’t have the time to prepare one immediately. But you can use my libraries if you wish you’ll find them in the downloads under ‘Software’ section of my blog. You will need the files in the LCD section which also require the files in the Delay section and perhaps the ones in the SDCard. Good luck!
      Hopefully somewhat later this year I’ll find the time to write a nice tutorial.

  2. ConZarks says:

    Hi, I know this might be out of topic but I really need your help.
    I’m trying to make complementary signals with TIM8 but nothing works. There is a fixed code for TIM1 complementary signals but when I change to TIM8 nothing works. Could help me out please? Thanks in advance!

    • Patrick says:

      Are you sure you have configured the correct output pins for TIM8? For TIM8 with channels 1 and 1N, you will have to assign PC6 and PA5 to alternate function TIM8 (GPIO_AF_TIM8). Moreover be careful when you configure both pins, they’re on different ports…

      • ConZarks says:

        Here is my code if you want to check it out. I think everything is as it should be.

        • Patrick says:

          I couldn’t see anything wrong with your code. I tested it on my STM32F4 discovery board and everything worked just fine. I checked with a logic sniffer the 1/1N and 2/2N signals and they checked out OK: both complementary and 2/2N shifted by 90° as defined in your code.

          • ConZarks says:

            Well, thanks for checking out. Probably there’s something wrong with my board. My PC6 and PC7 are always ON and their complementaries always OFF. Even a simple PWM code doesn’t work on my TIM8. What could possibly be so wrong?

          • Patrick says:

            Sounds like your TIM8 is not working at all. What board and chip are you using? I also recommend to use STM32CubeMX to check if there are no conflicts.

  3. ConZarks says:

    I am using STM32F407VG discovery board with CooCox. How do I check if there are no conflicts? If it wasn’t working at all, shouldn’t all the channels be OFF?

    • Patrick says:

      With STM32CubeMX, in the IP tree pane you select all the peripherals you use. Conflicts will appear in red, partial conflicts in yellow. This should exclude any internal conflict in the STM32F407 chip.
      Furthermore, check if your electronics connected to the PWM pins are not blocking the signal; I guess not, otherwise everything would be high or low, but not both.
      Another possible source, is that you didn’t initialize the board correctly in the software. In CooCox, make sure that the SystemInit() method is called at the very first beginning of your code. SystemInit() is defined in system_stm32f4xx.h, but you have to call it explicitly.
      Last possibility, broken chip/board…

      • ConZarks says:

        Hello again and happy new year!

        I checked the peripherals I use (TIM8) and here are the conflicts:



        Please check them out and explain me.

        What’s more, I’ve initialized the board as you say in your first tutorials and I called it again at the beginning of my code, though nothing changed.

        Finally I have one more question, is it possible to code from the STM32CubeMX? On the configuration panel I see this and when I click on TIM8 I can change the values:

        • Patrick says:

          Happy new year to you too!

          The conflicts you see are normal, the resources on the STM32F4 are sometimes overlapping and you’ll have to pick one of both; the F4 has some configuration set by default, which might might bock other peripherals. As long as the conflicts are not interfering with your peripherals you should be fine.
          But I see that you used a blank STM32F407VG chip in STM32CubeMX, you should use the preset configuration for the discovery board, the discovery board behaves a bit different than the blank chip. But anyway, I already checked the discovery board for TIM8 and couldn’t find any problem. I’ll try to rerun your code this evening (I’m having my lunch break at work now).

          The code generated by STM32CubeMX is not directly usable by CooCox, the libraries are not the same and therefore some objects are different. CubeMX was designed for Keil and IAR if I’m not mistaken. But the generated code is pretty damn close to what we do in CooCox, so it should give a good overview of what needs to be done in CooCox.

          • Patrick says:

            I think I figured out your problem. The discovery board does have a conflict: the embedded accelerometer LIS302 uses SPI and locks ports PA5, PA6 and PA7 (SPI1). These will interfere with TIM8 outputs if you don’t disable the LIS302 but setting PE3 = 1.

  4. ConZarks says:

    I’m a bit confused, how do I set PE1=1? I tried this but nothing happens:

    void PE3_Config(void)
    GPIO_InitTypeDef GPIO_InitStructure;

    /* GPIOE clocks enable */
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);

    /* GPIOE Configuration */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(GPIOE, &GPIO_InitStructure);



    • ConZarks says:

      Oops, I had a mistake, I found it, sorry. I did change PE1 to high like this:

      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;

      Nothing changed on the other pins, even though PE1 is always ON. Hope the new board I’ve ordered will work.

  5. ConZarks says:

    Well I didn’t notice that you said PE3, so I changed it. It is ON but the PC6, PC7 are still always ON.

    Thanks for the help, I’ll let you know when the new board arrives.

  6. ConZarks says:

    Bad news, even the new board doesn’t work. Is it possible to be a problem that the TIM8_BKIN isn’t a “Free I/O” as labelled here:

    Well the TIM1_BKIN is a “Free I/O” and works fine. What’s more, there are two TIM8_BKIN, PA6 and PI4. The second one doesn’t exist on my discovery kit board.

  7. Patrick says:

    As I mentioned before the SPI used by the accelerometer uses PA6. Disabling the accelerometer by setting PE3 high should be enough to gain control over PA6.

    • ConZarks says:

      I did set PE3 high but nothing changed. You can check the code bellow :

      void PE3_Config(void)
      GPIO_InitTypeDef GPIO_InitStructure;

      /* GPIOE clocks enable */
      RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);

      /* GPIOE Configuration */
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
      GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
      GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
      GPIO_Init(GPIOE, &GPIO_InitStructure);



      Still nothing happened though..

  8. ConZarks says:

    Hello again,

    I figured out that this part of the code makes my TIM8 outputs go high:

    TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable;
    TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable;
    TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1;
    TIM_BDTRInitStructure.TIM_DeadTime = 250;
    TIM_BDTRInitStructure.TIM_Break = TIM_Break_Enable;
    TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
    TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
    TIM_BDTRConfig(TIM8, &TIM_BDTRInitStructure);

    When I comment this part of the code it works fine. Could you please tell which board you are using? I’m thinking about buying the STM32F407IGT6 which has more pins and a second TIM8_BKIN (PI4) which might work.

  9. Patrick says:

    Discovery board with STM32F407VGT6
    I need to find more time to test your TIM8 code, because I don’t believe there is a hardware problem anymore. Could be that gcc is causing a problem. Maybe this WE I can spare some time…

  10. ConZarks says:

    Hello again, do you have any news regarding my code?

    • Patrick says:

      Not really, I used your setup together with Aaron’s tutorial on CubeMX and it didn’t work. At least the TIM8 PWM didn’t work with the tutorial, everything else just worked fine. Problem was that there was no way get a PWM on TIM8, as TIM8 doesn’t support PWM according to CubeMX. However I managed to get some PWM signal on PC6 by overruling CubeMX’s code and hardcoding sConfigOC.OCMode = TIM_OCMODE_PWM1. This got the PWM to work, but it’s lost every time CubeMX regenerates the code.
      This makes me conclude that the STM32F407 does have PWM on TIM8; but somehow it is not documented and it can be very hard to get to work.
      I suggest you to try Aaron’s tutorial (it’s excellent by the way) and adapt it for TIM8. Overrule the generated code in main.c and set OCMode to PWM1.

      • Patrick says:

        I had problems myself yesterday with TIM8 and a simple PWM. After some investigation I realized that I didn’t initialize the timer structures before using and initializing them. This left half of the attributes in these structures with defaults from the stack and TIM8 didn’t work. After I used TIM_TimeBaseStructInit and TIM_OCStructInit to initialize all defaults properly, TIM8 started to work as expected.

  11. ConZarks says:

    Hello again,

    it’s been a while since my last post. I’ve come to realize that is just the PA6 error that causes all my problems.

    At first I thought it would be a cmsis or gcc problem, so I rewrote my program from scratch according to the manual’s functions without using any library. TIM1 worked great, as it was supposed to, using PB12 as TIM1_BKIN. Then I checked that I could also use PA6 as TIM1’s BKIN (which is TIM8’s BKIN as well). Guess what happened, it didn’t work at all. Just like TIM8.

    I haven’t tried to make a simple PWM yet, I’ll try and let you know as soon as I can.

    Here are the TIM1 codes I made:


    (doesn’t work)

  12. ConZarks says:

    Finally it worked,

    it seems the problem was that PA6 is always ON even if you press the RESET button on the board!! Don’t know why but I only chenged the polarity from high to low..

    The truth is I have another problem now. I want to control the phase shift between the two timers and each timers’ channels separately.

    If you see my code you’ll realize that with TOGGLE mode even if you change the pulse of a channel you can’t get over 180 degrees. That happens ’cause TIM_Period controls the time for each channel to stay ON and then the equal time OFF. So by changing TIM_Pulse you control only the 0-180 degrees.

    I noticed that, if you put TIM_Pulse=0 to one channel and TIM_Pulse=1 to another then you get complementary signals!! So I used for-loops to separate each channels’ frequency from 0-180 and 180-360. If you’ll see my code bellow you will understand.

    F1 controls the TIM1 phase shift of ch1 and ch2 and F2 the TIM8 phase shift of ch1 and ch2. F controls the phase shift of TIM1 and TIM8. The problem I have is when F1>180 and F+F2<180, or the opposite. It happens 'cause as you can see on the code, F1 starts with "0" whereas F+F2 starts with "1"..

    Please if you find any good solution to this or even ANOTHER solution let me know, thanks in advance!!


    • ConZarks says:

      PS: some comments in the code are not correct! They are from previous versions of the code!

  13. Mark says:

    Hello, Patrick!
    Maybe my question is out of topic. But I really need your help!
    I need to connect PSM Module. But for me it became really big problem! I have Rittal module and also I followed all instructures and even watched video on Youtube..But unsycessful. Can you share with me some additional informtion or advices. I will be very thankful!

  14. Aniket M says:

    Hi, i am driving stepper motor and for that i need to give PWM with variable frequency like from 300Hz to 3000Hz in steps of 200Hz Approx.
    So how to achieve that?
    Coz when i am configuring for low frequencies and switching it to next frequency transition is not smooth.
    Please give solution ASAP.
    Thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *