| // Copyright 2024 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_chrono/system_clock.h" |
| #include "pw_digital_io/digital_io.h" |
| #include "pw_framebuffer/framebuffer.h" |
| #include "pw_thread/sleep.h" |
| |
| using namespace std::chrono_literals; |
| |
| using pw::digital_io::State; |
| using pw::framebuffer::Framebuffer; |
| using pw::framebuffer::PixelFormat; |
| 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 |
| |
| constexpr uint8_t HighByte(uint16_t val) { return val >> 8; } |
| |
| constexpr uint8_t LowByte(uint16_t val) { return val & 0xff; } |
| |
| } // namespace |
| |
| DisplayDriverST7789::DisplayDriverST7789(const Config& config) |
| : config_(config) {} |
| |
| Status DisplayDriverST7789::SetMode(Mode mode) { |
| // Set the D/CX pin to indicate data or command values. |
| if (mode == Mode::kData) { |
| // Set data command pin to 1 |
| return config_.data_cmd_gpio.SetState(State::kActive); |
| } else { |
| // Set data command pin to 0 |
| return config_.data_cmd_gpio.SetState(State::kInactive); |
| } |
| } |
| |
| Status DisplayDriverST7789::WriteCommand(Device::Transaction& transaction, |
| const Command& command) { |
| PW_TRY(SetMode(Mode::kCommand)); |
| byte buff[1]{static_cast<byte>(command.command)}; |
| auto s = transaction.Write(buff); |
| if (!s.ok()) |
| return s; |
| |
| PW_TRY(SetMode(Mode::kData)); |
| if (command.command_data.empty()) { |
| return OkStatus(); |
| } |
| return transaction.Write(command.command_data); |
| } |
| |
| Status DisplayDriverST7789::Init() { |
| auto transaction = config_.spi_device_8_bit.StartTransaction( |
| ChipSelectBehavior::kPerWriteRead); |
| |
| PW_TRY(WriteCommand(transaction, |
| {ST7789_SWRESET, kEmptyArray})); // Software reset |
| pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(100ms)); |
| |
| PW_TRY(WriteCommand(transaction, {ST7789_TEON, kEmptyArray})); |
| PW_TRY( |
| WriteCommand(transaction, {ST7789_COLMOD, array<byte, 1>{byte{0x05}}})); |
| |
| PW_TRY(WriteCommand(transaction, |
| {ST7789_PORCTRL, |
| array<byte, 5>{ |
| byte{0x0c}, |
| byte{0x0c}, |
| byte{0x00}, |
| byte{0x33}, |
| byte{0x33}, |
| }})); |
| PW_TRY( |
| WriteCommand(transaction, {ST7789_LCMCTRL, array<byte, 1>{byte{0x2c}}})); |
| PW_TRY( |
| WriteCommand(transaction, {ST7789_VDVVRHEN, array<byte, 1>{byte{0x01}}})); |
| PW_TRY(WriteCommand(transaction, {ST7789_VRHS, array<byte, 1>{byte{0x12}}})); |
| PW_TRY(WriteCommand(transaction, {ST7789_VDVS, array<byte, 1>{byte{0x20}}})); |
| PW_TRY(WriteCommand(transaction, |
| {ST7789_PWCTRL1, |
| array<byte, 2>{ |
| byte{0xa4}, |
| byte{0xa1}, |
| }})); |
| PW_TRY( |
| WriteCommand(transaction, {ST7789_FRCTRL2, array<byte, 1>{byte{0x0f}}})); |
| |
| PW_TRY(WriteCommand(transaction, {ST7789_INVON, kEmptyArray})); |
| PW_TRY(WriteCommand(transaction, {ST7789_SLPOUT, kEmptyArray})); |
| PW_TRY(WriteCommand(transaction, {ST7789_DISPON, kEmptyArray})); |
| |
| // Landscape drawing Column Address Set |
| const uint16_t kMaxColumn = config_.screen_width - 1; |
| PW_TRY(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 = config_.screen_height - 1; |
| PW_TRY(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 (config_.screen_width == 240 && config_.screen_height == 240) { |
| // TODO: Figure out 240x240 square display MADCTL values for rotation. |
| madctl = ST7789_MADCTL_HORIZ_ORDER; |
| } else if (config_.screen_width == 320 && config_.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; |
| } |
| |
| PW_TRY( |
| WriteCommand(transaction, {ST7789_MADCTL, array<byte, 1>{byte{madctl}}})); |
| |
| pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(50ms)); |
| |
| return OkStatus(); |
| } |
| |
| void DisplayDriverST7789::WriteFramebuffer(Framebuffer frame_buffer, |
| WriteCallback write_callback) { |
| PW_ASSERT(frame_buffer.pixel_format() == PixelFormat::RGB565); |
| if (config_.pixel_pusher) { |
| // Write the pixel data. |
| config_.pixel_pusher->WriteFramebuffer(std::move(frame_buffer), |
| std::move(write_callback)); |
| return; |
| } |
| |
| // Write the frame_buffer using pw_spi. |
| // Let controller know a write is coming. |
| Status s; |
| { |
| auto transaction = config_.spi_device_8_bit.StartTransaction( |
| ChipSelectBehavior::kPerWriteRead); |
| s = WriteCommand(transaction, {ST7789_RAMWR, kEmptyArray}); |
| if (!s.ok()) { |
| write_callback(std::move(frame_buffer), s); |
| return; |
| } |
| } |
| |
| // Write the pixel data. |
| auto transaction = config_.spi_device_16_bit.StartTransaction( |
| ChipSelectBehavior::kPerWriteRead); |
| const uint16_t* fb_data = static_cast<const uint16_t*>(frame_buffer.data()); |
| const int num_pixels = frame_buffer.size().width * frame_buffer.size().height; |
| s = transaction.Write( |
| ConstByteSpan(reinterpret_cast<const byte*>(fb_data), num_pixels)); |
| write_callback(std::move(frame_buffer), s); |
| } |
| |
| Status DisplayDriverST7789::WriteRow(span<uint16_t> row_pixels, |
| uint16_t row_idx, |
| uint16_t col_idx) { |
| { |
| // Let controller know a write is coming. |
| auto transaction = config_.spi_device_8_bit.StartTransaction( |
| ChipSelectBehavior::kPerWriteRead); |
| // Landscape drawing Column Address Set |
| const uint16_t max_col_idx = |
| std::max(config_.screen_width - 1, |
| col_idx + static_cast<int>(row_pixels.size())); |
| PW_TRY(WriteCommand(transaction, |
| {ST7789_CASET, |
| std::array<std::byte, 4>{ |
| std::byte{HighByte(col_idx)}, |
| std::byte{LowByte(col_idx)}, |
| std::byte{HighByte(max_col_idx)}, |
| std::byte{LowByte(max_col_idx)}, |
| }})); |
| |
| // Page Address Set |
| uint16_t max_row_idx = row_idx; |
| PW_TRY(WriteCommand(transaction, |
| {ST7789_RASET, |
| std::array<std::byte, 4>{ |
| std::byte{HighByte(row_idx)}, |
| std::byte{LowByte(row_idx)}, |
| std::byte{HighByte(max_row_idx)}, |
| std::byte{LowByte(max_row_idx)}, |
| }})); |
| PW_TRY(WriteCommand(transaction, {ST7789_RAMWR, kEmptyArray})); |
| } |
| |
| auto transaction = config_.spi_device_16_bit.StartTransaction( |
| ChipSelectBehavior::kPerTransaction); |
| return transaction.Write( |
| ConstByteSpan(reinterpret_cast<const std::byte*>(row_pixels.data()), |
| row_pixels.size())); |
| } |
| |
| Status DisplayDriverST7789::Reset() { |
| if (!config_.reset_gpio) |
| return Status::Unavailable(); |
| auto s = config_.reset_gpio->SetStateActive(); |
| if (!s.ok()) |
| return s; |
| pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(100ms)); |
| s = config_.reset_gpio->SetStateInactive(); |
| pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(100ms)); |
| return s; |
| } |
| |
| bool DisplayDriverST7789::SupportsResize() const { |
| return config_.pixel_pusher != nullptr && |
| config_.pixel_pusher->SupportsResize(); |
| } |
| |
| } // namespace pw::display_driver |