ESP32 ST7789 Brightness Control PWM vs. DAC

Hi folks,

I‘ve hooked up an ST7789 IPS screen via SPI to an ESP32. There is a special pin for brightness control. I tried connecting this pin to any PWM output but it didn‘t work.

Only when I connected the brightness pin to one of the two DAC pins of the ESP32 it worked and I could change the brightness with the ledcwrite function.

Why is this? Shouldn‘t I be able to use any PWM output to control the brightness?

Nearly any pin on portA should work with the ESP32 LEDC API.

Please post a link to the actual display that you have bought.
Most boards are designed for PWM. If you provide a genuine Analog signal the LED transistor will be operating in its linear region. This means a higher power dissipation in the SMD transistor. e.g. J3Y on the common 1.3 inch 3.3V 7-pin blue displays.

You will probably get away with it on small 1.3 inch. But a large 3.2 inch panel will take much more current. Hence the reason for asking for a link.

Note that some boards omit the transistor. These expect you to provide external control e.g. series resistor. This is BAD for DAC pin and BAD for PWM too. Neither pin is intended for high LED currents.

Life is so much simpler when people provide a link to their display. Then we don't have to guess. We can give a straight answer.


Dimming a Adafruit_ST7789

My setup routine for the LEDC.

void setup()
ledcSetup( 4, 12000, 8 ); // ledc: 4  => Group: 0, Channel: 2, Timer: 1, led frequency, resolution  bits 
  ledcAttachPin( GPIO_NUM_12, 4 );   // gpio number and channel
  ledcWrite( 4, 0 ); // write to channel number 4}

My dimming routine.

void fDoTheDisplayThing( void * parameter )
  tft.init( 240, 320 ); // Init ST7789 320x240
  tft.setRotation( 3 );
  tft.setTextSize( 3 );
  tft.fillScreen( ST77XX_BLACK );
  tft.setTextWrap( false );
  struct stu_eData px_eData;
  int OneTwoThree = 0;
  int countUpDown = 255;
  ledcWrite( 4, countUpDown ); //backlight set
  int dimDelaytime = 7;
  for (;;)
    if ( xQueueReceive(xQ_eData, &px_eData, portMAX_DELAY) == pdTRUE )
      for ( countUpDown; countUpDown-- > 0; )
        ledcWrite( 4, countUpDown ); // write to channel number 4, dim backlight
        vTaskDelay( dimDelaytime );
      tft.setCursor( 0, 0 );
      if ( OneTwoThree == 1 )
        tft.setTextColor( ST77XX_RED );
      if ( OneTwoThree == 2 )
        tft.setTextColor( ST77XX_WHITE );
      if ( OneTwoThree == 3 )
        tft.setTextColor( ST77XX_BLUE );
        OneTwoThree = 0;
      tft.println( "Temp " + String(px_eData.Temperature) + "F" );
      tft.setCursor( 0, 30 );
      tft.println( "Hum  " + String(px_eData.Humidity) + "%" );
      tft.setCursor( 0, 60 );
      tft.println( "Pres " + String(px_eData.Pressure) + "mmHg" );
      tft.setCursor( 0, 90 );
      tft.println( "AQI  " + String(px_eData.IAQ) + "%" );
      tft.setCursor( 0, 120 );
      tft.println( "RM0  " + String(px_eData.RM0) + "%" );
      tft.setCursor( 0, 150 );
      tft.println( "PM2  " + String(px_eData.PM2) + "ug/m3" );
      tft.setCursor( 0, 180 );
      if ( px_eData.PM2 <= 35.0f )
        //tft.setTextColor( ST77XX_GREEN );
        tft.println( "PM2 is Excellent" );
      if ( (px_eData.PM2 > 35.0f) && px_eData.PM2 <= 75.0f )
        tft.println( "PM2 is Average" );
      if ( (px_eData.PM2 > 75.0f) && px_eData.PM2 <= 115.0f )
        tft.println( "PM2 is Light" );
      if ( (px_eData.PM2 > 115.0f) && px_eData.PM2 <= 150.0f )
        tft.println( "PM2 is Moderate" );
      if ( (px_eData.PM2 > 150.0f) && px_eData.PM2 <= 250.0f )
        tft.println( "PM2 is Heavy" );
      if ( (px_eData.PM2 > 250.0f) )
        tft.println( "PM2 is Serious" );
      //brighten blacklight level
      vTaskDelay( 400 ); // wait for screen update to be done
      for ( countUpDown; countUpDown <= 255; countUpDown++ )
        ledcWrite( 4, countUpDown ); // write to channel number 4
        vTaskDelay( dimDelaytime  );
      //log_i( "DoTheBME280Thing high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
    } //if ( xQueueReceive(xQ_eData, &px_eData, portMAX_DELAY) == pdTRUE )
  } //for (;;)
  vTaskDelete( NULL );
} //void fDoTheDisplayTHing( void * parameter )

