Created
April 22, 2026 14:24
-
-
Save mpilone/2302813a27636880e0a2ca3c9b499f13 to your computer and use it in GitHub Desktop.
ST7735 Driver based on Pi4J
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import com.pi4j.io.gpio.digital.DigitalOutput; | |
| import com.pi4j.io.spi.Spi; | |
| import java.awt.*; | |
| import java.awt.image.BufferedImage; | |
| /** | |
| * Implementation of an ST7735 LCD driver. The driver supports initializing the display, drawing images, and | |
| * shutting down the display. The implementation is highly inspired by the Adafruit CircuitPython library. | |
| * | |
| * @author mpilone | |
| * @see <a href="http://www.adafruit.com/datasheets/ST7735R_V0.2.pdf">ST7735R Datasheet</a> | |
| * @see <a href="https://github.com/adafruit/Adafruit-GFX-Library">Adafruit GFX Library</a> | |
| * @see <a href="https://github.com/adafruit/Adafruit-ST7735-Library">Adafruit ST7735 Library</a> | |
| * @since 12/19/24. | |
| */ | |
| public class ST7735 { | |
| public static final int WIDTH = 128; | |
| public static final int HEIGHT = 160; | |
| /** | |
| * The maximum width the ST7735. The device should be configured at the hardware level to display | |
| * {@link #WIDTH} pixels, but the actual display RAM will store more data, and we found that it needs | |
| * to be cleared to avoid artifacts on the screen. | |
| */ | |
| private static final int DISPLAY_RAM_WIDTH = 132; | |
| /** | |
| * The maximum height the ST7735. The device should be configured at the hardware level to display | |
| * {@link #HEIGHT} pixels, but the actual display RAM will store more data, and we found that it needs | |
| * to be cleared to avoid artifacts on the screen. | |
| */ | |
| private static final int DISPLAY_RAM_HEIGHT = 162; | |
| /** | |
| * The display RAM size in bytes. 18 bits per pixel per the datasheet. | |
| */ | |
| private static final int DISPLAY_RAM_SIZE = (DISPLAY_RAM_WIDTH * DISPLAY_RAM_HEIGHT * 18) / 8; | |
| private final byte ST77XX_NOP = (byte) 0x00; | |
| private final byte ST77XX_SWRESET = (byte) 0x01; | |
| private final byte ST77XX_RDDID = (byte) 0x04; | |
| private final byte ST77XX_RDDST = (byte) 0x09; | |
| private final byte ST77XX_SLPIN = (byte) 0x10; | |
| private final byte ST77XX_SLPOUT = (byte) 0x11; | |
| private final byte ST77XX_PTLON = (byte) 0x12; | |
| private final byte ST77XX_NORON = (byte) 0x13; | |
| private final byte ST77XX_INVOFF = (byte) 0x20; | |
| private final byte ST77XX_INVON = (byte) 0x21; | |
| private final byte ST77XX_DISPOFF = (byte) 0x28; | |
| private final byte ST77XX_DISPON = (byte) 0x29; | |
| private final byte ST77XX_CASET = (byte) 0x2A; | |
| private final byte ST77XX_RASET = (byte) 0x2B; | |
| private final byte ST77XX_RAMWR = (byte) 0x2C; | |
| private final byte ST77XX_RAMRD = (byte) 0x2E; | |
| private final byte ST77XX_PTLAR = (byte) 0x30; | |
| private final byte ST77XX_TEOFF = (byte) 0x34; | |
| private final byte ST77XX_TEON = (byte) 0x35; | |
| private final byte ST77XX_MADCTL = (byte) 0x36; | |
| private final byte ST77XX_COLMOD = (byte) 0x3A; | |
| private final byte ST77XX_MADCTL_MY = (byte) 0x80; | |
| private final byte ST77XX_MADCTL_MX = (byte) 0x40; | |
| private final byte ST77XX_MADCTL_MV = (byte) 0x20; | |
| private final byte ST77XX_MADCTL_ML = (byte) 0x10; | |
| private final byte ST77XX_MADCTL_RGB = (byte) 0x00; | |
| private final byte ST77XX_RDID1 = (byte) 0xDA; | |
| private final byte ST77XX_RDID2 = (byte) 0xDB; | |
| private final byte ST77XX_RDID3 = (byte) 0xDC; | |
| private final byte ST77XX_RDID4 = (byte) 0xDD; | |
| private final byte ST7735_MADCTL_BGR = (byte) 0x08; | |
| private final byte ST7735_MADCTL_MH = (byte) 0x04; | |
| private final byte ST7735_FRMCTR1 = (byte) 0xB1; | |
| private final byte ST7735_FRMCTR2 = (byte) 0xB2; | |
| private final byte ST7735_FRMCTR3 = (byte) 0xB3; | |
| private final byte ST7735_INVCTR = (byte) 0xB4; | |
| private final byte ST7735_DISSET5 = (byte) 0xB6; | |
| private final byte ST7735_PWCTR1 = (byte) 0xC0; | |
| private final byte ST7735_PWCTR2 = (byte) 0xC1; | |
| private final byte ST7735_PWCTR3 = (byte) 0xC2; | |
| private final byte ST7735_PWCTR4 = (byte) 0xC3; | |
| private final byte ST7735_PWCTR5 = (byte) 0xC4; | |
| private final byte ST7735_VMCTR1 = (byte) 0xC5; | |
| private final byte ST7735_PWCTR6 = (byte) 0xFC; | |
| private final byte ST7735_GMCTRP1 = (byte) 0xE0; | |
| private final byte ST7735_GMCTRN1 = (byte) 0xE1; | |
| /** | |
| * The GPIO pin corresponding to the D/C line on the display. | |
| */ | |
| private final DigitalOutput dcPin; | |
| /** | |
| * The internal SPI device. | |
| */ | |
| private final Spi spi; | |
| private boolean optimizeDraw = true; | |
| private BufferedImage cachedImg; | |
| public ST7735(DigitalOutput rstPin, DigitalOutput dcPin, Spi spi) { | |
| this.dcPin = dcPin; | |
| this.spi = spi; | |
| // Hardware reset | |
| rstPin.high(); | |
| Threads.safeSleep(50); | |
| rstPin.low(); | |
| Threads.safeSleep(50); | |
| rstPin.high(); | |
| Threads.safeSleep(50); | |
| // I'm not sure which of these initialization commands we need. This is the full list from the | |
| // sample Adafruit code, but I don't know if they apply to all devices or just the Adafruit module. | |
| // Commenting them out doesn't seem to make a difference, but it is possible they are already set | |
| // on my hardware from initial testing with CircuitPython. The reference manual defines the | |
| // defaults after power-on. | |
| command(ST77XX_SWRESET); // 1: Software reset, 0 args, w/delay | |
| Threads.safeSleep(150); | |
| command(ST77XX_SLPOUT); // 2: Out of sleep mode, 0 args, w/delay | |
| Threads.safeSleep(150); | |
| // sendCommand(ST7735_FRMCTR1, // 3: Framerate ctrl - normal mode, 3 arg: | |
| // 0x01, 0x2C, 0x2D); // Rate = fosc/(1x2+40) * (LINE+2C+2D) | |
| // sendCommand(ST7735_FRMCTR2, // 4: Framerate ctrl - idle mode, 3 args: | |
| // 0x01, 0x2C, 0x2D); // Rate = fosc/(1x2+40) * (LINE+2C+2D) | |
| // sendCommand(ST7735_FRMCTR3, // 5: Framerate - partial mode, 6 args: | |
| // 0x01, 0x2C, 0x2D, // Dot inversion mode | |
| // 0x01, 0x2C, 0x2D); // Line inversion mode | |
| // sendCommand(ST7735_INVCTR, // 6: Display inversion ctrl, 1 arg: | |
| // 0x07); // No inversion | |
| // sendCommand(ST7735_PWCTR1, // 7: Power control, 3 args, no delay: | |
| // 0xA2, | |
| // 0x02, // -4.6V | |
| // 0x84); // AUTO mode | |
| // sendCommand(ST7735_PWCTR2, // 8: Power control, 1 arg, no delay: | |
| // 0xC5); // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD | |
| // sendCommand(ST7735_PWCTR3, // 9: Power control, 2 args, no delay: | |
| // 0x0A, // Opamp current small | |
| // 0x00); // Boost frequency | |
| // sendCommand(ST7735_PWCTR4, // 10: Power control, 2 args, no delay: | |
| // 0x8A, // BCLK/2, | |
| // 0x2A); // opamp current small & medium low | |
| // sendCommand(ST7735_PWCTR5, // 11: Power control, 2 args, no delay: | |
| // 0x8A, 0xEE); | |
| // sendCommand(ST7735_VMCTR1, // 12: Power control, 1 arg, no delay: | |
| // 0x0E); | |
| command(ST77XX_INVOFF); // 13: Don't invert display, no args | |
| command(ST77XX_MADCTL, // 14: Mem access ctl (directions), 1 arg: | |
| ST77XX_MADCTL_RGB); // row/col addr, top-bottom refresh | |
| command(ST77XX_COLMOD, // 15: set color mode, 1 arg, no delay: | |
| 0x05); // 16-bit color | |
| // command(ST7735_GMCTRP1, // 1: Gamma Adjustments (pos. polarity), 16 args, no delay: | |
| // 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides | |
| // 0x37, 0x32, 0x29, 0x2d, // accurate colors) | |
| // 0x29, 0x25, 0x2B, 0x39, | |
| // 0x00, 0x01, 0x03, 0x10); | |
| // command(ST7735_GMCTRN1, // 2: Gamma Adjustments (neg. polarity), 16 args, no delay: | |
| // 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides | |
| // 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) | |
| // 0x2E, 0x2E, 0x37, 0x3F, | |
| // 0x00, 0x00, 0x02, 0x10); | |
| command(ST77XX_NORON); // 3: Normal display on, no args, w/delay | |
| Threads.safeSleep(10); // 10 ms delay | |
| clearDisplayMemory(); | |
| setRotation(0); | |
| command(ST77XX_DISPON); // 4: Main screen turn on, no args w/delay | |
| Threads.safeSleep(10); // 10 ms delay | |
| } | |
| /** | |
| * Enables the optimized draw mode when drawing. In this mode, a cached version of the display image is kept in | |
| * memory and on redraw, only modified blocks are send to the display. This increases the load on the Java side | |
| * but it can greatly reduce the amound of data and redrawing on the display hardware which can help eliminate | |
| * flicker in high FPS applications. Defaults to true (enabled). | |
| * | |
| * @param optimizeDraw true to enable optimized drawing, false to always draw the provided image | |
| */ | |
| public void setOptimizeDraw(boolean optimizeDraw) { | |
| this.optimizeDraw = optimizeDraw; | |
| } | |
| /** | |
| * Convenience method for {@link #drawImage(BufferedImage, int, int)} that assumes a (0, 0) starting position. | |
| * | |
| * @param img the image to draw to the display | |
| */ | |
| public void drawImage(BufferedImage img) { | |
| drawImage(img, 0, 0); | |
| } | |
| /** | |
| * Draws the entire given image to the display, starting at the given coordinates on the display. If | |
| * {@link #setOptimizeDraw(boolean)} is enabled, only the modified portions of the image are written to the | |
| * display. The image must be type {@link BufferedImage#TYPE_USHORT_565_RGB}. | |
| * | |
| * @param img the image to draw to the display | |
| * @param x the start x coordinate on the display | |
| * @param y the start y coordinate on the display | |
| */ | |
| public void drawImage(BufferedImage img, int x, int y) { | |
| if (img.getType() != BufferedImage.TYPE_USHORT_565_RGB) { | |
| throw new IllegalArgumentException("Image type must be 565 RGB."); | |
| } | |
| if (optimizeDraw) { | |
| drawImageDelta(img, x, y); | |
| } | |
| else { | |
| imageRamWrite(img, x, y); | |
| } | |
| } | |
| /** | |
| * Writes the given image to the display RAM, starting at the given coordinates using the appropriate CASET, | |
| * RASET, and RAMWR commands. The entire image is written, | |
| * therefore, the image should be resized/sliced before calling this method if only a sub-image should be written. | |
| * | |
| * @param img the image to write | |
| * @param x the start x coordinate on the display | |
| * @param y the start y coordinate on the display | |
| */ | |
| private void imageRamWrite(BufferedImage img, int x, int y) { | |
| if (img.getType() != BufferedImage.TYPE_USHORT_565_RGB) { | |
| throw new IllegalArgumentException("Image type must be 565 RGB."); | |
| } | |
| // We can't convert the raster data directly because sub-images may be backed by larger | |
| // arrays. This is a trade-off when using BufferedImage.getSubimage which shares the backing | |
| // data buffer with the original image. We could write this image | |
| // to a new buffered image and then extract the raster, but I'm assuming that would | |
| // be slower because it requires an extra raster data buffer creation and data copy. However, fetching | |
| // pixel by pixel is probably the same amount of work. In either case, it's such a small amount of | |
| // data it probably doesn't matter. | |
| // WritableRaster raster = img.getRaster(); | |
| // DataBufferUShort buffer = (DataBufferUShort) raster.getDataBuffer(); | |
| // short[] imgData = buffer.getData(); | |
| // | |
| // for (int i = 0; i < imgData.length; i++) { | |
| // data[i * 2] = (byte) ((imgData[i] >> 8) & 0xFF); | |
| // data[i * 2 + 1] = (byte) (imgData[i] & 0xFF); | |
| // } | |
| // System.out.printf("Drawing block. x=%d, y=%d, width=%d, height=%d\n", | |
| // x, y, img.getWidth(), img.getHeight()); | |
| byte[] data = new byte[img.getWidth() * img.getHeight() * 2]; | |
| int dataIndex = 0; | |
| for (int row = 0; row < img.getHeight(); row++) { | |
| for (int col = 0; col < img.getWidth(); col++) { | |
| // 888 | |
| int rgb = img.getRGB(col, row); | |
| byte r = (byte) ((rgb & 0xFF0000) >> 16); | |
| byte g = (byte) ((rgb & 0xFF00) >> 8); | |
| byte b = (byte) ((rgb & 0xFF)); | |
| // 565 | |
| rgb = ((r >> 3) & 0x1f) << 11; | |
| rgb |= ((g >> 2) & 0x3f) << 5; | |
| rgb |= (b >> 3) & 0x1f; | |
| data[dataIndex++] = (byte) ((rgb >> 8) & 0xFF); | |
| data[dataIndex++] = (byte) (rgb & 0xFF); | |
| } | |
| } | |
| command(ST77XX_CASET, 0x00, x, 0x00, x + img.getWidth() - 1); | |
| command(ST77XX_RASET, 0x00, y, 0x00, y + img.getHeight() - 1); | |
| command(ST77XX_RAMWR); | |
| data(data); | |
| } | |
| /** | |
| * Draws only the changed blocks of the given image to the display. Blocks that have not changed will not be sent | |
| * to the display. | |
| * | |
| * @param img the image to draw | |
| * @param x the start x coordinate on the display | |
| * @param y the start y coordinate on the display | |
| */ | |
| private void drawImageDelta(BufferedImage img, int x, int y) { | |
| // Arbitrary block size. Smaller blocks mean more writes, but ideally fewer changed blocks. | |
| // Larger blocks mean fewer writes, but more changes in a single block. | |
| final int blockSize = 10; | |
| for (int blockY = 0; blockY < img.getHeight(); blockY += blockSize) { | |
| int blockHeight = Math.min(blockSize, img.getHeight() - blockY); | |
| for (int blockX = 0; blockX < img.getWidth(); blockX += blockSize) { | |
| int blockWidth = Math.min(blockSize, img.getWidth() - blockX); | |
| BufferedImage imgBlock = img.getSubimage(blockX, blockY, blockWidth, blockHeight); | |
| BufferedImage cachedImgBlock = cachedImg.getSubimage(x + blockX, y + blockY, blockWidth, blockHeight); | |
| if (!imgEquals(imgBlock, cachedImgBlock)) { | |
| // Blocks differ. | |
| // Write it to the hardware. | |
| imageRamWrite(imgBlock, x + blockX, y + blockY); | |
| // Update the cached image. | |
| Graphics2D g = cachedImgBlock.createGraphics(); | |
| g.drawImage(imgBlock, 0, 0, null); | |
| g.dispose(); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Returns true if the two images are equal. That is, they contain the same pixel data. | |
| * | |
| * @param img1 the first image to compare | |
| * @param img2 the second image to compare | |
| * @return true if equal, false otherwise | |
| */ | |
| private static boolean imgEquals(BufferedImage img1, BufferedImage img2) { | |
| if (img1.getWidth() != img2.getWidth() || img1.getHeight() != img2.getHeight()) { | |
| return false; | |
| } | |
| for (int x = 0; x < img1.getWidth(); x++) { | |
| for (int y = 0; y < img1.getHeight(); y++) { | |
| int rgb1 = img1.getRGB(x, y); | |
| int rgb2 = img2.getRGB(x, y); | |
| if (rgb1 != rgb2) { | |
| // Stop comparing as soon as we find a difference. | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| // We can't compare the raster data directly because sub-images may be backed by larger | |
| // arrays. This is a trade-off when using BufferedImage.getSubimage which shares the backing | |
| // data buffer with the original image. | |
| // WritableRaster raster = img1.getRaster(); | |
| // DataBufferUShort buffer = (DataBufferUShort) raster.getDataBuffer(); | |
| // short[] img1Data = buffer.getData(); | |
| // | |
| // raster = img2.getRaster(); | |
| // buffer = (DataBufferUShort) raster.getDataBuffer(); | |
| // short[] img2Data = buffer.getData(); | |
| // | |
| // return Arrays.equals(img1Data, img2Data); | |
| } | |
| /** | |
| * Convenience method for {@link #command(byte, byte...)} that takes the lowest byte of each integer argument. | |
| * The upper 3 bytes of the integer are ignored. | |
| * | |
| * @param command the command to send | |
| * @param args the arguments for the command | |
| */ | |
| private void command(byte command, int... args) { | |
| byte[] byteArgs = new byte[args.length]; | |
| for (int i = 0; i < args.length; i++) { | |
| byteArgs[i] = (byte) args[i]; | |
| } | |
| command(command, byteArgs); | |
| } | |
| /** | |
| * Sends the given command to the display, followed by the command arguments as data. | |
| * | |
| * @param command the command to send | |
| * @param args the arguments for the command | |
| */ | |
| private void command(byte command, byte... args) { | |
| dcPin.low(); | |
| spi.write(command); | |
| if (args != null && args.length > 0) { | |
| data(args); | |
| } | |
| } | |
| /** | |
| * Sends the data to the display. | |
| * | |
| * @param data the data bytes | |
| */ | |
| private void data(byte[] data) { | |
| // System.out.println("Sending data: " + data.length); | |
| dcPin.high(); | |
| // Default Pi5 SPI buffer size is 4096. We might want to add some kind of support for detecting | |
| // or querying the SPI for its buffer size. | |
| // See https://github.com/dotnet/iot/issues/997#issuecomment-598228905 | |
| int len; | |
| for (int offset = 0; offset < data.length; offset += len) { | |
| len = Math.min(4096, data.length - offset); | |
| spi.write(data, offset, len); | |
| } | |
| } | |
| /** | |
| * Sets the rotation of the display. Once rotated, the width and height of the display may be swapped. All draw | |
| * operations must use the appropriately rotated width and height. For example, in the default rotation (0), | |
| * width = {@link #WIDTH}. When rotated (90), width = {@link #HEIGHT}. | |
| * | |
| * @param rotation the rotation angle (0, 90, 180, or 270) | |
| */ | |
| public void setRotation(int rotation) { | |
| int madctl; | |
| int rotatedWidth; | |
| int rotatedHeight; | |
| switch (rotation) { | |
| case 0 -> { | |
| madctl = ST77XX_MADCTL_RGB; | |
| rotatedWidth = WIDTH; | |
| rotatedHeight = HEIGHT; | |
| } | |
| case 90 -> { | |
| madctl = ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST77XX_MADCTL_RGB; | |
| rotatedWidth = HEIGHT; | |
| rotatedHeight = WIDTH; | |
| } | |
| case 180 -> { | |
| madctl = ST77XX_MADCTL_MX | ST77XX_MADCTL_MY | ST77XX_MADCTL_RGB; | |
| rotatedWidth = WIDTH; | |
| rotatedHeight = HEIGHT; | |
| } | |
| case 270 -> { | |
| madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV | ST77XX_MADCTL_RGB; | |
| rotatedWidth = HEIGHT; | |
| rotatedHeight = WIDTH; | |
| } | |
| default -> throw new IllegalArgumentException("Rotation must be 0, 90, 180, or 270."); | |
| } | |
| command(ST77XX_MADCTL, madctl); | |
| cachedImg = new BufferedImage(rotatedWidth, rotatedHeight, BufferedImage.TYPE_USHORT_565_RGB); | |
| var gfx = this.cachedImg.getGraphics(); | |
| gfx.setColor(Color.WHITE); | |
| gfx.fillRect(0, 0, rotatedWidth, rotatedHeight); | |
| // Update the screen to match the cached image. | |
| imageRamWrite(cachedImg, 0, 0); | |
| } | |
| /** | |
| * Sets if the display is on, that is, rendering pixels. Defaults to true (on). | |
| * | |
| * @param displayOn true to turn the display on and render pixels, false to blank the screen | |
| */ | |
| public void setDisplayOn(boolean displayOn) { | |
| command(displayOn ? ST77XX_DISPON : ST77XX_DISPOFF); | |
| } | |
| /** | |
| * Sets if the display is in sleep (low power) mode. Defaults to false (sleep out). | |
| * | |
| * @param sleepIn true to go into sleep mode, false to come out of sleep mode | |
| */ | |
| public void setSleepIn(boolean sleepIn) { | |
| command(sleepIn ? ST77XX_SLPIN : ST77XX_SLPOUT); | |
| } | |
| /** | |
| * Reset the display data RAM to 0 (black). If the RAM isn't reset, we've seen displays | |
| * show random noise until the first display is written. The full available memory on | |
| * the device is cleared, even if we're only using a smaller display. For example, some ST7735 | |
| * displays support 132x162 resolution, but will be configured for 128x160 at the hardware level. | |
| * If the entire display data RAM isn't cleared, we've seen pixel noise on the edges of the screen. | |
| * We haven't seen an issue writing this memory size to all displays, even if they don't advertise | |
| * the larger size memory, but we should keep this in mind if the display doesn't prevent memory | |
| * overflows by ignoring data beyond the display data RAM size. | |
| */ | |
| private void clearDisplayMemory() { | |
| byte[] data = new byte[DISPLAY_RAM_SIZE]; | |
| command(ST77XX_CASET, 0x00, 0, 0x00, DISPLAY_RAM_WIDTH - 1); | |
| command(ST77XX_RASET, 0x00, 0, 0x00, DISPLAY_RAM_HEIGHT - 1); | |
| command(ST77XX_RAMWR); | |
| data(data); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment