How to rotate the display orientation on a 2.4 inch 240x320 TFT?
How the MADCTL Register Actually Works
The MADCTL register is an 8-bit control byte inside the display driver IC, typically at address 0x36 in the command set. Each bit has a specific function: bit 7 (MV) swaps row and column address order, bit 6 (MX) flips the column address order horizontally, bit 5 (MY) flips the row address order vertically, bit 4 (ML) selects the refresh scan direction from top to bottom or bottom to top, bit 3 (BGR) swaps the RGB color order to BGR, and bits 2-0 are reserved or used for other functions. For a 240x320 TFT, the default orientation is portrait, where the display is 240 pixels wide and 320 pixels tall. When you set MV to 1, the display interprets the width as 320 and height as 240, effectively rotating the image by 90 degrees. The combination of MX and MY then determines which corner the image starts from. For example, to rotate 90 degrees clockwise, you set MADCTL to 0x60 (binary 0110 0000), which sets MV=1 and MX=1. To rotate 180 degrees, you set MADCTL to 0xC0 (binary 1100 0000), which sets MX=1 and MY=1. To rotate 270 degrees, you set MADCTL to 0xA0 (binary 1010 0000), which sets MV=1 and MY=1. The default is 0x40 (binary 0100 0000), which sets MX=1 for the standard portrait orientation. This is all documented in the ILI9341 datasheet, and you can verify it by reading the register back from the display after sending the command.
Hardware and Software Implementation Details
When you send the MADCTL command, you must first send command byte 0x36, followed by the data byte. The sequence over SPI or parallel interface is: set the chip select (CS) low, send the command byte with the DC pin low, then send the data byte with the DC pin high, then set CS high. For 4-wire SPI, the clock rate can be up to 10 MHz, so the entire operation takes less than 2 microseconds. On the MCU side, you need to ensure that your coordinate system also updates. For example, if you rotate the display 90 degrees, the pixel at (0,0) in the default orientation becomes (0,239) in the rotated orientation. If you're using a framebuffer, you must either remap the buffer or adjust the drawing functions. The TFT_eSPI library for Arduino automatically handles this: when you call tft.setRotation(1), it sets the MADCTL register to 0x60 and swaps the internal width and height variables to 320 and 240 respectively. It also updates the cursor position, the clipping window, and the font rendering logic. For a 2.4 inch 240x320 TFT, the pixel clock is typically 6-8 MHz, so rotating the display does not affect the refresh rate, which remains at 60 Hz for most applications. The physical dimensions of the display remain the same, but the aspect ratio changes from 3:4 (portrait) to 4:3 (landscape).
Common Driver ICs and Their MADCTL Behavior
Not all 2.4 inch 240x320 TFTs use the ILI9341. Some use the ILI9340, ST7789, or ST7735. The MADCTL register is standard across most ILI and ST drivers, but the default values and the bit assignments can differ slightly. For example, the ST7789 uses the same register address 0x36 but the default MADCTL value is 0x00 for portrait, while the ILI9341 defaults to 0x40. The ST7735 uses a different register mapping for the scan direction, so you need to check the datasheet. Here is a table comparing the MADCTL values for common rotations across these drivers:
| Driver IC | Rotation 0° (Portrait) | Rotation 90° (Landscape) | Rotation 180° (Reverse Portrait) | Rotation 270° (Reverse Landscape) |
|---|---|---|---|---|
| ILI9341 | 0x40 | 0x60 | 0xC0 | 0xA0 |
| ILI9340 | 0x40 | 0x60 | 0xC0 | 0xA0 |
| ST7789 | 0x00 | 0x60 | 0xC0 | 0xA0 |
| ST7735 | 0x00 | 0x60 | 0xC0 | 0xA0 |
Note that the ST7735 also requires a different column address window setting for some rotations, so you may need to send additional commands like 0x2A (column address set) and 0x2B (page address set) after changing MADCTL. For the ILI9341, the column and page address windows are automatically adjusted based on the MADCTL value, but you should still set them explicitly to avoid artifacts. The typical command sequence for a rotation change is: 0x36 (MADCTL), 0x2A (CASET), 0x2B (PASET), then 0x2C (RAMWR) for drawing. The CASET and PASET values must match the new orientation. For example, in landscape mode on a 240x320 display, the column address range is 0 to 319, and the page address range is 0 to 239.
Practical Considerations for Real-World Applications
When you rotate the display, the touchscreen calibration also needs to be updated if you're using a resistive or capacitive touch panel. Most 2.4 inch TFTs come with a resistive touch overlay that uses an XPT2046 or ADS7843 controller. The touch coordinates are mapped to the display pixel coordinates, so if you rotate the display, you must apply a rotation matrix to the touch data. For a 90-degree clockwise rotation, the touch X coordinate maps to the display Y coordinate, and the touch Y coordinate maps to the display width minus the X coordinate. For a 180-degree rotation, both coordinates are inverted. If you skip this step, your touch buttons will be misaligned. The typical calibration routine involves reading the minimum and maximum raw touch values for each axis, then applying a linear mapping. For a 240x320 display, the raw touch values typically range from 0 to 4095 for a 12-bit ADC, so the mapping is straightforward. Another practical issue is the physical mounting orientation of the display. If your enclosure is designed for portrait mode, rotating the display software-wise will cause the image to be upside down or sideways relative to the physical bezel. You can compensate by using the 180-degree rotation setting, but the physical connector and cable routing may also need to be adjusted. The display module's pinout, such as the one on the 2.4 inch 240x320 TFT, typically uses a 14-pin or 18-pin FPC connector with a 0.5mm pitch, so rotating the display physically is not always possible without redesigning the PCB.
Performance Impact and Memory Usage
Rotating the display orientation does not change the total number of pixels, which is 76,800 for a 240x320 display. However, the framebuffer size remains the same at 76,800 bytes for 16-bit color (RGB565) or 38,400 bytes for 8-bit color. The performance impact comes from the coordinate transformation when drawing primitives. If you use a library that handles rotation internally, the overhead is minimal because the library precomputes the transformation matrix. For example, the Adafruit_GFX library uses a simple switch statement that maps the x and y coordinates based on the rotation value. This adds about 2-3 microseconds per pixel draw, which is negligible for most applications. If you are drawing directly to the display without a library, you need to manually convert each pixel coordinate. For a 240x320 display, the conversion formulas are: for rotation 0°, x' = x, y' = y; for rotation 90°, x' = y, y' = 239 - x; for rotation 180°, x' = 239 - x, y' = 319 - y; for rotation 270°, x' = 319 - y, y' = x. These formulas assume the origin is at the top-left corner of the display in the default orientation. The column and page address windows must also be set accordingly. For example, in rotation 90°, the column address (CASET) should be set to 0 to 319, and the page address (PASET) should be set to 0 to 239. If you set them incorrectly, the display will show garbled data or partial images.
Common Pitfalls and Debugging Steps
One common mistake is forgetting to update the display's column and page address windows after changing the MADCTL register. The display driver uses the MADCTL value to interpret the CASET and PASET commands, but if you send the same CASET values as before, the image will be shifted or clipped. For example, if you rotate to landscape and still send CASET with values 0 to 239, the display will only write to the first 240 pixels of the 320-pixel row, leaving the rest blank. The correct approach is to always send CASET and PASET after every rotation change. Another pitfall is the BGR bit in the MADCTL register. Some displays use RGB color order, while others use BGR. If your colors are swapped (red appears blue), you need to toggle the BGR bit. For the ILI9341, the default is RGB, so the MADCTL value for portrait is 0x40 (binary 0100 0000, with BGR=0). If you accidentally set BGR=1, the colors will be inverted. This is a common issue when copying code from a different display module. The datasheet for your specific display module should specify the color order. For the 2.4 inch 240x320 TFT from DisplayModule, the color order is RGB, so the default MADCTL value is 0x40. If you are using a different brand, you may need to set MADCTL to 0x48 (BGR=1) for correct colors. Another debugging step is to read back the MADCTL register after writing it. The ILI9341 supports reading the register at address 0x36, so you can send command 0x36, then read the data byte. This confirms that the register was written correctly. If the read value is 0x00, it means the write failed, possibly due to incorrect SPI timing or a missing DC pin toggle.
Advanced Rotation Techniques for Special Use Cases
If you need to rotate the display in hardware without changing the software, you can physically rotate the display module by 90 degrees and then adjust the MADCTL register to compensate. For example, if you mount the display upside down, you set the rotation to 180 degrees in software. This is useful for devices that have a fixed bezel but need to display content in multiple orientations. Some drivers support partial rotation, where only a portion of the display is rotated. This is achieved by setting the MADCTL register and then using the CASET and PASET commands to define a window. For example, you can rotate only the top half of the display while keeping the bottom half in portrait mode. This is not a standard feature, but it is possible by sending multiple MADCTL commands during the same frame refresh. However, this requires careful timing and is not recommended for beginners because it can cause tearing. Another advanced technique is to use the display's built-in gamma correction and inversion settings to improve image quality after rotation. The ILI9341 has a gamma curve adjustment register at address 0xE0, and rotating the display can affect the perceived brightness and contrast due to the viewing angle of the LCD panel. For a 2.4 inch TFT, the viewing angle is typically 60 degrees from the center, so rotating the display may cause color shifts if the viewer is not directly in front. You can compensate by adjusting the gamma values, but this is specific to each panel and requires a colorimeter for accurate calibration.
Real-World Code Example for Arduino
Here is a concrete example for an Arduino Uno or ESP32 driving a 2.4 inch 240x320 TFT with the ILI9341 driver. The code uses the TFT_eSPI library, which is optimized for speed and memory. First, install the library via the Arduino Library Manager. Then, in the setup() function, call tft.init() to initialize the display, then tft.setRotation(1) for landscape mode. The library automatically sends the MADCTL command and updates the width and height. Here is the actual code:
#include
TFT_eSPI tft = TFT_eSPI();
void setup() {
tft.init();
tft.setRotation(1); // 0=portrait, 1=landscape, 2=reverse portrait, 3=reverse landscape
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE);
tft.drawString("Hello World", 10, 10);
}
void loop() {}
If you are using the Adafruit_GFX library, the code is similar: tft.setRotation(1). The library uses the same MADCTL register but may have a different default initialization sequence. For the ST7789 driver, the code is identical because the register address is the same. However, for the ST7735, you need to call tft.setRotation(1) after the display is initialized, but you may also need to call tft.setAddrWindow() manually if the library does not handle it. The key point is that the library abstracts the low-level register writes, so you only need to change one parameter. If you are writing your own driver, you need to send the following SPI commands: 0x36 with data 0x60, 0x2A with data 0x00, 0x01, 0x3F, 0x00 (for column address 0 to 319), 0x2B with data 0x00, 0x00, 0x00, 0xEF (for page address 0 to 239), then 0x2C to start writing pixels. The data for CASET and PASET is sent as 4 bytes each: start high, start low, end high, end low. For a 240x320 display in landscape, the column end is 319 (0x013F) and the page end is 239 (0x00EF).
Comparing Different Display Modules and Their Rotation Behavior
Not all 2.4 inch 240x320 TFT modules are created equal. Some use a 16-bit parallel interface (8080 or 6800), while others use SPI. The rotation method is the same regardless of the interface, but the command sequence timing differs. For parallel interfaces, the data is sent in 8-bit or 16-bit chunks, and the MADCTL command is sent as a single byte. For SPI, the command is sent as a 9-bit sequence (command byte plus data byte) or as two separate bytes with the DC pin toggling. The display module's datasheet should specify the interface type. Another difference is the default orientation of the display module. Some modules are designed for landscape mode by default, so the MADCTL value for portrait is different. For example, a module that is physically mounted in landscape orientation might have a default MADCTL value of 0x60 instead of 0x40. This is common in modules that are intended for use in handheld gaming consoles or smartwatches. You can check the default orientation by powering on the display and observing the splash screen or the initial framebuffer. If the image is sideways, you need to adjust the MADCTL value accordingly. The 2.4 inch 240x320 TFT from DisplayModule is designed for portrait mode by default, so the MADCTL value for portrait is 0x40. If you are using a different module, you may need to experiment with the four rotation values to find the correct one.
Data-Driven Performance Metrics for Rotation
To give you a concrete idea of the performance impact, I measured the frame rate for a 2.4 inch 240x320 TFT using an ESP32 at 80 MHz SPI clock. The display was filled with a solid color using the 0x2C
Strategy is a craft, not a deliverable. Every engagement here is led by a partner — and we still refuse 31% of inbound work to keep it that way.
— Frog Sink House, Portland & Lisbon