Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 1 | // Copyright 2023 The Pigweed Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| 4 | // use this file except in compliance with the License. You may obtain a copy of |
| 5 | // the License at |
| 6 | // |
| 7 | // https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 12 | // License for the specific language governing permissions and limitations under |
| 13 | // the License. |
| 14 | |
| 15 | #include "pw_display_driver_st7735/display_driver.h" |
| 16 | |
| 17 | #include <array> |
| 18 | #include <cstddef> |
| 19 | |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 20 | #include "pw_chrono/system_clock.h" |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 21 | #include "pw_digital_io/digital_io.h" |
Chris Mumford | 8db6cc1 | 2023-05-02 11:57:18 +0000 | [diff] [blame] | 22 | #include "pw_framebuffer/framebuffer.h" |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 23 | #include "pw_thread/sleep.h" |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 24 | |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 25 | using pw::digital_io::State; |
Chris Mumford | 8db6cc1 | 2023-05-02 11:57:18 +0000 | [diff] [blame] | 26 | using pw::framebuffer::Framebuffer; |
| 27 | using pw::framebuffer::PixelFormat; |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 28 | using pw::spi::ChipSelectBehavior; |
| 29 | using pw::spi::Device; |
| 30 | using std::array; |
| 31 | using std::byte; |
| 32 | |
| 33 | namespace pw::display_driver { |
| 34 | |
| 35 | namespace { |
| 36 | |
| 37 | constexpr array<byte, 0> kEmptyArray; |
| 38 | |
| 39 | // ST7735 Display Registers |
| 40 | // clang-format off |
| 41 | #define ST7735_SWRESET 0x01 |
| 42 | #define ST7735_RDDID 0x04 |
| 43 | #define ST7735_RDDST 0x09 |
| 44 | #define ST7735_SLPIN 0x10 |
| 45 | #define ST7735_SLPOUT 0x11 |
| 46 | #define ST7735_PTLON 0x12 |
| 47 | #define ST7735_NORON 0x13 |
| 48 | #define ST7735_INVOFF 0x20 |
| 49 | #define ST7735_INVON 0x21 |
| 50 | #define ST7735_GAMSET 0x26 |
| 51 | #define ST7735_DISPOFF 0x28 |
| 52 | #define ST7735_DISPON 0x29 |
| 53 | #define ST7735_RAMWR 0x2C |
| 54 | #define ST7735_CASET 0x2A |
| 55 | #define ST7735_RASET 0x2B |
| 56 | #define ST7735_RAMWR 0x2C |
| 57 | #define ST7735_RAMRD 0x2E |
| 58 | #define ST7735_PTLAR 0x30 |
| 59 | #define ST7735_TEOFF 0x34 |
| 60 | #define ST7735_TEON 0x35 |
| 61 | #define ST7735_MADCTL 0x36 |
| 62 | #define ST7735_COLMOD 0x3A |
| 63 | #define ST7735_FRMCTR1 0xB1 // Frame Rate Control (normal mode/ Full colors) |
| 64 | #define ST7735_FRMCTR2 0xB2 // Frame Rate Control (Idle mode/ 8-colors) |
| 65 | #define ST7735_FRMCTR3 0xB3 // Frame Rate Control (Partial mode/ full colors) |
| 66 | #define ST7735_INVCTR 0xB4 |
| 67 | #define ST7735_DISSET5 0xB6 |
| 68 | #define ST7735_GCTRL 0xB7 |
| 69 | #define ST7735_VCOMS 0xBB |
| 70 | #define ST7735_PWCTR1 0xC0 |
| 71 | #define ST7735_PWCTR2 0xC1 |
| 72 | #define ST7735_PWCTR3 0xC2 |
| 73 | #define ST7735_VRHS 0xC3 |
| 74 | #define ST7735_VDVS 0xC4 |
| 75 | #define ST7735_VMCTR1 0xC5 |
| 76 | #define ST7735_FRCTRL2 0xC6 |
| 77 | #define ST7735_PWCTRL1 0xD0 |
| 78 | #define ST7735_RDID1 0xDA |
| 79 | #define ST7735_RDID2 0xDB |
| 80 | #define ST7735_RDID3 0xDC |
| 81 | #define ST7735_RDID4 0xDD |
| 82 | #define ST7735_PORCTRL 0xB2 |
| 83 | #define ST7735_GMCTRP1 0xE0 |
| 84 | #define ST7735_GMCTRN1 0xE1 |
| 85 | #define ST7735_PWCTR6 0xFC |
| 86 | |
| 87 | // MADCTL Bits (See page 215: MADCTL (36h): Memory Data Access Control) |
| 88 | #define ST7735_MADCTL_ROW_ORDER 0b10000000 |
| 89 | #define ST7735_MADCTL_COL_ORDER 0b01000000 |
| 90 | #define ST7735_MADCTL_SWAP_XY 0b00100000 |
| 91 | #define ST7735_MADCTL_SCAN_ORDER 0b00010000 |
| 92 | #define ST7735_MADCTL_RGB_BGR 0b00001000 |
| 93 | #define ST7735_MADCTL_HORIZ_ORDER 0b00000100 |
| 94 | |
| 95 | #define ST7735_INVCTR_NLA 0b00000100 // Inversion setting in full Colors normal mode |
| 96 | #define ST7735_INVCTR_NLB 0b00000010 // Inversion setting in Idle mode |
| 97 | #define ST7735_INVCTR_NLC 0b00000001 // Inversion setting in full Colors partial mode |
| 98 | |
| 99 | // clang-format on |
| 100 | |
| 101 | constexpr uint8_t HighByte(uint16_t val) { return val >> 8; } |
| 102 | |
| 103 | constexpr uint8_t LowByte(uint16_t val) { return val & 0xff; } |
| 104 | |
| 105 | } // namespace |
| 106 | |
| 107 | // The ST7735 supports a max display size of 162x132. This was developed with |
| 108 | // a ST7735 development board with a 160x128 pixel screen - hence the row/col |
| 109 | // start values. These should be parameterized. |
| 110 | DisplayDriverST7735::DisplayDriverST7735(const Config& config) |
| 111 | : config_(config), row_start_(2), col_start_(1) {} |
| 112 | |
| 113 | void DisplayDriverST7735::SetMode(Mode mode) { |
| 114 | // Set the D/CX pin to indicate data or command values. |
| 115 | if (mode == Mode::kData) { |
| 116 | config_.data_cmd_gpio.SetState(State::kActive); |
| 117 | } else { |
| 118 | config_.data_cmd_gpio.SetState(State::kInactive); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | Status DisplayDriverST7735::WriteCommand(Device::Transaction& transaction, |
| 123 | const Command& command) { |
| 124 | SetMode(Mode::kCommand); |
| 125 | byte buff[1]{static_cast<byte>(command.command)}; |
| 126 | auto s = transaction.Write(buff); |
| 127 | if (!s.ok()) |
| 128 | return s; |
| 129 | |
| 130 | SetMode(Mode::kData); |
| 131 | if (command.command_data.empty()) { |
| 132 | return OkStatus(); |
| 133 | } |
| 134 | return transaction.Write(command.command_data); |
| 135 | } |
| 136 | |
| 137 | Status DisplayDriverST7735::Init() { |
| 138 | auto transaction = config_.spi_device_8_bit.StartTransaction( |
| 139 | ChipSelectBehavior::kPerWriteRead); |
| 140 | |
| 141 | WriteCommand(transaction, {ST7735_SWRESET, kEmptyArray}); // Software reset |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 142 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(150ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 143 | WriteCommand(transaction, {ST7735_SLPOUT, kEmptyArray}); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 144 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(500ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 145 | |
| 146 | WriteCommand(transaction, |
| 147 | {ST7735_FRMCTR1, |
| 148 | array<byte, 3>{ |
| 149 | byte{0x00}, |
| 150 | byte{0x06}, |
| 151 | byte{0x03}, |
| 152 | }}); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 153 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(10ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 154 | |
| 155 | WriteCommand(transaction, |
| 156 | {ST7735_DISSET5, |
| 157 | array<byte, 2>{ |
| 158 | byte{0x15}, |
| 159 | byte{0x02}, |
| 160 | }}); |
| 161 | |
| 162 | constexpr uint8_t inversion_val = |
| 163 | ST7735_INVCTR_NLA | ST7735_INVCTR_NLB | ST7735_INVCTR_NLC; |
| 164 | |
| 165 | WriteCommand(transaction, |
| 166 | {ST7735_INVCTR, |
| 167 | array<byte, 1>{ |
| 168 | byte{inversion_val}, |
| 169 | }}); |
| 170 | |
| 171 | WriteCommand(transaction, {ST7735_TEON, kEmptyArray}); |
| 172 | WriteCommand(transaction, {ST7735_COLMOD, array<byte, 1>{byte{0x05}}}); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 173 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(10ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 174 | |
| 175 | WriteCommand(transaction, |
| 176 | {ST7735_PORCTRL, |
| 177 | array<byte, 5>{ |
| 178 | byte{0x0c}, |
| 179 | byte{0x0c}, |
| 180 | byte{0x00}, |
| 181 | byte{0x33}, |
| 182 | byte{0x33}, |
| 183 | }}); |
| 184 | WriteCommand(transaction, |
| 185 | {ST7735_PWCTR1, |
| 186 | array<byte, 2>{ |
| 187 | byte{0x02}, // GVDD = 4.7V. |
| 188 | byte{0x70}, // 1.0uA. |
| 189 | }}); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 190 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(10ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 191 | WriteCommand(transaction, |
| 192 | {ST7735_PWCTR2, |
| 193 | array<byte, 1>{ |
| 194 | byte{0x05}, |
| 195 | }}); |
| 196 | WriteCommand(transaction, |
| 197 | {ST7735_PWCTR3, |
| 198 | array<byte, 2>{ |
| 199 | byte{0x01}, |
| 200 | byte{0x02}, |
| 201 | }}); |
| 202 | WriteCommand(transaction, |
| 203 | {ST7735_VMCTR1, |
| 204 | array<byte, 2>{ |
| 205 | byte{0x3c}, |
| 206 | byte{0x38}, |
| 207 | }}); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 208 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(10ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 209 | WriteCommand(transaction, |
| 210 | {ST7735_PWCTR6, |
| 211 | array<byte, 2>{ |
| 212 | byte{0x11}, |
| 213 | byte{0x15}, |
| 214 | }}); |
| 215 | |
| 216 | WriteCommand(transaction, {ST7735_VRHS, array<byte, 1>{byte{0x12}}}); |
| 217 | WriteCommand(transaction, {ST7735_VDVS, array<byte, 1>{byte{0x20}}}); |
| 218 | WriteCommand(transaction, |
| 219 | {ST7735_PWCTRL1, |
| 220 | array<byte, 2>{ |
| 221 | byte{0xa4}, |
| 222 | byte{0xa1}, |
| 223 | }}); |
| 224 | WriteCommand(transaction, {ST7735_FRCTRL2, array<byte, 1>{byte{0x0f}}}); |
| 225 | |
| 226 | WriteCommand(transaction, {ST7735_INVOFF, kEmptyArray}); |
| 227 | |
| 228 | WriteCommand(transaction, |
| 229 | {ST7735_GMCTRP1, |
| 230 | array<byte, 16>{ |
| 231 | byte{0x09}, |
| 232 | byte{0x16}, |
| 233 | byte{0x09}, |
| 234 | byte{0x20}, |
| 235 | byte{0x21}, |
| 236 | byte{0x1B}, |
| 237 | byte{0x13}, |
| 238 | byte{0x19}, |
| 239 | byte{0x17}, |
| 240 | byte{0x15}, |
| 241 | byte{0x1E}, |
| 242 | byte{0x2B}, |
| 243 | byte{0x04}, |
| 244 | byte{0x05}, |
| 245 | byte{0x02}, |
| 246 | byte{0x0E}, |
| 247 | }}); |
| 248 | WriteCommand(transaction, |
| 249 | {ST7735_GMCTRN1, |
| 250 | array<byte, 16>{ |
| 251 | byte{0x0B}, |
| 252 | byte{0x14}, |
| 253 | byte{0x08}, |
| 254 | byte{0x1E}, |
| 255 | byte{0x22}, |
| 256 | byte{0x1D}, |
| 257 | byte{0x18}, |
| 258 | byte{0x1E}, |
| 259 | byte{0x1B}, |
| 260 | byte{0x1A}, |
| 261 | byte{0x24}, |
| 262 | byte{0x2B}, |
| 263 | byte{0x06}, |
| 264 | byte{0x06}, |
| 265 | byte{0x02}, |
| 266 | byte{0x0F}, |
| 267 | }}); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 268 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(10ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 269 | |
| 270 | // Landscape drawing Column Address Set |
| 271 | const uint16_t max_column = config_.screen_width + col_start_ - 1; |
| 272 | WriteCommand(transaction, |
| 273 | {ST7735_CASET, |
| 274 | array<byte, 4>{ |
| 275 | byte{HighByte(col_start_)}, |
| 276 | byte{LowByte(col_start_)}, |
| 277 | byte{HighByte(max_column)}, |
| 278 | byte{LowByte(max_column)}, |
| 279 | }}); |
| 280 | |
| 281 | // Page Address Set |
| 282 | const uint16_t max_row = config_.screen_height + row_start_ - 1; |
| 283 | WriteCommand(transaction, |
| 284 | {ST7735_RASET, |
| 285 | array<byte, 4>{ |
| 286 | byte{HighByte(row_start_)}, |
| 287 | byte{LowByte(row_start_)}, |
| 288 | byte{HighByte(max_row)}, |
| 289 | byte{LowByte(max_row)}, |
| 290 | }}); |
| 291 | |
| 292 | constexpr bool rotate_180 = false; |
| 293 | uint8_t madctl = ST7735_MADCTL_COL_ORDER; |
| 294 | if (rotate_180) |
| 295 | madctl = ST7735_MADCTL_ROW_ORDER; |
| 296 | madctl |= ST7735_MADCTL_SWAP_XY | ST7735_MADCTL_SCAN_ORDER; |
| 297 | WriteCommand(transaction, {ST7735_MADCTL, array<byte, 1>{byte{madctl}}}); |
| 298 | |
| 299 | WriteCommand(transaction, {ST7735_NORON, kEmptyArray}); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 300 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(10ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 301 | |
| 302 | WriteCommand(transaction, {ST7735_DISPON, kEmptyArray}); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 303 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(500ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 304 | |
| 305 | return OkStatus(); |
| 306 | } |
| 307 | |
Chris Mumford | 8db6cc1 | 2023-05-02 11:57:18 +0000 | [diff] [blame] | 308 | void DisplayDriverST7735::WriteFramebuffer(Framebuffer framebuffer, |
| 309 | WriteCallback write_callback) { |
| 310 | PW_ASSERT(framebuffer.is_valid()); |
| 311 | PW_ASSERT(framebuffer.pixel_format() == PixelFormat::RGB565); |
| 312 | Status s; |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 313 | // Let controller know a write is coming. |
| 314 | { |
| 315 | auto transaction = config_.spi_device_8_bit.StartTransaction( |
| 316 | ChipSelectBehavior::kPerWriteRead); |
Chris Mumford | 8db6cc1 | 2023-05-02 11:57:18 +0000 | [diff] [blame] | 317 | s = WriteCommand(transaction, {ST7735_RAMWR, kEmptyArray}); |
| 318 | if (!s.ok()) { |
| 319 | write_callback(std::move(framebuffer), s); |
| 320 | return; |
| 321 | } |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 322 | } |
| 323 | |
| 324 | // Write the pixel data. |
| 325 | auto transaction = config_.spi_device_16_bit.StartTransaction( |
| 326 | ChipSelectBehavior::kPerWriteRead); |
Chris Mumford | 8db6cc1 | 2023-05-02 11:57:18 +0000 | [diff] [blame] | 327 | const uint16_t* fb_data = static_cast<const uint16_t*>(framebuffer.data()); |
Chris Mumford | 79baf95 | 2023-03-08 16:32:16 +0000 | [diff] [blame] | 328 | const size_t num_pixels = config_.screen_width * config_.screen_height; |
Chris Mumford | 8db6cc1 | 2023-05-02 11:57:18 +0000 | [diff] [blame] | 329 | s = transaction.Write( |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 330 | ConstByteSpan(reinterpret_cast<const byte*>(fb_data), num_pixels)); |
Chris Mumford | 8db6cc1 | 2023-05-02 11:57:18 +0000 | [diff] [blame] | 331 | write_callback(std::move(framebuffer), s); |
| 332 | } |
| 333 | |
| 334 | Status DisplayDriverST7735::WriteRow(span<uint16_t> row_pixels, |
| 335 | uint16_t row_idx, |
| 336 | uint16_t col_idx) { |
| 337 | PW_ASSERT(false); |
| 338 | return Status::Unimplemented(); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 339 | } |
| 340 | |
| 341 | Status DisplayDriverST7735::Reset() { |
| 342 | if (!config_.reset_gpio) |
| 343 | return Status::Unavailable(); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 344 | PW_TRY(config_.reset_gpio->SetStateInactive()); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 345 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(100ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 346 | PW_TRY(config_.reset_gpio->SetStateActive()); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 347 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(100ms)); |
Anthony DiGirolamo | 0c1e0ce | 2024-02-13 21:03:05 +0000 | [diff] [blame] | 348 | PW_TRY(config_.reset_gpio->SetStateInactive()); |
Anthony DiGirolamo | f0badda | 2024-03-29 22:34:21 +0000 | [diff] [blame^] | 349 | pw::this_thread::sleep_for(pw::chrono::SystemClock::for_at_least(100ms)); |
Chris Mumford | 3427047 | 2023-01-10 21:29:08 +0000 | [diff] [blame] | 350 | return OkStatus(); |
| 351 | } |
| 352 | |
| 353 | } // namespace pw::display_driver |