| // Copyright 2022 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_display_driver_st7789/display_driver.h" |
| |
| #include <array> |
| #include <cstddef> |
| |
| #include "pw_digital_io/digital_io.h" |
| #include "pw_framebuffer/rgb565.h" |
| #include "pw_spin_delay/delay.h" |
| |
| using pw::color::color_rgb565_t; |
| using pw::digital_io::State; |
| using pw::spi::ChipSelectBehavior; |
| using pw::spi::Device; |
| using std::array; |
| using std::byte; |
| |
| namespace pw::display_driver { |
| |
| namespace { |
| |
| constexpr array<byte, 0> kEmptyArray; |
| |
| // ST7789 Display Registers |
| // clang-format off |
| #define ST7789_SWRESET 0x01 |
| #define ST7789_TEOFF 0x34 |
| #define ST7789_TEON 0x35 |
| #define ST7789_MADCTL 0x36 |
| #define ST7789_COLMOD 0x3A |
| #define ST7789_GCTRL 0xB7 |
| #define ST7789_VCOMS 0xBB |
| #define ST7789_LCMCTRL 0xC0 |
| #define ST7789_VDVVRHEN 0xC2 |
| #define ST7789_VRHS 0xC3 |
| #define ST7789_VDVS 0xC4 |
| #define ST7789_FRCTRL2 0xC6 |
| #define ST7789_PWCTRL1 0xD0 |
| #define ST7789_PORCTRL 0xB2 |
| #define ST7789_GMCTRP1 0xE0 |
| #define ST7789_GMCTRN1 0xE1 |
| #define ST7789_INVOFF 0x20 |
| #define ST7789_SLPOUT 0x11 |
| #define ST7789_DISPON 0x29 |
| #define ST7789_GAMSET 0x26 |
| #define ST7789_DISPOFF 0x28 |
| #define ST7789_RAMWR 0x2C |
| #define ST7789_INVON 0x21 |
| #define ST7789_CASET 0x2A |
| #define ST7789_RASET 0x2B |
| |
| // MADCTL Bits (See page 215: MADCTL (36h): Memory Data Access Control) |
| #define ST7789_MADCTL_ROW_ORDER 0b10000000 |
| #define ST7789_MADCTL_COL_ORDER 0b01000000 |
| #define ST7789_MADCTL_SWAP_XY 0b00100000 |
| #define ST7789_MADCTL_SCAN_ORDER 0b00010000 |
| #define ST7789_MADCTL_RGB_BGR 0b00001000 |
| #define ST7789_MADCTL_HORIZ_ORDER 0b00000100 |
| // clang-format on |
| |
| } // namespace |
| |
| DisplayDriverST7789::DisplayDriverST7789(const Config& config) |
| : data_cmd_gpio_(config.data_cmd_gpio), |
| reset_gpio_(config.reset_gpio), |
| spi_device_(config.spi_device), |
| spi_helper_(config.spi_helper), |
| screen_width_(config.screen_width), |
| screen_height_(config.screen_height) {} |
| |
| void DisplayDriverST7789::SetMode(Mode mode) { |
| // Set the D/CX pin to indicate data or command values. |
| if (mode == Mode::kData) { |
| data_cmd_gpio_.SetState(State::kActive); |
| } else { |
| data_cmd_gpio_.SetState(State::kInactive); |
| } |
| } |
| |
| Status DisplayDriverST7789::WriteCommand(Device::Transaction& transaction, |
| const Command& command) { |
| SetMode(Mode::kCommand); |
| byte buff[1]{static_cast<byte>(command.command)}; |
| auto s = transaction.Write(buff); |
| if (!s.ok()) |
| return s; |
| |
| SetMode(Mode::kData); |
| if (command.command_data.empty()) { |
| return OkStatus(); |
| } |
| return transaction.Write(command.command_data); |
| } |
| |
| Status DisplayDriverST7789::Init() { |
| auto transaction = |
| spi_device_.StartTransaction(ChipSelectBehavior::kPerWriteRead); |
| |
| spi_helper_.SetDataBits(8); |
| |
| WriteCommand(transaction, {ST7789_SWRESET, kEmptyArray}); // Software reset |
| pw::spin_delay::WaitMillis(150); |
| |
| WriteCommand(transaction, {ST7789_TEON, kEmptyArray}); |
| WriteCommand(transaction, {ST7789_COLMOD, array<byte, 1>{byte{0x05}}}); |
| |
| WriteCommand(transaction, |
| {ST7789_PORCTRL, |
| array<byte, 5>{ |
| byte{0x0c}, |
| byte{0x0c}, |
| byte{0x00}, |
| byte{0x33}, |
| byte{0x33}, |
| }}); |
| WriteCommand(transaction, {ST7789_LCMCTRL, array<byte, 1>{byte{0x2c}}}); |
| WriteCommand(transaction, {ST7789_VDVVRHEN, array<byte, 1>{byte{0x01}}}); |
| WriteCommand(transaction, {ST7789_VRHS, array<byte, 1>{byte{0x12}}}); |
| WriteCommand(transaction, {ST7789_VDVS, array<byte, 1>{byte{0x20}}}); |
| WriteCommand(transaction, |
| {ST7789_PWCTRL1, |
| array<byte, 2>{ |
| byte{0xa4}, |
| byte{0xa1}, |
| }}); |
| WriteCommand(transaction, {ST7789_FRCTRL2, array<byte, 1>{byte{0x0f}}}); |
| |
| WriteCommand(transaction, {ST7789_INVON, kEmptyArray}); |
| WriteCommand(transaction, {ST7789_SLPOUT, kEmptyArray}); |
| WriteCommand(transaction, {ST7789_DISPON, kEmptyArray}); |
| |
| // Landscape drawing Column Address Set |
| const uint16_t kMaxColumn = screen_width_ - 1; |
| WriteCommand(transaction, |
| {ST7789_CASET, |
| array<byte, 4>{ |
| byte{0x0}, |
| byte{0x0}, |
| byte{static_cast<uint8_t>(kMaxColumn >> 8)}, |
| byte{static_cast<uint8_t>(kMaxColumn & 0xff)}, |
| }}); |
| |
| // Page Address Set |
| const uint16_t kMaxRow = screen_height_ - 1; |
| WriteCommand(transaction, |
| {ST7789_RASET, |
| array<byte, 4>{ |
| byte{0x0}, |
| byte{0x0}, |
| byte{static_cast<uint8_t>(kMaxRow >> 8)}, |
| byte{static_cast<uint8_t>(kMaxRow & 0xff)}, |
| }}); |
| |
| uint8_t madctl = 0; |
| bool rotate_180 = false; |
| |
| if (screen_width_ == 240 && screen_height_ == 240) { |
| // TODO: Figure out 240x240 square display MADCTL values for rotation. |
| madctl = ST7789_MADCTL_HORIZ_ORDER; |
| } else if (screen_width_ == 320 && screen_height_ == 240) { |
| madctl = ST7789_MADCTL_COL_ORDER; |
| if (rotate_180) |
| madctl = ST7789_MADCTL_ROW_ORDER; |
| |
| madctl |= ST7789_MADCTL_SWAP_XY | ST7789_MADCTL_SCAN_ORDER; |
| } |
| |
| WriteCommand(transaction, {ST7789_MADCTL, array<byte, 1>{byte{madctl}}}); |
| |
| pw::spin_delay::WaitMillis(50); |
| |
| return OkStatus(); |
| } |
| |
| Status DisplayDriverST7789::Update( |
| pw::framebuffer::FramebufferRgb565* frame_buffer) { |
| // Let controller know a write is coming. |
| auto transaction = |
| spi_device_.StartTransaction(ChipSelectBehavior::kPerWriteRead); |
| PW_TRY(spi_helper_.SetDataBits(8)); |
| PW_TRY(WriteCommand(transaction, {ST7789_RAMWR, kEmptyArray})); |
| PW_TRY(spi_helper_.SetDataBits(16)); |
| |
| // Write the pixel data. |
| const uint16_t* fb_data = frame_buffer->GetFramebufferData(); |
| const int num_pixels = frame_buffer->GetWidth() * frame_buffer->GetHeight(); |
| return transaction.Write( |
| ConstByteSpan(reinterpret_cast<const byte*>(fb_data), num_pixels)); |
| } |
| |
| Status DisplayDriverST7789::Reset() { |
| if (!reset_gpio_) |
| return Status::Unavailable(); |
| auto s = reset_gpio_->SetStateInactive(); |
| if (!s.ok()) |
| return s; |
| pw::spin_delay::WaitMillis(100); |
| s = reset_gpio_->SetStateActive(); |
| pw::spin_delay::WaitMillis(100); |
| return s; |
| } |
| |
| } // namespace pw::display_driver |