From 2c22fae2f4c34992843ffbc35169cb8d3fe7a97f Mon Sep 17 00:00:00 2001 From: Tomasz Wasilczyk Date: Mon, 30 Aug 2021 16:19:56 -0700 Subject: [PATCH] Monochrome implementation of ST7586S display This panel is 4-level grayscale, but this color mode isn't supported by modm yet. --- README.md | 5 +- examples/srxe/display/main.cpp | 27 +++ examples/srxe/display/project.xml | 9 + src/modm/board/srxe/board.hpp | 29 ++- src/modm/board/srxe/board_display.cpp | 18 ++ src/modm/board/srxe/module.lb | 8 +- src/modm/driver/display/st7586s.hpp | 43 +++++ src/modm/driver/display/st7586s.lb | 28 +++ src/modm/driver/display/st7586s_impl.hpp | 126 +++++++++++++ src/modm/driver/display/st7586s_protocol.hpp | 184 +++++++++++++++++++ 10 files changed, 469 insertions(+), 8 deletions(-) create mode 100644 examples/srxe/display/main.cpp create mode 100644 examples/srxe/display/project.xml create mode 100644 src/modm/board/srxe/board_display.cpp create mode 100644 src/modm/driver/display/st7586s.hpp create mode 100644 src/modm/driver/display/st7586s.lb create mode 100644 src/modm/driver/display/st7586s_impl.hpp create mode 100644 src/modm/driver/display/st7586s_protocol.hpp diff --git a/README.md b/README.md index 93364df869..81fca96f93 100644 --- a/README.md +++ b/README.md @@ -613,20 +613,21 @@ you specific needs. SK9822 SSD1306 +ST7586S STTS22H STUSB4500 SX1276 TCS3414 TCS3472 -TLC594X +TLC594X TMP102 TMP175 TOUCH2046 VL53L0 VL6180 -WS2812 +WS2812 diff --git a/examples/srxe/display/main.cpp b/examples/srxe/display/main.cpp new file mode 100644 index 0000000000..8ec09361db --- /dev/null +++ b/examples/srxe/display/main.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021, Tomasz Wasilczyk + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#include + +int +main() +{ + Board::initialize(); + + auto& display = Board::display; + + display.setCursor(115, 54); + display.setFont(modm::font::Ubuntu_36); + display << "Hello World"; + display.drawRoundedRectangle({100, 48}, 184, 40, 10); + + display.update(); +} diff --git a/examples/srxe/display/project.xml b/examples/srxe/display/project.xml new file mode 100644 index 0000000000..5501905b2f --- /dev/null +++ b/examples/srxe/display/project.xml @@ -0,0 +1,9 @@ + + modm:srxe + + + + + modm:build:scons + + diff --git a/src/modm/board/srxe/board.hpp b/src/modm/board/srxe/board.hpp index aabd12dac7..b4bdf4e095 100644 --- a/src/modm/board/srxe/board.hpp +++ b/src/modm/board/srxe/board.hpp @@ -13,6 +13,8 @@ #include +#include + using namespace modm::platform; /// @ingroup modm_board_srxe @@ -25,13 +27,26 @@ using SystemClock = modm::platform::SystemClock; using LedDebug = GpioB0; using Leds = SoftwareGpioPort; -namespace Display { +namespace spi { + +using Sck = GpioB1; +using Mosi = GpioB2; +using Miso = GpioB3; +using SpiMaster = modm::platform::SpiMaster; + +} // namespace spi + +namespace DisplayGpio { using DC = GpioD6; using CS = GpioE7; using RST = GpioG2; -} // namespace Display +} // namespace DisplayGpio + +using Display = modm::St7586s; +extern Display display; inline void initialize() { @@ -39,7 +54,17 @@ initialize() { LedDebug::setOutput(); + spi::SpiMaster::connect(); + /* Display controller datasheet requires minimum 60+140ns clock pulse width (that's 3.5MHz + * for 200ns cycle or 5MHz for 280ns), but it seems to work just fine with 8MHz. + */ + spi::SpiMaster::initialize(); + // Clock is high when inactive, data is sampled on clock trailing edge. + spi::SpiMaster::setDataMode(spi::SpiMaster::DataMode::Mode3); + enableInterrupts(); + + display.initialize(); } } // namespace Board diff --git a/src/modm/board/srxe/board_display.cpp b/src/modm/board/srxe/board_display.cpp new file mode 100644 index 0000000000..104e90524a --- /dev/null +++ b/src/modm/board/srxe/board_display.cpp @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2021, Tomasz Wasilczyk + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#include "board.hpp" + +namespace Board { + +Display display; + +} // namespace Board diff --git a/src/modm/board/srxe/module.lb b/src/modm/board/srxe/module.lb index 7519d18efd..6ff6e9c88c 100644 --- a/src/modm/board/srxe/module.lb +++ b/src/modm/board/srxe/module.lb @@ -18,7 +18,7 @@ def init(module): Smart Response XE is an obsolete classroom clicker, sold for as little as 5 USD on well known online auction site. It's a compelling platform that's fully reverse engineered and ready to hack out of box, featuring: - ATmega128RFA1 MCU - - 384x160 LCD display + - 384x136 LCD display - QWERTY keyboard - External 1M SPI flash - Exposed ISP and JTAG headers @@ -36,13 +36,13 @@ def prepare(module, options): return False module.depends( - ":architecture:clock", - ":architecture:interrupt", ":debug", ":platform:clock", ":platform:core", ":platform:gpio", - ":platform:uart:0") + ":platform:spi", + ":platform:uart:0", + ":driver:st7586s") return True def build(env): diff --git a/src/modm/driver/display/st7586s.hpp b/src/modm/driver/display/st7586s.hpp new file mode 100644 index 0000000000..a4fb666d6d --- /dev/null +++ b/src/modm/driver/display/st7586s.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021, Tomasz Wasilczyk + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#pragma once + +#include + +#include "st7586s_protocol.hpp" + +namespace modm +{ + +template +// TODO: this controller has pixels packed by 3, not 8 per byte +class St7586s : public MonochromeGraphicDisplayHorizontal +{ + using Command = detail::st7586s::Command; + static constexpr uint8_t pixelsPerByte = 3; + + void sendCommand(Command cmd, const void *data = nullptr, size_t len = 0); + template + void sendCommand(Command cmd, Data data) { + sendCommand(cmd, &data, sizeof(data)); + } + + void setClipping(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + +public: + void initialize(); + virtual void update() override; +}; + +} // namespace modm + +#include "st7586s_impl.hpp" diff --git a/src/modm/driver/display/st7586s.lb b/src/modm/driver/display/st7586s.lb new file mode 100644 index 0000000000..f5e7ce778f --- /dev/null +++ b/src/modm/driver/display/st7586s.lb @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2021, Tomasz Wasilczyk +# +# This file is part of the modm project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# ----------------------------------------------------------------------------- + + +def init(module): + module.name = ":driver:st7586s" + module.description = "ST7586S 4-level grayscale LCD controller" + +def prepare(module, options): + module.depends( + ":architecture:delay", + ":ui:display") + return True + +def build(env): + env.outbasepath = "modm/src/modm/driver/display" + env.copy("st7586s.hpp") + env.copy("st7586s_impl.hpp") + env.copy("st7586s_protocol.hpp") diff --git a/src/modm/driver/display/st7586s_impl.hpp b/src/modm/driver/display/st7586s_impl.hpp new file mode 100644 index 0000000000..e29336c6d2 --- /dev/null +++ b/src/modm/driver/display/st7586s_impl.hpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2021, Tomasz Wasilczyk + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#pragma once +#include "st7586s.hpp" + +#include +#include + +namespace modm +{ + +template +void +St7586s::sendCommand(Command cmd, const void *data, size_t len) +{ + CS::reset(); + DC::reset(); // command mode + SPI::transferBlocking(static_cast(cmd)); + DC::set(); // data mode + if (len > 0) { + SPI::transferBlocking(reinterpret_cast(data), nullptr, len); + } + CS::set(); + // exit with data mode on +} + +template +void +St7586s::initialize() +{ + namespace payload = detail::st7586s::payload; + + // Configure GPIO + RST::setOutput(false); + CS::setOutput(true); + DC::setOutput(); + + // Reset display + modm::delay(10us); + RST::set(); + modm::delay(120ms); + + // Power ON operation flow (see datasheet) + sendCommand(Command::SleepOff); + sendCommand(Command::DisplayOff); + modm::delay(50ms); // t_{ON-V2} + sendCommand(Command::SetVop, payload::SetVop(13.52f)); + sendCommand(Command::SetBias, payload::SetBias::Ratio_1_11); + sendCommand(Command::SetBooster, payload::SetBooster::x8); + sendCommand(Command::AnalogControl, payload::AnalogControl::Enable); + sendCommand(Command::NLineInversion, + payload::LineInversionType_t(payload::LineInversionType::FrameInversion)); + sendCommand(Command::DisplayModeMono); // TODO: grayscale + sendCommand(Command::EnableDdram, payload::EnableDdram::Enable); + sendCommand(Command::ScanDirection, payload::DisplayControl::ComInc + | payload::DisplayControl::SegInc); + sendCommand(Command::DisplayDuty, uint8_t(Height - 1)); + sendCommand(Command::InverseOff); + setClipping(0, 0, Width, Height); + update(); // clear the display + sendCommand(Command::DisplayOn); +} + +template +void +St7586s::setClipping(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + namespace payload = detail::st7586s::payload; + + modm_assert_continue_fail_debug(x < Width, "st4586s.sc.x", "x >= Width", x); + modm_assert_continue_fail_debug(x + w <= Width, "st4586s.sc.xw", "x + w >= Width", x + w); + modm_assert_continue_fail_debug(y < Height, "st4586s.sc.y", "y >= Height", y); + modm_assert_continue_fail_debug(y + h <= Height, "st4586s.sc.yh", "y + h >= Height", y + h); + modm_assert_continue_fail_debug((x % pixelsPerByte) == 0, "st4586s.sc.x%", + "x is not a multiply of PBB", x); + modm_assert_continue_fail_debug((w % pixelsPerByte) == 0, "st4586s.sc.w%", + "w is not a multiply of PBB", w); + + const payload::SetColumnRow columnRange(x / pixelsPerByte, (x + w) / pixelsPerByte - 1); + sendCommand(Command::SetColumn, columnRange); + + const payload::SetColumnRow rowRange(y, y + h - 1); + sendCommand(Command::SetRow, rowRange); +} + +template +void +St7586s::update() +{ + sendCommand(Command::WriteDisplayData); + CS::reset(); + // TODO: support windows other than full screen (then make setClipping public) + // TODO: transfer the whole memory area, not individual pixels + for (uint16_t y = 0; y < Height; y++) { + const uint8_t* row = this->buffer[y]; + uint16_t currentByte = 0; + uint8_t validBits = 0; + for (uint16_t x = 0; x + 2 < Width; x += 3) { + if (validBits < 3) { + currentByte |= (*row++) << validBits; + validBits += 8; + } + + uint8_t cell = 0; + if (currentByte & 0b001) cell |= (0b11 << 6); + if (currentByte & 0b010) cell |= (0b11 << 3); + if (currentByte & 0b100) cell |= (0b11 << 0); + validBits -= 3; + currentByte >>= 3; + + SPI::transferBlocking(cell); + } + } + CS::set(); +} + +} // namespace modm diff --git a/src/modm/driver/display/st7586s_protocol.hpp b/src/modm/driver/display/st7586s_protocol.hpp new file mode 100644 index 0000000000..9b24987b76 --- /dev/null +++ b/src/modm/driver/display/st7586s_protocol.hpp @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2021, Tomasz Wasilczyk + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#pragma once + +#include + +namespace modm::detail::st7586s +{ + +enum class Command : uint8_t { + Nop = 0x00, + Reset = 0x01, + SleepOn = 0x10, + SleepOff = 0x11, + PartialOn = 0x12, + PartialOff = 0x13, + InverseOff = 0x20, + InverseOn = 0x21, + AllPixelsOn = 0x22, + AllPixelsOff = 0x23, + DisplayOff = 0x28, + DisplayOn = 0x29, + SetColumn = 0x2A, + SetRow = 0x2B, + WriteDisplayData = 0x2C, + ReadDisplayData = 0x2E, + PartialDisplayArea = 0x30, + ScrollArea = 0x33, + DisplayControl = 0x36, + ScanDirection = 0x36, + StartLine = 0x37, + DisplayModeGray = 0x38, + DisplayModeMono = 0x39, + EnableDdram = 0x3A, + DisplayDuty = 0xB0, + FirstOutputCom = 0xB1, + FoscDivider = 0xB3, + PartialDisplay = 0xB4, + NLineInversion = 0xB5, + ReadModifyWriteOn = 0xB8, + ReadModifyWriteOff = 0xB9, + SetVop = 0xC0, + VopIncrease = 0xC1, + VopDecrease = 0xC2, + SetBias = 0xC3, + SetBooster = 0xC4, + SetVopOffset = 0xC7, + AnalogControl = 0xD0, + AutoReadControl = 0xD7, + OtpRdWrControl = 0xE0, + OtpControlOut = 0xE1, + OtpWrite = 0xE2, + OtpRead = 0xE3, + OtpSelectionControl = 0xE4, + OtpProgramming = 0xE5, + FrameRateGray = 0xF0, + FrameRateMono = 0xF1, + TempRange = 0xF2, + TempGradient = 0xF4, +}; + +namespace payload { + +struct SetColumnRow { + uint8_t startHi = 0; + uint8_t start; + uint8_t endHi = 0; + uint8_t end; + + constexpr SetColumnRow(uint8_t start, uint8_t end) : start(start), end(end) {} +}; + +enum class DisplayControl : uint8_t { + ComInc = 0, + ComDec = Bit7, + SegInc = 0, + SegDec = Bit6, +}; + +using DisplayControlFlags = modm::Flags8; +MODM_TYPE_FLAGS(DisplayControlFlags); + +enum class EnableDdram : uint8_t { + Enable = 0x02, +}; + +enum class NLineInversion : uint8_t { + /** + * When this bit is set, inversion is idependent from frame. + * Otherwise, inversion occurs in every frame. + */ + FrameIndependent = Bit7, +}; +typedef modm::Flags8 NLineInversion_t; +MODM_TYPE_FLAGS(NLineInversion_t); + +enum class LineInversionType : uint8_t { + FrameInversion = 0, + Invert_3_line = 2, + Invert_4_line = 3, + Invert_5_line = 4, + Invert_6_line = 5, + Invert_7_line = 6, + Invert_8_line = 7, + Invert_9_line = 8, + Invert_10_line = 9, + Invert_11_line = 10, + Invert_12_line = 11, + Invert_13_line = 12, + Invert_14_line = 13, + Invert_15_line = 14, + Invert_16_line = 15, + Invert_17_line = 16, + Invert_18_line = 17, + Invert_19_line = 18, + Invert_20_line = 19, + Invert_21_line = 20, + Invert_22_line = 21, + Invert_23_line = 22, + Invert_24_line = 23, + Invert_25_line = 24, + Invert_26_line = 25, + Invert_27_line = 26, + Invert_28_line = 27, + Invert_29_line = 28, + Invert_30_line = 29, + Invert_31_line = 30, + Invert_32_line = 31, +}; +typedef Configuration LineInversionType_t; + +struct SetVop { + LittleEndian vop; + + constexpr SetVop(const float voltage) { + /* Formula to calculate Vop from datasheet: + * V0 = 3.6 + (Vop + Vof + VopInc + VopDec) * 0.04 V + */ + auto rawVoltage = std::round((voltage - 3.6f) / 0.04f); + + if (rawVoltage < 0.0f) rawVoltage = 0.0f; + constexpr uint16_t maxVoltage = 0b111111111; + if (rawVoltage > float(maxVoltage)) rawVoltage = float(maxVoltage); + + vop = rawVoltage; + } +}; + +enum class SetBias : uint8_t { + Ratio_1_14 = 0, + Ratio_1_13 = 1, + Ratio_1_12 = 2, + Ratio_1_11 = 3, + Ratio_1_10 = 4, + Ratio_1_9 = 5, +}; + +enum class SetBooster : uint8_t { + x1 = 0, + x2 = 1, + x3 = 2, + x4 = 3, + x5 = 4, + x6 = 5, + x7 = 6, + x8 = 7, +}; + +enum class AnalogControl : uint8_t { + Enable = 0x1D, +}; + +} // namespace payload + +} // namespace modm::detail::st7586s