Thank you for your reply David. I thought as an IPS screen the one I used should be a common occurrence without hardware variations. Nevertheless, here is a link to my display. It's a generic 1.33" IPS ST7789 3.3V.

@Idahowalker I do believe that ledc and PWM work as expected with an ESP32, but it won't do anything with my display, except if I hook it up to one of the DACs.

And thank you as well for your code. Mine is similar, I just used ledcSetup(0, 1000, 8) and ledcAttachPin(25, 0) - here for example pin 35 won't work.

A possible issue with using the hardware timer is when other devices use the timer without a timer designation.

Example the ESP32ServoLibrary which does not assign timers but lets the OS choose a timer. The automated timer choose done by the ESp32 starts at timer 0. If your application is also using timer 0 and ESP32Servo library is being used, the servo library messes with the timer programming. It's better to use the timers in reverse order, starting with using timer 3 and work down to timer 0.

I do not have ANY other dimming circuits to the same display as you are using and it dimms wonderfully well.

Of course GPIO_NUM_35 does not work for the timer.

Did you read/heed the above post?

GPIO_NUM_35 is on portB.

Thanks for the link. That was easy.

It is always wise to post a link to your display. Hardware varies. It avoids guesswork.

That particular board has a transistor U2 which is designed for PWM. It is not intended for linear operation.

Actually, the ESP32 GPIO pins can source and sink relatively high currents. Similar to AVR. Much higher than most ARM chips.
So the ESP32 will not be harmed by driving U2 in a linear fashion from the DAC. U2 will probably be ok for the 1.3 inch backlight.

I suppose that I have only used it as 100% ON or OFF. I will investigate PWM for myself. It should work just fine on that display. And be safer than using the DAC.

GPIO35 is an Analog-only pin. It should really be called GPIO at all.


I used this pinout diagram which stated that GPIO 35 is a PWM pin. The working GPIO 25 is a few pins below.

GPIO_NUM_35 is on portB. One of PortB's quirks is that the port is INPUT ONLY. It will be important for you to know and understand the ESP32 GPIO quirks.

Also, I did not see in your code where you setup the GPIO_25 as a DAC pin. I see in your code that you set GPIO_NUM_25 as an output pin connected and controlled by the HRT.

I do not see that, in the diagram you posted that GPIO_NUM_35 is a PWM pin. I see the diagram showing that GPIO_NUM_35 is a RTCIO5, ADC1_7 or a vdet2 but not PWM.

As a note, with the advent of the ESP developer model S, the pins on portB can be used like the pins on portA but use the RTC API to set up the pins instead of the GPIO API.

Thank you @Idahowalker and @david_prentice . Both of you helped me to understand and solve my problem. So I better move the brightness control over to the right side of the MCU onto PortA.

I thought the wave-like line connecting the specs with the boards pins signifies a PWM pin as it is shown in the legend.

That's what I did and nothing else. Just the two lines and it worked.

I'll spend some time learning the GPIO quirks of the ESP32, this will be badly needed.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.