From b94f00fb542dc16586065dc3c9f69769317daada Mon Sep 17 00:00:00 2001 From: Malcolm McKellips Date: Fri, 8 Aug 2025 15:33:07 -0600 Subject: [PATCH 1/5] Initial additions for ch1120 and 1.5in OLED. (Unverified) --- src/SparkFun_Qwiic_OLED.h | 10 +- src/qwiic_grch1120.cpp | 1105 +++++++++++++++++++++++++++++++++++++ src/qwiic_grch1120.h | 192 +++++++ src/qwiic_grcommon.h | 33 ++ src/qwiic_grssd1306.cpp | 1 + src/qwiic_grssd1306.h | 31 +- src/qwiic_oled_1in5.h | 92 +++ 7 files changed, 1448 insertions(+), 16 deletions(-) create mode 100644 src/qwiic_grch1120.cpp create mode 100644 src/qwiic_grch1120.h create mode 100644 src/qwiic_grcommon.h create mode 100644 src/qwiic_oled_1in5.h diff --git a/src/SparkFun_Qwiic_OLED.h b/src/SparkFun_Qwiic_OLED.h index 752af45..6169c84 100644 --- a/src/SparkFun_Qwiic_OLED.h +++ b/src/SparkFun_Qwiic_OLED.h @@ -49,6 +49,7 @@ // for the Qwiic OLED driver. // include the underlying SDK implementation headers for the OLED devices +#include "qwiic_oled_1in5.h" #include "qwiic_oled_1in3.h" #include "qwiic_oled_custom.h" #include "qwiic_oledmicro.h" @@ -83,11 +84,11 @@ typedef QwBitmap QwiicBitmap; // Define the template and fill in the interface methods in-line. -template class QwiicOLEDBaseClass : public Print // NOTE: implementing Arduino Print +template class QwiicOLEDBaseClass : public Print // NOTE: implementing Arduino Print { protected: // our device driver - SSD1306DeviceType m_device; + DeviceType m_device; private: QwI2C m_i2cBus; // our i2c object @@ -845,6 +846,11 @@ class Qwiic1in3OLED : public QwiicOLEDBaseClass // nothing here - see above }; +class Qwiic1in5OLED : public QwiicOLEDBaseClass +{ + // nothing here - see above +}; + class QwiicCustomOLED : public QwiicOLEDBaseClass { public: diff --git a/src/qwiic_grch1120.cpp b/src/qwiic_grch1120.cpp new file mode 100644 index 0000000..b507d9d --- /dev/null +++ b/src/qwiic_grch1120.cpp @@ -0,0 +1,1105 @@ + +// qwiic_grch1120.cpp +// +// This is a library written for SparkFun Qwiic OLED boards that use the +// CH1120. This driver is VERY similar to the SSD1306 driver, but with a few +// subtle differences. +// +// SparkFun sells these at its website: www.sparkfun.com +// +// Do you like this library? Help support SparkFun. Buy a board! +// +// Micro OLED https://www.sparkfun.com/products/14532 +// Transparent OLED https://www.sparkfun.com/products/15173 +// "Narrow" OLED https://www.sparkfun.com/products/17153 +// +// +// Written by SparkFun Electronics, August 2025 +// +// This library configures and draws graphics to OLED boards that use the +// CH1120 display hardware. The library only supports I2C. +// +// Repository: +// https://github.com/sparkfun/SparkFun_Qwiic_OLED_Arduino_Library +// +// Documentation: +// https://sparkfun.github.io/SparkFun_Qwiic_OLED_Arduino_Library/ +// +// +// SparkFun code, firmware, and software is released under the MIT +// License(http://opensource.org/licenses/MIT). +// +// SPDX-License-Identifier: MIT +// +// The MIT License (MIT) +// +// Copyright (c) 2022 SparkFun Electronics +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: The +// above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED +// "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "qwiic_grch1120.h" + +///////////////////////////////////////////////////////////////////////////// +// Class that implements graphics support for devices that use the CH1120 +// + + +///////////////////////////////////////////////////////////////////////////// +// Device Commands +// +// The commands are codes used to communicate with the CH1120 device and are +// from the devices datasheet. (See ) +// +// Control Bytes: +// Control Bytes consist of C0 and D~C Bits followed by the rest of the command byte: + +// | ---- Command Byte --- | +// | C0 | D~C | 0 | 0 | 0 | 0 | 0 | 0 | + +// The Co bit (MSB) has the following meaning: +// 0 : This is the last control byte, only data bytes follow. +// 1: More control bytes follow + +// The D~C bit (2nd MSB) has the following meaning: +// 0: The data byte is for command operation +// 1: The byte is for RAM (data) operation + +#define kCmdControlByte ((uint8_t)0x00) +#define kCmdAnotherControlByte ((uint8_t)0xC0) +#define kCmdControlDataByteFollow ((uint8_t)0x80) +#define kCmdRamControlByte ((uint8_t)0x40) + +#define kCmdRowStartEnd ((uint8_t)0x22) // Two byte command - command, start, stop +#define kCmdColStartEnd ((uint8_t)0x21) // Two byte command - command, start, stop +#define kCmdStartRow ((uint8_t)0xB0) // TODO: this is analagous to the start "page" for the SSD1306 +#define kCmdStartColLow ((uint8_t)0x0F) // Start Column = StartColHigh (MSB) | StartColLow (LSB) +#define kCmdStartColHigh ((uint8_t)0x10) +#define kCmdStartLine ((uint8_t)0xA2) // Notice how this is different from the same cmd on the 1306 (0x40) + +#define kCmdContrastControl ((uint8_t)0x81) // Eli had this as 0x0F, is that what this should be? +#define kCmdHorizAddressing ((uint8_t)0x20) +#define kCmdDownVerticalScroll ((uint8_t)0x24) // Down vertical scroll +#define kCmdUpVerticalScroll ((uint8_t)0x25) // Up vertical scroll +#define kCmdRightHorizontalScroll ((uint8_t)0x26) // Right horizontal scroll +#define kCmdLeftHorizontalScroll ((uint8_t)0x27) // Left horizontal scroll +#define kCmdDeactivateScroll ((uint8_t)0x2E) // Stop scrolling (default) +#define kCmdActivateScroll ((uint8_t)0x2F) // Activate Scrolling scrolling +#define kCmdDisplayOn ((uint8_t)0xAF) +#define kCmdDisplayOff ((uint8_t)0xAE) +#define kCmdPanelID ((uint8_t)0xE1) +#define kCmdDriverID ((uint8_t)0xE2) // BUSY+ON/OFF+0x60 +#define kCmdGrayMono ((uint8_t)0xAC) +#define kCmdSegRemapDown ((uint8_t)0xA0) // default +#define kCmdSegRemapUp ((uint8_t)0xA1) // reverse +#define kCmdComOutScan0First ((uint8_t)0xC0) // scan COM0 to COM[n-1] (default) +#define kCmdComOutScan0Last ((uint8_t)0xC8) // scan COM[n-1] to COM0 (reverse) +#define kCmdDisplayRotation ((uint8_t)0xA3) +#define kCmdDisableEntireDisplay ((uint8_t)0xA4) +#define kCmdEnableEntireDisplay ((uint8_t)0xA5) +#define kCmdNormalDisplay ((uint8_t)0xA6) // default +#define kCmdReverseDisplay ((uint8_t)0xA7) +#define kCmdMultiplexRatio ((uint8_t)0xA8) +#define kCmdDisplayOffset ((uint8_t)0xD3) +#define kCmdDisplayDivideRatio ((uint8_t)0xD5) +#define kCmdDischargeFront ((uint8_t)0x93) +#define kCmdDischargeBack ((uint8_t)0xD8) +#define kCmdPreCharge ((uint8_t)0xD9) +#define kCmdSEGpads ((uint8_t)0xDA) +#define kCmdVCOMDeselectLevel ((uint8_t)0xDB) +#define kCmdExternalIREF ((uint8_t)0xAD) + +///////////////////////////////////////////////////////////////////////////// +// Manufacturer Default Settings +// +// The default settings given in the sample quickstart code. +// +#define kDefaultMonoMode ((uint8_t)0x01) // grayscale by default "0x00" +#define kDefaultRowStart ((uint8_t)0x00) // default +#define kDefaultColStart ((uint8_t)0x00) // default +#define kDefaultRowEnd ((uint8_t)0x3F) // default +#define kDefaultColEnd ((uint8_t)0x1F) // default +#define kDefaultDisplayStart ((uint8_t)0x00) // default +#define kDefaultHorizontalAddressing ((uint8_t)0x00) // default +#define kDefaultContrast ((uint8_t)0xC8) // default = 0x80, upper end = 0xFF +#define kDefaultRotateDisplayNinety ((uint8_t)0x01) // Default is 0 degrees +#define kDefaultMultiplexRatio ((uint8_t)0x7F) // default is 0x9F +#define kDefaultDisplayOffset ((uint8_t)0x10) // default is 0x00 +#define kDefaultDivideRatio ((uint8_t)0x00) // default +#define kDefaultDischargeFront ((uint8_t)0x02) // default is 0x02 +#define kDefaultDischargeBack ((uint8_t)0x02) // default +#define kDefaultPreCharge ((uint8_t)0x1F) // 0x1F is default +#define kDefaultSegPads ((uint8_t)0x00) // default +#define kDefaultVCOMDeselect ((uint8_t)0x3F) // default +#define kDefaultExternalIREF ((uint8_t)0x02) + +///////////////////////////////////////////////////////////////////////////// +// Other Constant Definitions +// +// Other constants used by this driver for basic operation +// + + + +////////////////////////////////////////////////////////////////////////////////// +// Screen Buffer +// +// A key feature of this library is that it only sends "dirty" pixels to the +// device, minimizing data transfer over the I2C bus. To accomplish this, the +// dirty range of each graphics buffer page (see device memory layout in the +// datasheet) is maintained during drawing operation. Whe data is sent to the +// device, only the pixels in these regions are sent to the device, not the +// entire page of data. +// +// The below macros are used to manage the record keeping of dirty page ranges. +// Given that these actions are taking place in the draw loop, macros are used +// for performance considerations. +// +// These macros work with the pageState_t struct type. +// +// Define unique values just outside of the screen buffer (1120) page range +// (0 base) Note: A page is 128 bits in length + +#define kPageMin -1 // outside bounds - low value +#define kPageMax 128 // outside bounds - high value + + +// clean/ no settings in the page +#define pageIsClean(_page_) (_page_.xmin == kPageMax) + +// Macro to reset page descriptor +#define pageSetClean(_page_) \ + do \ + { \ + _page_.xmin = kPageMax; \ + _page_.xmax = kPageMin; \ + } while (false) + +// Macro to check and adjust record bounds based on a single location +#define pageCheckBounds(_page_, _x_) \ + do \ + { \ + if (_x_ < _page_.xmin) \ + _page_.xmin = _x_; \ + if (_x_ > _page_.xmax) \ + _page_.xmax = _x_; \ + } while (false) + +// Macro to check and adjust record bounds using another page descriptor +#define pageCheckBoundsDesc(_page_, _page2_) \ + do \ + { \ + if (_page2_.xmin < _page_.xmin) \ + _page_.xmin = _page2_.xmin; \ + if (_page2_.xmax > _page_.xmax) \ + _page_.xmax = _page2_.xmax; \ + } while (false) + +// Macro to check and adjust record bounds using bounds values +#define pageCheckBoundsRange(_page_, _x0_, _x1_) \ + do \ + { \ + if (_x0_ < _page_.xmin) \ + _page_.xmin = _x0_; \ + if (_x1_ > _page_.xmax) \ + _page_.xmax = _x1_; \ + } while (false) + +//////////////////////////////////////////////////////////////////////////////////// +// Pixel write/set operations +// +// Using LAMBDAs to create fast raster write/set operations. Using this pattern +// eleminates the need for switch/if statements in each draw routine. This is +// basically classic ROPs' +// +// NOTE - the order in the arrays is based on grRasterOp_t enum +// +// The Graphic operator functions (ROPS) +// - Copy - copy the pixel value in to the buffer (default) +// - Not Copy - copy the not of the pixel value to buffer +// - Not - Set the buffer value to not it's current value +// - XOR - XOR of color and current pixel value +// - Black - Set value to always be black +// - White - set value to always be white + +typedef void (*rasterOPsFn)(uint8_t *dest, uint8_t src, uint8_t mask); + +static const rasterOPsFn m_rasterOps[] = { + // COPY + [](uint8_t *dst, uint8_t src, uint8_t mask) -> void { *dst = (~mask & *dst) | (src & mask); }, + // NOT COPY + [](uint8_t *dst, uint8_t src, uint8_t mask) -> void { *dst = (~mask & *dst) | ((!src) & mask); }, + // NOT DEST + [](uint8_t *dst, uint8_t src, uint8_t mask) -> void { *dst = (~mask & *dst) | ((!(*dst)) & mask); }, + // XOR + [](uint8_t *dst, uint8_t src, uint8_t mask) -> void { *dst = (~mask & *dst) | ((*dst ^ src) & mask); }, + // Always Black + [](uint8_t *dst, uint8_t src, uint8_t mask) -> void { *dst = ~mask & *dst; }, + // Always White + [](uint8_t *dst, uint8_t src, uint8_t mask) -> void { *dst = mask | *dst; }}; + +//////////////////////////////////////////////////////////////////////////////////// +// setup defaults - called from constructors +// +// Just a bunch of member variable inits (TODO: should these be the defaults on reset or what we want them to be after init?) + +void QwGrCH1120::setupDefaults(void) +{ + default_address = {0}; + m_pBuffer = {nullptr}; + m_color = {1}; + m_rop = {grROPCopy}; + m_i2cBus = {nullptr}; + m_i2cAddress = {0x3C}; // address of the device (0x3D for closed) + // m_initHWComPins = {kDefaultPinConfig}; + m_initPreCharge = {kDefaultPreCharge}; + // m_initVCOMDeselect = {kDefaultVCOMDeselect}; + m_initContrast = {kDefaultContrast}; + m_isInitialized = {false}; +} + +//////////////////////////////////////////////////////////////////////////////////// +// init() +// +// Called by user when the device/system is up and ready to be "initialized." +// +// This implementation performs the basic setup for the CH1120 device +// +// The startup sequence is as follows: +// +// - Make sure a device is connected +// - Call super class +// - Shutdown the device (display off), initial device setup, turn on +// device +// - Init the local graphics buffers/system +// +// When this method is complete, the driver and device are ready for use +// +bool QwGrCH1120::init(void) +{ + if (m_isInitialized) + return true; + + // do we have a bus yet? Buffer? Note - buffer is set by subclass of this + // object + if (!m_i2cBus || !m_i2cAddress || !m_pBuffer) + return false; + + // Is the device connected? + if (!m_i2cBus->ping(m_i2cAddress)) + return false; + + // Call super class init + if (!(this->QwGrBufferDevice::init())) + return false; + + // setup the oled device + setupOLEDDevice(); + + // Finish up setting up this object + + // number of pages used for this device? + m_nPages = m_viewport.height / kByteNBits; // height / number of pixels per byte. + // TODO - support multiples != 8 + + // init the graphics buffers + initBuffers(); + + m_isInitialized = true; + + return true; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// reset() +// +// Return the OLED system to its initial state +// +// Returns true on success, false on failure + +bool QwGrCH1120::reset(bool clearDisplay) +{ + // If we are not in an init state, just call init + if (!m_isInitialized) + return init(); + + // is the device connected? + if (!m_i2cBus->ping(m_i2cAddress)) + return false; + + // setup oled + setupOLEDDevice(clearDisplay); + + // Init internal/drawing buffers and device screen buffer + if (clearDisplay) + initBuffers(); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Configuration API +// +// This allows sub-classes to setup for their device, while preserving +// encapsulation. +// +// These should be called/set before calling init +// +// For details of each of these settings -- see the datasheet +// +void QwGrCH1120::setCommPins(uint8_t pin_code) +{ + // NOTE: For now, we won't really use this for the CH1120, but is here for compatibility/symmetry with other OLED drivers + m_initHWComPins = pin_code; +} + +void QwGrCH1120::setPreCharge(uint8_t pre_charge) +{ + m_initPreCharge = pre_charge; +} + +void QwGrCH1120::setVcomDeselect(uint8_t vcom_d) +{ + m_initVCOMDeselect = vcom_d; +} + +void QwGrCH1120::setContrast(uint8_t contrast) +{ + if (!m_isInitialized) + m_initContrast = contrast; + else + sendDevCommand(kCmdContrastControl, contrast); +} + +//////////////////////////////////////////////////////////////////////////////////// +// setupOLEDDevice() +// +// Method sends the init/setup commands to the OLED device, placing +// it in a state for use by this driver/library. +void QwGrCH1120::setupOLEDDevice(bool clearDisplay){ + if (clearDisplay) + sendDevCommand(kCmdDisplayOff); + + sendDevCommand(kCmdRowStartEnd, kDefaultRowStart, kDefaultRowEnd); + sendDevCommand(kCmdColStartEnd, kDefaultColStart, kDefaultColEnd); + sendDevCommand(kCmdStartLine, kDefaultDisplayStart); + sendDevCommand(kCmdContrastControl, m_initContrast); + sendDevCommand(kCmdGrayMono, kDefaultMonoMode); + sendDevCommand(kCmdHorizAddressing, kDefaultHorizontalAddressing); + sendDevCommand(kCmdSegRemapDown); + sendDevCommand(kCmdComOutScan0First); + sendDevCommand(kCmdDisplayRotation, kDefaultRotateDisplayNinety); + sendDevCommand(kCmdDisableEntireDisplay); + sendDevCommand(kCmdNormalDisplay); + sendDevCommand(kCmdMultiplexRatio, kDefaultMultiplexRatio); + sendDevCommand(kCmdDisplayOffset, kDefaultDisplayOffset); + sendDevCommand(kCmdDischargeFront, kDefaultDischargeFront); + sendDevCommand(kCmdDischargeBack, kDefaultDischargeBack); + sendDevCommand(kCmdPreCharge, m_initPreCharge); + sendDevCommand(kCmdSEGpads, kDefaultSegPads); + sendDevCommand(kCmdVCOMDeselectLevel, kDefaultVCOMDeselect); + sendDevCommand(kCmdExternalIREF, kDefaultExternalIREF); + + // TODO: In Eli's example, we also performed additional commands after the manufacturer setup: + // writeCommandParameter(0xB0, 0x00); + // writeCommand(0x00); + // writeCommand(0x10); + // Is this needed? + + if (clearDisplay) + // now, turn it back on + sendDevCommand(kCmdDisplayOn); +} + +//////////////////////////////////////////////////////////////////////////////////// +// setCommBus() +// +// Method to set the bus object that is used to communicate with the device +// +// TODO - In the *future*, generalize to match SDK + +void QwGrCH1120::setCommBus(QwI2C &theBus, uint8_t id_bus) +{ + m_i2cBus = &theBus; + m_i2cAddress = id_bus; +} + +//////////////////////////////////////////////////////////////////////////////////// +// setBuffer() +// +// Protected method - used by sub-class to set the graphics buffer array. +// +// The subclass knows the size of the specific device, so it statically defines +// the graphics buffer array. The buffer is often set in the subclasses +// on_initialize() method. +// +// +void QwGrCH1120::setBuffer(uint8_t *pBuffer) +{ + if (pBuffer) + m_pBuffer = pBuffer; +} + +//////////////////////////////////////////////////////////////////////////////////// +// clearScreenBuffer() +// +// Clear out all the on-device memory. +// +void QwGrCH1120::clearScreenBuffer(void) +{ + // Clear out the screen buffer on the device + uint8_t emptyPage[kPageMax] = {0}; + + for (int i = 0; i < kMaxPageNumber; i++) + { + setScreenBufferAddress(i, 0); // start of page + sendDevData((uint8_t *)emptyPage, kPageMax); // clear out page + } +} + +//////////////////////////////////////////////////////////////////////////////////// +// initBuffers() +// +// Will clear the local graphics buffer, and the devices screen buffer. Also +// resets page state descriptors to a "clean" state. +void QwGrCH1120::initBuffers(void) +{ + int i; + + // clear out the local graphics buffer + if (m_pBuffer) + memset(m_pBuffer, 0, m_viewport.width * m_nPages); + + // Set page descs to "clean" state + for (i = 0; i < m_nPages; i++) + { + pageSetClean(m_pageState[i]); + pageSetClean(m_pageErase[i]); + } + + m_pendingErase = false; + + // clear out the screen buffer + clearScreenBuffer(); +} + +//////////////////////////////////////////////////////////////////////////////////// +// resendGraphics() +// +// Re-send the region in the graphics buffer (local) that contains drawn +// graphics. This region is defined by the contents of the m_pageErase +// descriptors. +// +// Copy these to the page state, and call display +// + +void QwGrCH1120::resendGraphics(void) +{ + // Set the page state dirty bounds to the bounds of erase state + for (int i = 0; i < m_nPages; i++) + m_pageState[i] = m_pageErase[i]; + + display(); // push bits to screen buffer +} + +//////////////////////////////////////////////////////////////////////////////////// +// Screen Control + +// TODO: It might make everything cleaner if we instead enumerate the flips as separate commands... + +//////////////////////////////////////////////////////////////////////////////////// +// flip_vert() +// +// Flip the onscreen graphics vertically. +void QwGrCH1120::flipVert(bool bFlip) +{ + sendDevCommand(bFlip ? kCmdComOutScan0Last : kCmdComOutScan0First); +} + +//////////////////////////////////////////////////////////////////////////////////// +// flip_horz() +// +// Flip the onscreen graphcis horizontally. This requires a resend of the +// graphics data to the device/screen buffer. +// +void QwGrCH1120::flipHorz(bool bFlip) +{ + sendDevCommand(bFlip ? kCmdSegRemapUp : kCmdSegRemapDown); + clearScreenBuffer(); + resendGraphics(); +} + +//////////////////////////////////////////////////////////////////////////////////// +// invert() +// +// Inverts the display contents on device +// +void QwGrCH1120::invert(bool bInvert) +{ + sendDevCommand(bInvert ? kCmdReverseDisplay : kCmdNormalDisplay); +} + +//////////////////////////////////////////////////////////////////////////////////// +void QwGrCH1120::stopScroll(void) +{ + sendDevCommand(kCmdDeactivateScroll); + + // After sending a deactivate command, the ram data in the device needs to be + // re-written. See datasheet + // + // First clear out the entire screen buffer (on device mem). The device uses + // this off screen area to scroll - if you don't erase it, when scroll starts + // back up, old graphics turds will appear ... + // + // Second - Send over the graphics again to the display + + clearScreenBuffer(); + resendGraphics(); +} + +//////////////////////////////////////////////////////////////////////////////////// +// scroll() +// +// Set scroll parametes on the device and start scrolling +// +void QwGrCH1120::scroll(uint16_t scroll_type, uint8_t start, uint8_t stop, uint8_t interval) +{ + // parameter sanity? + if (stop < start) + return; + + // Setup a default command list + uint8_t n_commands = 6; + + // See page 48 of the CH1120 datasheet. This differs from the SSD1306 + // This is a 6 Byte Command With the Bytes: + // 0) Scroll Direction Set (0x24 - down, 0x25 - up, 0x26 - right, 0x27 - left) + // 1) Start Column Position Set (0x00 to 0x9F) + // 2) End Column Position Set (0x00 to 0x9F) + // 3) Start Row Position Set (0x00 to 0x9F) + // 4) End Row Position Set (0x00 to 0x9F) + // 5) Time Interval Set (X + B0) where B0 is 0b000 (6frames) to 0b111 (2 frames) (with several options up to 128 frames in between) + + uint8_t commands[n_commands] = {kCmdRightHorizontalScroll, // default scroll right + 0x00, // default 0x00 start column + 0x80, // let's default to 128 for the 128x128 display + 0x00, // default 0x00 start row + 0x80, // let's default to 128 for the 128x128 display + 0x00 // default of 6 frames + }; + + + // Which way to scroll + switch (scroll_type) + { + case SCROLL_LEFT: + commands[0] = kCmdLeftHorizontalScroll; + commands[1] = start; + commands[2] = stop; + break; + case SCROLL_UP: + commands[0] = kCmdUpVerticalScroll; + commands[3] = start; + commands[4] = stop; + break; + case SCROLL_DOWN: + commands[0] = kCmdDownVerticalScroll; + commands[3] = start; + commands[4] = stop; + break; + case SCROLL_RIGHT: + default: + // Note that the 1306 (and thus the higher level driver) support scroll right vert etc. which we don't. We'll stick w/ horizontal right in this case + commands[0] = kCmdRightHorizontalScroll; + commands[1] = start; + commands[2] = stop; + } + + // send the scroll commands to the device + + // Do not use scroll_stop() - that method resets the display - memory ...etc - + // to it's start state - the graphics displayed before scrolling was initially + // started. + // + // Here, we just stop scrolling and keep device memory state as is. This + // allows scrolling to change paraterms during a scroll session - gives a + // smooth presentation on screen. + sendDevCommand(kCmdDeactivateScroll); + sendDevCommand(commands, n_commands); + sendDevCommand(kCmdActivateScroll); +} + +//////////////////////////////////////////////////////////////////////////////////// +// displayPower() +// +// Used to set the power of the screen. +void QwGrCH1120::displayPower(bool enable) +{ + if (!m_isInitialized) + return; + + sendDevCommand((enable ? kCmdDisplayOn : kCmdDisplayOff)); +} + +//////////////////////////////////////////////////////////////////////////////////// +// Drawing Methods + +//////////////////////////////////////////////////////////////////////////////////// +// erase() +// +// Erase the graphics that are on screen and anything that's been draw but +// haven't been sent to the screen. +// + +void QwGrCH1120::erase(void) +{ + if (!m_pBuffer) + return; + + // Cleanup the dirty parts of each page in the graphics buffer. + for (uint8_t i = 0; i < m_nPages; i++) + { + // m_pageState + // The current "dirty" areas of the graphics [local] buffer. + // Areas that haven't been sent to the screen/device but are + // "dirty" + // + // Add the areas with pixels set and have been sent to the + // device - this is the contents of m_pageErase + + pageCheckBoundsDesc(m_pageState[i], m_pageErase[i]); + + // if this page is clean, there is nothing to update + if (pageIsClean(m_pageState[i])) + continue; + + // clear out memory that is dirty on this page + memset(m_pBuffer + i * m_viewport.width + m_pageState[i].xmin, 0, + m_pageState[i].xmax - m_pageState[i].xmin + 1); // add one b/c values are 0 based + + // clear out any pending dirty range for this page - it's erased + pageSetClean(m_pageState[i]); + } + + // Indicate that the data transfer to the device should include the erase + // region + m_pendingErase = true; +} + +//////////////////////////////////////////////////////////////////////////////////// +// +// draw_pixel() +// +// Used to set a pixel in the graphics buffer - uses the current write operator +// function +// + +void QwGrCH1120::drawPixel(uint8_t x, uint8_t y, uint8_t clr) +{ + // quick sanity check on range + if (x >= m_viewport.width || y >= m_viewport.height) + return; // out of bounds + + uint8_t bit = byte_bits[mod_byte(y)]; + + m_rasterOps[m_rop](m_pBuffer + x + y / kByteNBits * m_viewport.width, // pixel offset + (clr ? bit : 0), bit); // which bit to set in byte + + pageCheckBounds(m_pageState[y / kByteNBits], + x); // update dirty range for page +} + +//////////////////////////////////////////////////////////////////////////////////// +// draw_line_horz() +// +// Fast horizontal line drawing routine +// +void QwGrCH1120::drawLineHorz(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t clr) +{ + // Basically we set a bit within a range in a page of our graphics buffer. + + // in range + if (y0 >= m_viewport.height) + return; + + if (x0 > x1) + swap_int(x0, x1); + + if (x1 >= m_viewport.width) + x1 = m_viewport.width - 1; + + uint8_t bit = byte_bits[mod_byte(y0)]; // bit to set + rasterOPsFn curROP = m_rasterOps[m_rop]; // current raster op + + // Get the start of this line in the graphics buffer + uint8_t *pBuffer = m_pBuffer + x0 + y0 / kByteNBits * m_viewport.width; + + // walk up x and set the target pixel using the pixel operator function + for (int i = x0; i <= x1; i++, pBuffer++) + curROP(pBuffer, (clr ? bit : 0), bit); + + // Mark the page dirty for the range drawn + pageCheckBoundsRange(m_pageState[y0 / kByteNBits], x0, x1); +} + +//////////////////////////////////////////////////////////////////////////////////// +// draw_line_vert() +// +// Fast vertical line drawing routine - also supports fast filled rects +// +void QwGrCH1120::drawLineVert(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t clr) +{ + if (x0 >= m_viewport.width) // out of bounds + return; + + // want an accending order + if (y0 > y1) + swap_int(y0, y1); + + // keep on screen + if (y1 >= m_viewport.height) + y1 = m_viewport.height - 1; + + uint8_t startBit, endBit, setBits; + + // Get the start and end pages we are writing to + uint8_t page0 = y0 / kByteNBits; + uint8_t page1 = y1 / kByteNBits; + + // loop over the pages. For each page determine the range of pixels + // to set in the target page byte and then set them using the current + // pixel operator function + + // Note: This function can also be used to draw filled rects - just iterate + // in the x direction. The base rect fill (in grBuffer) calls this + // method x1-x0 times, and each of those calls has some overhead. So + // just iterating over each page - x1-x0 times here - saves overhead + // costs. + // + // To make this work, make sure x0 > x1. Also, this method is wired in + // as the draw_rect_filled entry in the draw interface. This is done + // above in the init process. + + int xinc; + if (x0 > x1) + swap_int(x0, x1); + + rasterOPsFn curROP = m_rasterOps[m_rop]; // current raster op + + for (int i = page0; i <= page1; i++) + { + startBit = mod_byte(y0); // start bit in this byte + + // last bit of this byte to set? Does the line end in this byte, or continue + // on... + endBit = y0 + kByteNBits - startBit > y1 ? mod_byte(y1) : kByteNBits - 1; + + // Set the bits from startBit to endBit + setBits = (0xFF >> ((kByteNBits - endBit) - 1)) << startBit; // what bits are being set in this byte + + // set the bits in the graphics buffer using the current byte operator + // function + + // Note - We iterate over x to fill in a rect if specified. + for (xinc = x0; xinc <= x1; xinc++) + curROP(m_pBuffer + i * m_viewport.width + xinc, (clr ? setBits : 0), setBits); + + y0 += endBit - startBit + 1; // increment Y0 to next page + + pageCheckBoundsRange(m_pageState[i], x0, + x1); // mark dirty range in page desc + } +} + +//////////////////////////////////////////////////////////////////////////////////////// +// draw_rect_fill() +// +// Does the actual drawing/logic + +void QwGrCH1120::drawRectFilled(uint8_t x0, uint8_t y0, uint8_t width, uint8_t height, uint8_t clr) +{ + uint8_t x1 = x0 + width - 1; + uint8_t y1 = y0 + height - 1; + + // just call vert line + drawLineVert(x0, y0, x1, y1, clr); +} + +//////////////////////////////////////////////////////////////////////////////////// +// draw_bitmap() +// +// Draw a 8 bit encoded (aka same y layout as this device) bitmap to the screen +// + +void QwGrCH1120::drawBitmap(uint8_t x0, uint8_t y0, uint8_t dst_width, uint8_t dst_height, uint8_t *pBitmap, + uint8_t bmp_width, uint8_t bmp_height) +{ + // some simple checks + if (x0 >= m_viewport.width || y0 >= m_viewport.height || !bmp_width || !bmp_height) + return; + + // Bounds check + if (x0 + dst_width > m_viewport.width) // out of bounds + dst_width = m_viewport.width - x0; + + if (bmp_width < dst_width) + dst_width = bmp_width; + + if (y0 + dst_height > m_viewport.height) // out of bounds + dst_height = m_viewport.height - y0; + + if (bmp_height < dst_height) + dst_height = bmp_height; + + // current position in the bitmap + uint8_t bmp_x = 0; + uint8_t bmp_y = 0; + + uint8_t page0, page1; + uint8_t startBit, endBit, grSetBits, grStartBit; + + uint8_t bmp_mask[2], bmp_data, bmpPage; + uint8_t remainingBits, neededBits; + + uint8_t y1 = y0 + dst_height - 1; + + page0 = y0 / kByteNBits; + page1 = y1 / kByteNBits; + + rasterOPsFn curROP = m_rasterOps[m_rop]; // current raster op + + // The Plan: + // - Walk down the graphics buffer range (y) one page at a time + // - For each page + // - Determine needed number of bits for the destination + // - Determine what bits to pull from the bitmap + // - Create a mask to pull out bits - from one or two bytes + // - Loop over the x dimension + // - pull bits from bitmap, build byte of data of bitmap bits, in + // right order for the destination (graphics buffer) + // - Write the bitmap bits to the graphis buffer using the + // current operator + + // Loop over the memory pages in the graphics buffer + for (int iPage = page0; iPage <= page1; iPage++) + { + // First, get the number of destination bits in the current page + grStartBit = mod_byte(y0); // start bit + + // last bit of this byte to set? Does the copy region end in this byte, or + // continue on... + endBit = y0 + kByteNBits - grStartBit > y1 ? mod_byte(y1) : kByteNBits - 1; + + // Set the bits from startBit to endBit + grSetBits = (0xFF >> (kByteNBits - endBit - 1)) << grStartBit; // what bits are being set in this byte + + // how many bits of data do we need to transfer from the bitmap? + neededBits = endBit - grStartBit + 1; + + // Okay, we have how much data to transfer to the current page. Now build + // the data from the bitmap. + + // First, build bit masks for pulling the data out of the bmp array. The + // data might straddle two bytes, so build two masks + + // as above, get the start and end bites for the current position in the + // bmp. + startBit = mod_byte(bmp_y); + endBit = (kByteNBits - startBit > neededBits ? startBit + neededBits : kByteNBits) - 1; + + // Set the bits from startBit to endBit + bmp_mask[0] = (0xFF >> (kByteNBits - endBit - 1)) << startBit; + + // any remaining bits to get? + remainingBits = neededBits - (endBit - startBit + 1); // +1 - needsBits is 1's based + bmp_mask[1] = 0xFF >> (kByteNBits - remainingBits); + + // What row in the source bitmap + bmpPage = bmp_y / kByteNBits; + + // we have the mask for the bmp - loop over the width of the copy region, + // pulling out bmp data and writing it to the graphics buffer + for (bmp_x = 0; bmp_x < dst_width; bmp_x++) + { + // get data bits out of current bitmap location and shift if needed + bmp_data = (pBitmap[bmp_width * bmpPage + bmp_x] & bmp_mask[0]) >> startBit; + + if (remainingBits) // more data to add from the next byte in this column + bmp_data |= (pBitmap[bmp_width * (bmpPage + 1) + bmp_x] & bmp_mask[1]) << (neededBits - remainingBits); + + // Write the bmp data to the graphics buffer - using current write op. + // Note, if the location in the buffer didn't start at bit 0, we shift + // bmp_data + curROP(m_pBuffer + iPage * m_viewport.width + bmp_x + x0, bmp_data << grStartBit, grSetBits); + } + // move up our y values (graphics buffer and bitmap) by the number of bits + // transferred + y0 += neededBits; + bmp_y += neededBits; + + pageCheckBoundsRange(m_pageState[iPage], x0, + x0 + dst_width); // mark dirty range in page desc + } +} + +//////////////////////////////////////////////////////////////////////////////////// +// Device Update Methods +//////////////////////////////////////////////////////////////////////////////////// +// setScreenBufferAddress() +// +// Sets the target screen buffer address for graphics buffer transfer to the +// device. +// +// The positon is specified by page and column +// +// The system runs in "page mode" - data is streamed along a page, based +// on the set starting position. +// +// This class takes advantage of this to just write the "dirty" ranges in a +// page. +// +// Page can be 0 to 0x9F +// Column can be + +bool QwGrCH1120::setScreenBufferAddress(uint8_t page, uint8_t column) +{ + if (page >= m_nPages || column >= m_viewport.width) + return false; + + // send the page (row) address + sendDevCommand(kCmdStartRow, page); // difference from the 1306, bytes sent after each other instead of OR'd together... + + // For the column start address, add the viewport x offset. Some devices + // (Micro OLED) don't start at column 0 in the screen buffer + sendDevCommand((kCmdStartColHigh | (column >> 4)) + m_viewport.x); + sendDevCommand(kCmdStartColLow & column); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////// +// display() +// +// Send the "dirty" areas of the graphics buffer to the device's screen buffer. +// Only send the areas that need to be updated. The update region is based on +// new graphics to display, and any currently displayed items that need to be +// erased. + +void QwGrCH1120::display() +{ + // Loop over our page descriptors - if a page is dirty, send the graphics + // buffer dirty region to the device for the current page + + pageState_t transferRange; + + for (int i = 0; i < m_nPages; i++) + { + // We keep the erase rect seperate from dirty rect. Make temp copy of + // dirty rect page range, expand to include erase rect page range. + + transferRange = m_pageState[i]; + + // If an erase has happend, we need to transfer/include erase update range + if (m_pendingErase) + pageCheckBoundsDesc(transferRange, m_pageErase[i]); + + if (pageIsClean(transferRange)) // both dirty and erase range for this + // page were null + continue; // next + + // set the start address to write the updated data to the devices screen + // buffer + setScreenBufferAddress(i, transferRange.xmin); + + // send the dirty data to the device + sendDevData(m_pBuffer + (i * m_viewport.width) + transferRange.xmin, // this page start + xmin + transferRange.xmax - transferRange.xmin + 1); // dirty region xmax - xmin. Add 1 b/c 0 based + + // If we sent the erase bounds, zero out the erase bounds - this area is now + // clear + if (m_pendingErase) + pageSetClean(m_pageErase[i]); + + // add the just send dirty range (non erase rec) to the erase rect + pageCheckBoundsDesc(m_pageErase[i], m_pageState[i]); + + // this page is no longer dirty - mark it clean + pageSetClean(m_pageState[i]); + } + m_pendingErase = false; // no longer pending +} + + +//////////////////////////////////////////////////////////////////////////////////// +// Device communication methods +//////////////////////////////////////////////////////////////////////////////////// +// sendDeviceCommand() +// +// send a single command to the device via the current bus object + +void QwGrCH1120::sendDevCommand(uint8_t command) +{ + m_i2cBus->writeRegisterByte(m_i2cAddress, kCmdControlByte, command); +} + +//////////////////////////////////////////////////////////////////////////////////// +// sendDeviceCommand() +// +// send a single command and value to the device via the current bus object. + +void QwGrCH1120::sendDevCommand(uint8_t *commands, uint8_t n_commands) +{ + if (!commands || n_commands == 0) + return; + + m_i2cBus->writeRegisterRegion(m_i2cAddress, kCmdControlByte, commands, n_commands); +} + +//////////////////////////////////////////////////////////////////////////////////// +// sendDeviceCommand() +// +// send a single command and value to the device via the current bus object. +// + +void QwGrCH1120::sendDevCommand(uint8_t command, uint8_t value) +{ + uint8_t buffer[] = {command, value}; + + sendDevCommand(buffer, 2); +} + +//////////////////////////////////////////////////////////////////////////////////// +// sendDeviceCommand() +// +// send a single command and value to the device via the current bus object. +// +void QwGrCH1120::sendDevCommand(uint8_t command, uint8_t start, uint8_t stop) +{ + uint8_t buffer[] = {command, start, stop}; + + sendDevCommand(buffer, 3); +} + +//////////////////////////////////////////////////////////////////////////////////// +// sendDeviceData() +// +// send a block of data to the RAM of the device via the current bus object +void QwGrCH1120::sendDevData(uint8_t *pData, uint8_t nData) +{ + if (!pData || nData == 0) + return; + + m_i2cBus->writeRegisterRegion(m_i2cAddress, kCmdRamControlByte, pData, nData); +} \ No newline at end of file diff --git a/src/qwiic_grch1120.h b/src/qwiic_grch1120.h new file mode 100644 index 0000000..79914e3 --- /dev/null +++ b/src/qwiic_grch1120.h @@ -0,0 +1,192 @@ +// qwiic_gr1120.h +// +// This is a library written for SparkFun Qwiic OLED boards that use the CH1120. +// +// Written by SparkFun Electronics, March 2025 +// +// This library configures and draws graphics to OLED boards that use the +// CH1120 display hardware. The library only supports I2C. +// +// Repository: +// https://github.com/sparkfun/SparkFun_Qwiic_OLED_Arduino_Library +// +// Documentation: +// https://sparkfun.github.io/SparkFun_Qwiic_OLED_Arduino_Library/ +// +// +// SparkFun code, firmware, and software is released under the MIT License(http://opensource.org/licenses/MIT). +// +// SPDX-License-Identifier: MIT +// +// The MIT License (MIT) +// +// Copyright (c) 2025 SparkFun Electronics +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to +// do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial +// portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +#pragma once + +#include "qwiic_grbuffer.h" +#include "qwiic_i2c.h" +#include "res/qwiic_resdef.h" +#include "qwiic_grcommon.h" + +///////////////////////////////////////////////////////////////////////////// +// Flags for scrolling +///////////////////////////////////////////////////////////////////////////// +#define SCROLL_RIGHT 0x02 +#define SCROLL_LEFT 0x04 +#define SCROLL_UP 0x08 +#define SCROLL_DOWN 0x10 + +#define SCROLL_INTERVAL_6_FRAMES 0x00 +#define SCROLL_INTERVAL_32_FRAMES 0x01 +#define SCROLL_INTERVAL_64_FRAMES 0x02 +#define SCROLL_INTERVAL_128_FRAMES 0x03 +#define SCROLL_INTERVAL_3_FRAMES 0x04 +#define SCROLL_INTERVAL_4_FRAMES 0x05 +#define SCROLL_INTERVAL_5_FRAMES 0x06 +#define SCROLL_INTERVAL_2_FRAMES 0x07 + +#define kMaxPageNumber 8 + +///////////////////////////////////////////////////////////////////////////// +// Buffer Management +///////////////////////////////////////////////////////////////////////////// +class QwGrCH1120 : public QwGrBufferDevice { + private: + void setupDefaults(void); + + public: + QwGrCH1120() { + setupDefaults(); // default constructor - always called + } + QwGrCH1120(uint8_t width, uint8_t height) : QwGrCH1120(0, 0, width, height) {}; + + // call super class + QwGrCH1120(uint8_t x0, uint8_t y0, uint8_t width, uint8_t height) : QwGrBufferDevice(x0, y0, width, height) { + setupDefaults(); + }; + + // Public draw methods + void display(void); // send graphics buffer to the devices screen buffer + void erase(void); + + // Device setup + virtual bool init(void); + + bool isInitialized(void) { + return m_isInitialized; + }; + + bool reset(bool clearDisplay = true); + + // method to set the communication bus this object should use + void setCommBus(QwI2C &theBus, uint8_t id_bus); + + // Set the current color/pixel write operation + void setColor(uint8_t color); + + // Settings/operational methods + void setContrast(uint8_t); + + // default address of the device - expect the sub to fill in. + uint8_t default_address; + + void setRasterOp(grRasterOp_t rop) { + m_rop = rop; + } + + // screen control + void invert(bool); + void flipVert(bool); + void flipHorz(bool); + + // screen scrolling + void stopScroll(void); + void scroll(uint16_t scroll_type = SCROLL_RIGHT, uint8_t start = 0, uint8_t stop = 128, uint8_t interval = SCROLL_INTERVAL_2_FRAMES); + + void displayPower(bool enable = true); + + protected: + // Subclasses of this class define the specifics of the device, including size. + // Subclass needs to define the graphics buffer array - stack based - and pass in + void setBuffer(uint8_t *pBuffer); + + /////////////////////////////////////////////////////////////////////////// + // Internal, fast draw routines - this are used in the overall + // draw interface (_QwIDraw) for this object/device/system. + // + // >> Pixels << + void drawPixel(uint8_t x, uint8_t y, uint8_t clr); + + // >> Fast Lines << + void drawLineHorz(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t clr); + void drawLineVert(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t clr); + + // fast rect fill + void drawRectFilled(uint8_t x0, uint8_t y0, uint8_t width, uint8_t height, uint8_t clr); + + // >> Fast Bitmap << + void drawBitmap(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t *pBitmap, uint8_t bmp_width, + uint8_t bmp_height); + + //TODO: Are these needed for this driver? + ///////////////////////////////////////////// + // configuration methods for sub-classes. Settings unique to a device + void setCommPins(uint8_t); + void setPreCharge(uint8_t); + void setVcomDeselect(uint8_t); + + private: + // Internal buffer management methods + bool setScreenBufferAddress(uint8_t page, uint8_t column); + void initBuffers(void); // clear graphics and screen buffer + void clearScreenBuffer(void); + void resendGraphics(void); + void setupOLEDDevice(bool clearDisplay = true); + + // device communication methods + void sendDevCommand(uint8_t command); + void sendDevCommand(uint8_t command, uint8_t value); + void sendDevCommand(uint8_t *commands, uint8_t n); + void sendDevData(uint8_t *pData, uint8_t nData); + void sendDevCommand(uint8_t command, uint8_t start, uint8_t stop); + + ///////////////////////////////////////////////////////////////////////////// + // instance vars + + // Buffer variables + uint8_t *m_pBuffer; // Pointer to the graphics buffer + uint8_t m_nPages; // number of pages for current device + pageState_t m_pageState[kMaxPageNumber]; // page state descriptors + pageState_t m_pageErase[kMaxPageNumber]; // keep track of erase boundaries + bool m_pendingErase; + + // display variables + uint8_t m_color; // current color (really 0 or 1) + grRasterOp_t m_rop; // current raster operation code + + // I2C things + QwI2C *m_i2cBus; // pointer to our i2c bus object + uint8_t m_i2cAddress = 0x3C; // address of the device (0x3D for closed) + + // Stash values for settings that are unique to each device. + uint8_t m_initHWComPins; + uint8_t m_initPreCharge; + uint8_t m_initVCOMDeselect; + uint8_t m_initContrast; + + bool m_isInitialized; // general init flag +}; diff --git a/src/qwiic_grcommon.h b/src/qwiic_grcommon.h new file mode 100644 index 0000000..ddf4713 --- /dev/null +++ b/src/qwiic_grcommon.h @@ -0,0 +1,33 @@ +// Common defines for multiple +// TODO: Probably should rework how this looks eventually... +// possibly we will define another abstraction class and can also have these defines here... + +///////////////////////////////////////////////////////////////////////////// +// The graphics Raster Operator functions (ROPS) +///////////////////////////////////////////////////////////////////////////// +// - Copy - copy the pixel value in to the buffer (default) +// - Not Copy - copy the not of the pixel value to buffer +// - Not - Set the buffer value to not it's current value +// - XOR - XOR of color and current pixel value +// - Black - Set value to always be black +// - White - set value to always be white + +#pragma once + +#include + +typedef enum gr_op_funcs_ +{ + grROPCopy = 0, + grROPNotCopy = 1, + grROPNot = 2, + grROPXOR = 3, + grROPBlack = 4, + grROPWhite = 5 +} grRasterOp_t; + +typedef struct +{ + int16_t xmin; + int16_t xmax; +} pageState_t; diff --git a/src/qwiic_grssd1306.cpp b/src/qwiic_grssd1306.cpp index ce30fca..c702e28 100644 --- a/src/qwiic_grssd1306.cpp +++ b/src/qwiic_grssd1306.cpp @@ -482,6 +482,7 @@ void QwGrSSD1306::flipHorz(bool bFlip) clearScreenBuffer(); resendGraphics(); } + //////////////////////////////////////////////////////////////////////////////////// // invert() // diff --git a/src/qwiic_grssd1306.h b/src/qwiic_grssd1306.h index aca583f..d27aac6 100644 --- a/src/qwiic_grssd1306.h +++ b/src/qwiic_grssd1306.h @@ -52,6 +52,7 @@ #include "qwiic_grbuffer.h" #include "qwiic_i2c.h" #include "res/qwiic_resdef.h" +#include "qwiic_grcommon.h" ///////////////////////////////////////////////////////////////////////////// // Device Config @@ -77,15 +78,16 @@ // - Black - Set value to always be black // - White - set value to always be white -typedef enum gr_op_funcs_ -{ - grROPCopy = 0, - grROPNotCopy = 1, - grROPNot = 2, - grROPXOR = 3, - grROPBlack = 4, - grROPWhite = 5 -} grRasterOp_t; +// moved to common for now... +// typedef enum gr_op_funcs_ +// { +// grROPCopy = 0, +// grROPNotCopy = 1, +// grROPNot = 2, +// grROPXOR = 3, +// grROPBlack = 4, +// grROPWhite = 5 +// } grRasterOp_t; ///////////////////////////////////////////////////////////////////////////// // Flags for scrolling @@ -146,11 +148,12 @@ typedef enum gr_op_funcs_ #define kMaxPageNumber 8 -typedef struct -{ - int16_t xmin; - int16_t xmax; -} pageState_t; +// moved to common for now... +// typedef struct +// { +// int16_t xmin; +// int16_t xmax; +// } pageState_t; ///////////////////////////////////////////////////////////////////////////// // QwGrSSD1306 diff --git a/src/qwiic_oled_1in5.h b/src/qwiic_oled_1in5.h new file mode 100644 index 0000000..254871f --- /dev/null +++ b/src/qwiic_oled_1in5.h @@ -0,0 +1,92 @@ +// qwiic_oled_1in3.h +// +// This is a library written for SparkFun Qwiic OLED boards that use the CH1120. +// +// SparkFun sells these at its website: www.sparkfun.com +// +// Do you like this library? Help support SparkFun. Buy a board! +// +// Written by SparkFun Electronics, August 2025 +// +// This library configures and draws graphics to OLED boards that use the +// CH1120 display hardware. The library only supports I2C. +// +// Repository: +// https://github.com/sparkfun/SparkFun_Qwiic_OLED_Arduino_Library +// +// Documentation: +// https://sparkfun.github.io/SparkFun_Qwiic_OLED_Arduino_Library/ +// +// +// SparkFun code, firmware, and software is released under the MIT License(http://opensource.org/licenses/MIT). +// +// SPDX-License-Identifier: MIT +// +// The MIT License (MIT) +// +// Copyright (c) 2022 SparkFun Electronics +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to +// do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial +// portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Implementation for the 1.5" OLED device + +#pragma once + +#include "qwiic_grch1120.h" + +////////////////////////////////////////////////////////////////// +// Set the defaults for the SparkFun Qwiic 1.5" OLED + +#define kOLED1in5Width 128 +#define kOLED1in5Height 128 + +#define kOLED1in5XOffset 0 +#define kOLED1in5YOffset 0 + +// Parameters for this device +#define kOLED1in5PinConfig 0x12 +#define kOLED1in5PreCharge 0xF1 +#define kOLED1in5VCOM 0x40 +#define kOLED1in5Contrast 0xCF + +// 0x3C = Open, 0x3D = Closed +#define kOLED1in5DefaultAddress 0x3C +#define kOLED1in5AltAddress 0x3D + +class QwOLED1in5 : public QwGrCH1120 { + public: + // Constructor - setup the viewport and default address for this device. + + QwOLED1in5() : QwGrCH1120(kOLED1in5XOffset, kOLED1in5YOffset, kOLED1in5Width, kOLED1in5Height) { + default_address = kOLED1in5DefaultAddress; + } + + // set up the specific device settings + bool init(void) + { + setBuffer(m_graphicsBuffer); // The buffer to use + + setCommPins(kOLED1in5PinConfig); + setPreCharge(kOLED1in5PreCharge); + setVcomDeselect(kOLED1in5VCOM); + setContrast(kOLED1in5Contrast); + + // Call the super class to do all the work + return this->QwGrCH1120::init(); + } + + private: + // Graphics buffer for this device + uint8_t m_graphicsBuffer[kOLED1in5Width * kOLED1in5Height / 8]; + +}; \ No newline at end of file From 0b27f8704d1e9a53ca82979c828c6f133cdec047 Mon Sep 17 00:00:00 2001 From: Malcolm McKellips Date: Thu, 14 Aug 2025 16:00:02 -0600 Subject: [PATCH 2/5] fix max page number for 1.5in OLED --- src/qwiic_grch1120.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qwiic_grch1120.h b/src/qwiic_grch1120.h index 79914e3..2936be2 100644 --- a/src/qwiic_grch1120.h +++ b/src/qwiic_grch1120.h @@ -59,7 +59,7 @@ #define SCROLL_INTERVAL_5_FRAMES 0x06 #define SCROLL_INTERVAL_2_FRAMES 0x07 -#define kMaxPageNumber 8 +#define kMaxPageNumber 16 ///////////////////////////////////////////////////////////////////////////// // Buffer Management From 2930dad7a048d81dd7a0c1985c01a2f15ca39cf1 Mon Sep 17 00:00:00 2001 From: Malcolm McKellips Date: Tue, 19 Aug 2025 13:49:48 -0600 Subject: [PATCH 3/5] start peeling out elements of old dirty page tracking for ch1120 --- src/qwiic_grch1120.cpp | 260 ++++++++++++++--------------------------- src/qwiic_grch1120.h | 4 +- src/qwiic_oled_1in5.h | 2 +- 3 files changed, 88 insertions(+), 178 deletions(-) diff --git a/src/qwiic_grch1120.cpp b/src/qwiic_grch1120.cpp index b507d9d..c39ccc6 100644 --- a/src/qwiic_grch1120.cpp +++ b/src/qwiic_grch1120.cpp @@ -3,7 +3,11 @@ // // This is a library written for SparkFun Qwiic OLED boards that use the // CH1120. This driver is VERY similar to the SSD1306 driver, but with a few -// subtle differences. +// subtle differences. Currently, our only device with the CH1120 driver is +// the Qwiic OLED 1.5", on which row addressing has not been verified. +// So, this library currently writes out the entire screen buffer each refresh +// instead of only the dirty bits like the SSD1306 driver does. This makes this +// display operate slower than the others in the library. // // SparkFun sells these at its website: www.sparkfun.com // @@ -144,78 +148,6 @@ #define kDefaultVCOMDeselect ((uint8_t)0x3F) // default #define kDefaultExternalIREF ((uint8_t)0x02) -///////////////////////////////////////////////////////////////////////////// -// Other Constant Definitions -// -// Other constants used by this driver for basic operation -// - - - -////////////////////////////////////////////////////////////////////////////////// -// Screen Buffer -// -// A key feature of this library is that it only sends "dirty" pixels to the -// device, minimizing data transfer over the I2C bus. To accomplish this, the -// dirty range of each graphics buffer page (see device memory layout in the -// datasheet) is maintained during drawing operation. Whe data is sent to the -// device, only the pixels in these regions are sent to the device, not the -// entire page of data. -// -// The below macros are used to manage the record keeping of dirty page ranges. -// Given that these actions are taking place in the draw loop, macros are used -// for performance considerations. -// -// These macros work with the pageState_t struct type. -// -// Define unique values just outside of the screen buffer (1120) page range -// (0 base) Note: A page is 128 bits in length - -#define kPageMin -1 // outside bounds - low value -#define kPageMax 128 // outside bounds - high value - - -// clean/ no settings in the page -#define pageIsClean(_page_) (_page_.xmin == kPageMax) - -// Macro to reset page descriptor -#define pageSetClean(_page_) \ - do \ - { \ - _page_.xmin = kPageMax; \ - _page_.xmax = kPageMin; \ - } while (false) - -// Macro to check and adjust record bounds based on a single location -#define pageCheckBounds(_page_, _x_) \ - do \ - { \ - if (_x_ < _page_.xmin) \ - _page_.xmin = _x_; \ - if (_x_ > _page_.xmax) \ - _page_.xmax = _x_; \ - } while (false) - -// Macro to check and adjust record bounds using another page descriptor -#define pageCheckBoundsDesc(_page_, _page2_) \ - do \ - { \ - if (_page2_.xmin < _page_.xmin) \ - _page_.xmin = _page2_.xmin; \ - if (_page2_.xmax > _page_.xmax) \ - _page_.xmax = _page2_.xmax; \ - } while (false) - -// Macro to check and adjust record bounds using bounds values -#define pageCheckBoundsRange(_page_, _x0_, _x1_) \ - do \ - { \ - if (_x0_ < _page_.xmin) \ - _page_.xmin = _x0_; \ - if (_x1_ > _page_.xmax) \ - _page_.xmax = _x1_; \ - } while (false) - //////////////////////////////////////////////////////////////////////////////////// // Pixel write/set operations // @@ -310,7 +242,10 @@ bool QwGrCH1120::init(void) // Finish up setting up this object // number of pages used for this device? - m_nPages = m_viewport.height / kByteNBits; // height / number of pixels per byte. + // By default, we are operating this device in "rotate 90" mode with "horizontal addressing" + // Each "page" is a byte representing 8 pixels + // so the number of pages that it takes to span entire row is the width divided by the number of bits in a byte + m_nPages = m_viewport.width / kByteNBits; // width / number of pixels per byte. // TODO - support multiples != 8 // init the graphics buffers @@ -460,12 +395,16 @@ void QwGrCH1120::setBuffer(uint8_t *pBuffer) void QwGrCH1120::clearScreenBuffer(void) { // Clear out the screen buffer on the device - uint8_t emptyPage[kPageMax] = {0}; + // each row is m_nPages bytes wide + // and we have m_viewport.height rows + uint8_t emptyRow[m_nPages] = {0}; + + setScreenBufferAddress(0, 0); // Warning: This function works-ish but only for even-numbered rows. + // so we can use it here, but do not expect it to work in all instances - for (int i = 0; i < kMaxPageNumber; i++) + for (int i = 0; i < m_viewport.height; i++) { - setScreenBufferAddress(i, 0); // start of page - sendDevData((uint8_t *)emptyPage, kPageMax); // clear out page + sendDevData((uint8_t *)emptyRow, m_nPages); // clear out row } } @@ -478,17 +417,12 @@ void QwGrCH1120::initBuffers(void) { int i; + // TODO: the concept of ('width' and 'height' might be sorta swapped for this as opposed to old driver) + // that might not matter so much since this is square 128x128 // clear out the local graphics buffer if (m_pBuffer) memset(m_pBuffer, 0, m_viewport.width * m_nPages); - // Set page descs to "clean" state - for (i = 0; i < m_nPages; i++) - { - pageSetClean(m_pageState[i]); - pageSetClean(m_pageErase[i]); - } - m_pendingErase = false; // clear out the screen buffer @@ -502,23 +436,15 @@ void QwGrCH1120::initBuffers(void) // graphics. This region is defined by the contents of the m_pageErase // descriptors. // -// Copy these to the page state, and call display -// void QwGrCH1120::resendGraphics(void) { - // Set the page state dirty bounds to the bounds of erase state - for (int i = 0; i < m_nPages; i++) - m_pageState[i] = m_pageErase[i]; - display(); // push bits to screen buffer } //////////////////////////////////////////////////////////////////////////////////// // Screen Control -// TODO: It might make everything cleaner if we instead enumerate the flips as separate commands... - //////////////////////////////////////////////////////////////////////////////////// // flip_vert() // @@ -663,39 +589,31 @@ void QwGrCH1120::displayPower(bool enable) // haven't been sent to the screen. // -void QwGrCH1120::erase(void) -{ - if (!m_pBuffer) - return; - - // Cleanup the dirty parts of each page in the graphics buffer. - for (uint8_t i = 0; i < m_nPages; i++) - { - // m_pageState - // The current "dirty" areas of the graphics [local] buffer. - // Areas that haven't been sent to the screen/device but are - // "dirty" - // - // Add the areas with pixels set and have been sent to the - // device - this is the contents of m_pageErase - - pageCheckBoundsDesc(m_pageState[i], m_pageErase[i]); - - // if this page is clean, there is nothing to update - if (pageIsClean(m_pageState[i])) - continue; - - // clear out memory that is dirty on this page - memset(m_pBuffer + i * m_viewport.width + m_pageState[i].xmin, 0, - m_pageState[i].xmax - m_pageState[i].xmin + 1); // add one b/c values are 0 based - - // clear out any pending dirty range for this page - it's erased - pageSetClean(m_pageState[i]); - } - - // Indicate that the data transfer to the device should include the erase - // region - m_pendingErase = true; +// This function sort of becomes useless because the entire page is rewritten each time now, so there isn't so much a concept of "erasing" +// void QwGrCH1120::erase(void) +// { +// if (!m_pBuffer) +// return; + +// // Cleanup the dirty parts of each page in the graphics buffer. +// for (uint8_t i = 0; i < m_nPages; i++) +// { +// // clear out memory that is dirty on this page +// memset(m_pBuffer + i * m_viewport.width + m_pageState[i].xmin, 0, +// m_pageState[i].xmax - m_pageState[i].xmin + 1); // add one b/c values are 0 based + +// // clear out any pending dirty range for this page - it's erased +// pageSetClean(m_pageState[i]); +// } + +// // Indicate that the data transfer to the device should include the erase +// // region +// m_pendingErase = true; +// } +//TODO: remove this if it's unused +void QwGrCH1120::erase(void) { + Serial.println("qwiic_grch1120.cpp erase(): REDUNDANT ERASE FUNCTION CALLED!"); + // if other layers assume that this will be used, this print will show us and we can handle that then... } //////////////////////////////////////////////////////////////////////////////////// @@ -817,9 +735,6 @@ void QwGrCH1120::drawLineVert(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, ui curROP(m_pBuffer + i * m_viewport.width + xinc, (clr ? setBits : 0), setBits); y0 += endBit - startBit + 1; // increment Y0 to next page - - pageCheckBoundsRange(m_pageState[i], x0, - x1); // mark dirty range in page desc } } @@ -949,8 +864,6 @@ void QwGrCH1120::drawBitmap(uint8_t x0, uint8_t y0, uint8_t dst_width, uint8_t d y0 += neededBits; bmp_y += neededBits; - pageCheckBoundsRange(m_pageState[iPage], x0, - x0 + dst_width); // mark dirty range in page desc } } @@ -961,25 +874,23 @@ void QwGrCH1120::drawBitmap(uint8_t x0, uint8_t y0, uint8_t dst_width, uint8_t d // // Sets the target screen buffer address for graphics buffer transfer to the // device. + +// The set page row command 0xB0 isn't working right! We are skipping every other column +// I've also tried this with the different 0x22 command for setting start and stop row and it still doesn't +// seem to work (either with the same (target) row specified twice or with the target row and the maximum row (0x9F) specified) +// SO: THIS COMMAND ONLY WORKS FOR EVEN NUMBERED ROWS ON THE 1.5" DISPLAY // -// The positon is specified by page and column -// -// The system runs in "page mode" - data is streamed along a page, based -// on the set starting position. -// -// This class takes advantage of this to just write the "dirty" ranges in a -// page. // -// Page can be 0 to 0x9F -// Column can be +// row can be 0 to 0x9F +// Column can be 0 to 0x9F -bool QwGrCH1120::setScreenBufferAddress(uint8_t page, uint8_t column) +bool QwGrCH1120::setScreenBufferAddress(uint8_t row, uint8_t column) { - if (page >= m_nPages || column >= m_viewport.width) + if (row >= m_viewport.height || column >= m_viewport.width) return false; - // send the page (row) address - sendDevCommand(kCmdStartRow, page); // difference from the 1306, bytes sent after each other instead of OR'd together... + // send the (row) address + sendDevCommand(kCmdStartRow, row); // difference from the 1306, bytes sent after each other instead of OR'd together... // For the column start address, add the viewport x offset. Some devices // (Micro OLED) don't start at column 0 in the screen buffer @@ -992,53 +903,54 @@ bool QwGrCH1120::setScreenBufferAddress(uint8_t page, uint8_t column) //////////////////////////////////////////////////////////////////////////////////// // display() // + +// OLD: // Send the "dirty" areas of the graphics buffer to the device's screen buffer. // Only send the areas that need to be updated. The update region is based on // new graphics to display, and any currently displayed items that need to be // erased. +// NEW: +// Send the ENTIRE graphics buffer to the device's screen buffer. Include things that are updated or not updated +// This is necessary because we cannot directly index to pixels since the row setting command is broken. +// In the future, if we ever get a row setting command that works, we can re-instate the fancy (only-update-dirty methodology) + void QwGrCH1120::display() { // Loop over our page descriptors - if a page is dirty, send the graphics // buffer dirty region to the device for the current page - pageState_t transferRange; - - for (int i = 0; i < m_nPages; i++) - { - // We keep the erase rect seperate from dirty rect. Make temp copy of - // dirty rect page range, expand to include erase rect page range. + // pageState_t transferRange; - transferRange = m_pageState[i]; + // for (int i = 0; i < m_nPages; i++) + // { + // // We keep the erase rect seperate from dirty rect. Make temp copy of + // // dirty rect page range, expand to include erase rect page range. - // If an erase has happend, we need to transfer/include erase update range - if (m_pendingErase) - pageCheckBoundsDesc(transferRange, m_pageErase[i]); + // // If an erase has happend, we need to transfer/include erase update range + // if (m_pendingErase) + // pageCheckBoundsDesc(transferRange, m_pageErase[i]); - if (pageIsClean(transferRange)) // both dirty and erase range for this - // page were null - continue; // next + // if (pageIsClean(transferRange)) // both dirty and erase range for this + // // page were null + // continue; // next - // set the start address to write the updated data to the devices screen - // buffer - setScreenBufferAddress(i, transferRange.xmin); + // // set the start address to write the updated data to the devices screen + // // buffer + // setScreenBufferAddress(i, transferRange.xmin); - // send the dirty data to the device - sendDevData(m_pBuffer + (i * m_viewport.width) + transferRange.xmin, // this page start + xmin - transferRange.xmax - transferRange.xmin + 1); // dirty region xmax - xmin. Add 1 b/c 0 based + // // send the dirty data to the device + // sendDevData(m_pBuffer + (i * m_viewport.width) + transferRange.xmin, // this page start + xmin + // transferRange.xmax - transferRange.xmin + 1); // dirty region xmax - xmin. Add 1 b/c 0 based - // If we sent the erase bounds, zero out the erase bounds - this area is now - // clear - if (m_pendingErase) - pageSetClean(m_pageErase[i]); + // // If we sent the erase bounds, zero out the erase bounds - this area is now + // // clear + // if (m_pendingErase) + // pageSetClean(m_pageErase[i]); - // add the just send dirty range (non erase rec) to the erase rect - pageCheckBoundsDesc(m_pageErase[i], m_pageState[i]); - - // this page is no longer dirty - mark it clean - pageSetClean(m_pageState[i]); - } - m_pendingErase = false; // no longer pending + // } + // m_pendingErase = false; // no longer pending + sendDevData(m_pBuffer, m_nPages * m_viewport.width); } diff --git a/src/qwiic_grch1120.h b/src/qwiic_grch1120.h index 2936be2..5f2aa4e 100644 --- a/src/qwiic_grch1120.h +++ b/src/qwiic_grch1120.h @@ -151,7 +151,7 @@ class QwGrCH1120 : public QwGrBufferDevice { private: // Internal buffer management methods - bool setScreenBufferAddress(uint8_t page, uint8_t column); + // bool setScreenBufferAddress(uint8_t page, uint8_t column); void initBuffers(void); // clear graphics and screen buffer void clearScreenBuffer(void); void resendGraphics(void); @@ -170,8 +170,6 @@ class QwGrCH1120 : public QwGrBufferDevice { // Buffer variables uint8_t *m_pBuffer; // Pointer to the graphics buffer uint8_t m_nPages; // number of pages for current device - pageState_t m_pageState[kMaxPageNumber]; // page state descriptors - pageState_t m_pageErase[kMaxPageNumber]; // keep track of erase boundaries bool m_pendingErase; // display variables diff --git a/src/qwiic_oled_1in5.h b/src/qwiic_oled_1in5.h index 254871f..10567a2 100644 --- a/src/qwiic_oled_1in5.h +++ b/src/qwiic_oled_1in5.h @@ -54,7 +54,7 @@ #define kOLED1in5YOffset 0 // Parameters for this device -#define kOLED1in5PinConfig 0x12 +#define kOLED1in5PinConfig 0x00 #define kOLED1in5PreCharge 0xF1 #define kOLED1in5VCOM 0x40 #define kOLED1in5Contrast 0xCF From a95b72c61a4605d2aa36ed91f962e562d3b4e3f5 Mon Sep 17 00:00:00 2001 From: Malcolm McKellips Date: Fri, 22 Aug 2025 10:52:52 -0600 Subject: [PATCH 4/5] basic full frame working (but horz and vert flips not) --- src/qwiic_grch1120.cpp | 208 ++++++++++++++++++++++++++++------------- src/qwiic_grch1120.h | 14 ++- 2 files changed, 157 insertions(+), 65 deletions(-) diff --git a/src/qwiic_grch1120.cpp b/src/qwiic_grch1120.cpp index c39ccc6..e9090f0 100644 --- a/src/qwiic_grch1120.cpp +++ b/src/qwiic_grch1120.cpp @@ -135,7 +135,8 @@ #define kDefaultRowEnd ((uint8_t)0x3F) // default #define kDefaultColEnd ((uint8_t)0x1F) // default #define kDefaultDisplayStart ((uint8_t)0x00) // default -#define kDefaultHorizontalAddressing ((uint8_t)0x00) // default +// #define kDefaultHorizontalAddressing ((uint8_t)0x00) // default +#define kDefaultHorizontalAddressing ((uint8_t)0x01) // default #define kDefaultContrast ((uint8_t)0xC8) // default = 0x80, upper end = 0xFF #define kDefaultRotateDisplayNinety ((uint8_t)0x01) // Default is 0 degrees #define kDefaultMultiplexRatio ((uint8_t)0x7F) // default is 0x9F @@ -148,6 +149,12 @@ #define kDefaultVCOMDeselect ((uint8_t)0x3F) // default #define kDefaultExternalIREF ((uint8_t)0x02) +// When we flip, we begin displaying data between our viewport and the maximum viewport of 160 +#define kPartialDisplayStartDefault ((uint8_t) 0) +#define kPartialDisplayEndDefault ((uint8_t) 127) +#define kPartialDisplayStartFlipped ((uint8_t) 32) +#define kPartialDisplayEndFlipped ((uint8_t) 159) + //////////////////////////////////////////////////////////////////////////////////// // Pixel write/set operations // @@ -327,15 +334,40 @@ void QwGrCH1120::setupOLEDDevice(bool clearDisplay){ if (clearDisplay) sendDevCommand(kCmdDisplayOff); + // TODO: dynamically calculate row start and row end and column start and column end based on passed in viewport + // we have to operate in "horizontally flipped" mode for text to appear correctly as it does in the rest of the library + // so, we will justify the columns we write out to the right of the screen memory so that when horizontally flipped, + // we don't get artifacts sendDevCommand(kCmdRowStartEnd, kDefaultRowStart, kDefaultRowEnd); + // sendDevCommand(kCmdRowStartEnd, 0x20, 0x9F); + // sendDevCommand(kCmdRowStartEnd, 0x10, 0x4F); sendDevCommand(kCmdColStartEnd, kDefaultColStart, kDefaultColEnd); + + // Note: do to either our rotation or addressing mode or both, below shifted in the axis we didn't want, so let's do it with rows like above... + //sendDevCommand(kCmdColStartEnd, 0x08, 0x27); // from columns 32 - 160 ( 32 / 4 = 8 and 160 / 4 - 1 = 39 = 0x27) + sendDevCommand(kCmdStartLine, kDefaultDisplayStart); + // sendDevCommand(kCmdStartLine, 5); + + // Instead of the startRow/endRow and startColumn/endColumn above, we will try here manually setting them at once with the + // "partial display" command + // uint8_t partialDisplayCmd[5] = { + // 0x2D, // partial display ON + // kPartialDisplayStartDefault, // Column Start (might have to change to follow weird paradigm above for all of these, for now: 0 - 128) + // kPartialDisplayEndDefault, // Column End + // kPartialDisplayStartDefault, // Row Start + // kPartialDisplayEndDefault, // Row End + // }; + + // sendDevCommand(partialDisplayCmd, 5); + sendDevCommand(kCmdContrastControl, m_initContrast); sendDevCommand(kCmdGrayMono, kDefaultMonoMode); sendDevCommand(kCmdHorizAddressing, kDefaultHorizontalAddressing); sendDevCommand(kCmdSegRemapDown); - sendDevCommand(kCmdComOutScan0First); - sendDevCommand(kCmdDisplayRotation, kDefaultRotateDisplayNinety); + sendDevCommand(kCmdComOutScan0Last); + // sendDevCommand(kCmdDisplayRotation, kDefaultRotateDisplayNinety); + sendDevCommand(kCmdDisplayRotation, 0x00); sendDevCommand(kCmdDisableEntireDisplay); sendDevCommand(kCmdNormalDisplay); sendDevCommand(kCmdMultiplexRatio, kDefaultMultiplexRatio); @@ -347,11 +379,15 @@ void QwGrCH1120::setupOLEDDevice(bool clearDisplay){ sendDevCommand(kCmdVCOMDeselectLevel, kDefaultVCOMDeselect); sendDevCommand(kCmdExternalIREF, kDefaultExternalIREF); - // TODO: In Eli's example, we also performed additional commands after the manufacturer setup: - // writeCommandParameter(0xB0, 0x00); - // writeCommand(0x00); - // writeCommand(0x10); - // Is this needed? + // // TODO: In Eli's example, we also performed additional commands after the manufacturer setup: + // // writeCommandParameter(0xB0, 0x00); + // // writeCommand(0x00); + // // writeCommand(0x10); + // sendDevCommand(0xB0, 0x00); + // sendDevCommand(0x00); + // sendDevCommand(0x10); + // // setScreenBufferAddress(0,0); + // // Is this needed? if (clearDisplay) // now, turn it back on @@ -399,9 +435,11 @@ void QwGrCH1120::clearScreenBuffer(void) // and we have m_viewport.height rows uint8_t emptyRow[m_nPages] = {0}; - setScreenBufferAddress(0, 0); // Warning: This function works-ish but only for even-numbered rows. + // setScreenBufferAddress(0, 0); // Warning: This function works-ish but only for even-numbered rows. // so we can use it here, but do not expect it to work in all instances + // setScreenBufferAddress(0, 32); + for (int i = 0; i < m_viewport.height; i++) { sendDevData((uint8_t *)emptyRow, m_nPages); // clear out row @@ -449,22 +487,57 @@ void QwGrCH1120::resendGraphics(void) // flip_vert() // // Flip the onscreen graphics vertically. -void QwGrCH1120::flipVert(bool bFlip) -{ - sendDevCommand(bFlip ? kCmdComOutScan0Last : kCmdComOutScan0First); +void QwGrCH1120::flipVert(bool bFlip){ + // If we are already formatted for the flipped display, just return + // if (rowStart == kPartialDisplayStartFlipped && rowEnd == kPartialDisplayEndFlipped) + // return; + sendDevCommand(bFlip ? kCmdSegRemapUp : kCmdSegRemapDown); + + // if (bFlip) { + // // Set the row start/end for flipped display + // sendDevCommand(kCmdRowStartEnd, kPartialDisplayStartFlipped, kPartialDisplayEndFlipped); + + // rowStart = kPartialDisplayStartFlipped; + // rowEnd = kPartialDisplayEndFlipped; + + // sendDevCommand(kCmdComOutScan0First); + // } + // else{ + // // Set the row start/end for normal (not flipped) display + // sendDevCommand(kCmdRowStartEnd, kPartialDisplayStartDefault, kPartialDisplayEndDefault); + + // rowStart = kPartialDisplayStartDefault; + // rowEnd = kPartialDisplayEndDefault; + + // sendDevCommand(kCmdComOutScan0Last); + // } + + // resendGraphics(); } //////////////////////////////////////////////////////////////////////////////////// // flip_horz() // -// Flip the onscreen graphcis horizontally. This requires a resend of the +// Flip the onscreen graphics horizontally. This requires a resend of the // graphics data to the device/screen buffer. -// -void QwGrCH1120::flipHorz(bool bFlip) -{ - sendDevCommand(bFlip ? kCmdSegRemapUp : kCmdSegRemapDown); - clearScreenBuffer(); - resendGraphics(); +void QwGrCH1120::flipHorz(bool bFlip){ + + sendDevCommand(bFlip ? kCmdComOutScan0First : kCmdComOutScan0Last); + // // follow a similar process to the flipVert above (but with columns being set to default or flipped instead of rows) + // if (bFlip) { + // // Set the column start/end for flipped display + // sendDevCommand(kCmdColStartEnd, kPartialDisplayStartFlipped, kPartialDisplayEndFlipped); + + // sendDevCommand(kCmdSegRemapUp); + // } + // else { + // // Set the column start/end for normal (not flipped) display + // sendDevCommand(kCmdColStartEnd, kPartialDisplayStartDefault, kPartialDisplayEndDefault); + + // sendDevCommand(kCmdSegRemapDown); + // } + + // resendGraphics(); } //////////////////////////////////////////////////////////////////////////////////// @@ -612,8 +685,10 @@ void QwGrCH1120::displayPower(bool enable) // } //TODO: remove this if it's unused void QwGrCH1120::erase(void) { - Serial.println("qwiic_grch1120.cpp erase(): REDUNDANT ERASE FUNCTION CALLED!"); - // if other layers assume that this will be used, this print will show us and we can handle that then... + // memset the entire buffer to 0 + memset(m_pBuffer, 0, m_nPages * m_viewport.width); + + m_pendingErase = true; } //////////////////////////////////////////////////////////////////////////////////// @@ -626,6 +701,7 @@ void QwGrCH1120::erase(void) { void QwGrCH1120::drawPixel(uint8_t x, uint8_t y, uint8_t clr) { + //Serial.println("Calling drawPixel with x: " + String(x) + ", y: " + String(y) + ", color: " + String(clr)); // quick sanity check on range if (x >= m_viewport.width || y >= m_viewport.height) return; // out of bounds @@ -634,9 +710,10 @@ void QwGrCH1120::drawPixel(uint8_t x, uint8_t y, uint8_t clr) m_rasterOps[m_rop](m_pBuffer + x + y / kByteNBits * m_viewport.width, // pixel offset (clr ? bit : 0), bit); // which bit to set in byte - - pageCheckBounds(m_pageState[y / kByteNBits], - x); // update dirty range for page + + // print Buffer after drawing pixel: + // printBuffer(); + //rawPrintBuffer(); } //////////////////////////////////////////////////////////////////////////////////// @@ -648,6 +725,7 @@ void QwGrCH1120::drawLineHorz(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, ui { // Basically we set a bit within a range in a page of our graphics buffer. + Serial.println("Calling Draw Line Horz with x0: " + String(x0) + ", y0: " + String(y0) + ", x1: " + String(x1) + ", y1: " + String(y1)); // in range if (y0 >= m_viewport.height) return; @@ -667,9 +745,6 @@ void QwGrCH1120::drawLineHorz(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, ui // walk up x and set the target pixel using the pixel operator function for (int i = x0; i <= x1; i++, pBuffer++) curROP(pBuffer, (clr ? bit : 0), bit); - - // Mark the page dirty for the range drawn - pageCheckBoundsRange(m_pageState[y0 / kByteNBits], x0, x1); } //////////////////////////////////////////////////////////////////////////////////// @@ -886,6 +961,7 @@ void QwGrCH1120::drawBitmap(uint8_t x0, uint8_t y0, uint8_t dst_width, uint8_t d bool QwGrCH1120::setScreenBufferAddress(uint8_t row, uint8_t column) { + Serial.println("Calling setScreenBufferAddress with row: " + String(row) + ", column: " + String(column)); if (row >= m_viewport.height || column >= m_viewport.width) return false; @@ -900,6 +976,34 @@ bool QwGrCH1120::setScreenBufferAddress(uint8_t row, uint8_t column) return true; } +bool QwGrCH1120::getPixel(uint8_t x, uint8_t y) { + if (x >= m_viewport.width || y >= m_viewport.height) + return 0; + + return (m_pBuffer[( y * (m_viewport.width / kByteNBits)) + (x / kByteNBits)] >> (x % kByteNBits)) & 0x01; +} + +// Print the buffer as a 2D array of bits +void QwGrCH1120::printBuffer() { + for (uint8_t y = 0; y < m_viewport.height; y++) { + for (uint8_t x = 0; x < m_viewport.width; x++) { + Serial.print(QwGrCH1120::getPixel(x, y) ? "1" : "0"); + } + Serial.println(); + } +} + +void QwGrCH1120::rawPrintBuffer() { + for (uint8_t y = 0; y < m_viewport.height; y++) { + for (uint8_t x = 0; x < m_viewport.width / kByteNBits; x++) { + // Serial.print(m_pBuffer[x + y * (m_viewport.width / kByteNBits)], HEX); + uint8_t val = m_pBuffer[x + y * (m_viewport.width / kByteNBits)]; + Serial.printf(" %02X", val); + } + Serial.println(); + } +} + //////////////////////////////////////////////////////////////////////////////////// // display() // @@ -917,40 +1021,12 @@ bool QwGrCH1120::setScreenBufferAddress(uint8_t row, uint8_t column) void QwGrCH1120::display() { - // Loop over our page descriptors - if a page is dirty, send the graphics - // buffer dirty region to the device for the current page - - // pageState_t transferRange; - - // for (int i = 0; i < m_nPages; i++) - // { - // // We keep the erase rect seperate from dirty rect. Make temp copy of - // // dirty rect page range, expand to include erase rect page range. - - // // If an erase has happend, we need to transfer/include erase update range - // if (m_pendingErase) - // pageCheckBoundsDesc(transferRange, m_pageErase[i]); - - // if (pageIsClean(transferRange)) // both dirty and erase range for this - // // page were null - // continue; // next - - // // set the start address to write the updated data to the devices screen - // // buffer - // setScreenBufferAddress(i, transferRange.xmin); - - // // send the dirty data to the device - // sendDevData(m_pBuffer + (i * m_viewport.width) + transferRange.xmin, // this page start + xmin - // transferRange.xmax - transferRange.xmin + 1); // dirty region xmax - xmin. Add 1 b/c 0 based - - // // If we sent the erase bounds, zero out the erase bounds - this area is now - // // clear - // if (m_pendingErase) - // pageSetClean(m_pageErase[i]); - - // } - // m_pendingErase = false; // no longer pending - sendDevData(m_pBuffer, m_nPages * m_viewport.width); + // Serial.println("Display Called with buffers: "); + // printBuffer(); + // Serial.println("Raw Hex:"); + // rawPrintBuffer(); + // setScreenBufferAddress(0, 32); + sendDevData(m_pBuffer, m_nPages * m_viewport.height); // send the entire buffer to the device } @@ -1008,10 +1084,16 @@ void QwGrCH1120::sendDevCommand(uint8_t command, uint8_t start, uint8_t stop) // sendDeviceData() // // send a block of data to the RAM of the device via the current bus object -void QwGrCH1120::sendDevData(uint8_t *pData, uint8_t nData) +void QwGrCH1120::sendDevData(uint8_t *pData, uint16_t nData) { - if (!pData || nData == 0) + if (!pData || nData == 0){ + Serial.println("Invalid data"); + Serial.print("pData: "); + Serial.println((uintptr_t)pData, HEX); + Serial.print("nData: "); + Serial.println(nData); return; + } m_i2cBus->writeRegisterRegion(m_i2cAddress, kCmdRamControlByte, pData, nData); } \ No newline at end of file diff --git a/src/qwiic_grch1120.h b/src/qwiic_grch1120.h index 5f2aa4e..b1f01b1 100644 --- a/src/qwiic_grch1120.h +++ b/src/qwiic_grch1120.h @@ -151,17 +151,22 @@ class QwGrCH1120 : public QwGrBufferDevice { private: // Internal buffer management methods - // bool setScreenBufferAddress(uint8_t page, uint8_t column); + bool setScreenBufferAddress(uint8_t page, uint8_t column); void initBuffers(void); // clear graphics and screen buffer void clearScreenBuffer(void); void resendGraphics(void); void setupOLEDDevice(bool clearDisplay = true); + // Added debug functions + bool getPixel(uint8_t x, uint8_t y); // Gets the pixel value at (x, y) + void printBuffer(void); // Prints all of the bits in the buffer as a 2D pixel array over serial + void rawPrintBuffer(void); // Prints all of the bits in the buffer directly + // device communication methods void sendDevCommand(uint8_t command); void sendDevCommand(uint8_t command, uint8_t value); void sendDevCommand(uint8_t *commands, uint8_t n); - void sendDevData(uint8_t *pData, uint8_t nData); + void sendDevData(uint8_t *pData, uint16_t nData); void sendDevCommand(uint8_t command, uint8_t start, uint8_t stop); ///////////////////////////////////////////////////////////////////////////// @@ -187,4 +192,9 @@ class QwGrCH1120 : public QwGrBufferDevice { uint8_t m_initContrast; bool m_isInitialized; // general init flag + + uint8_t colStart = 0; + uint8_t colEnd = 127; + uint8_t rowStart = 0; + uint8_t rowEnd = 127; }; From df010619d64d0e0417092b09ca8d26563e938cbe Mon Sep 17 00:00:00 2001 From: Malcolm McKellips Date: Tue, 26 Aug 2025 10:26:19 -0600 Subject: [PATCH 5/5] Fixes for horizontal flips. This acheives MVP for the 1in5 display. All examples seem to be working well. --- .../Example-01_Hello/Example-01_Hello.ino | 3 +- .../Example-02_Shapes/Example-02_Shapes.ino | 2 + .../Example-03_Bitmap/Example-03_Bitmap.ino | 2 + examples/Example-04_Text/Example-04_Text.ino | 2 + .../Example-05_ScrollFlip.ino | 19 +- .../Example-06_Clock/Example-06_Clock.ino | 2 + examples/Example-07_Cube/Example-07_Cube.ino | 2 + .../Example-08_Multi/Example-08_Multi.ino | 2 + src/SparkFun_Qwiic_OLED.h | 16 +- src/qwiic_grch1120.cpp | 166 +++++++++--------- src/qwiic_grch1120.h | 20 +-- src/qwiic_grcommon.h | 37 +++- src/qwiic_grssd1306.h | 40 ----- 13 files changed, 155 insertions(+), 158 deletions(-) diff --git a/examples/Example-01_Hello/Example-01_Hello.ino b/examples/Example-01_Hello/Example-01_Hello.ino index e130d21..04619ca 100644 --- a/examples/Example-01_Hello/Example-01_Hello.ino +++ b/examples/Example-01_Hello/Example-01_Hello.ino @@ -9,6 +9,7 @@ Transparent OLED https://www.sparkfun.com/products/15173 "Narrow" OLED https://www.sparkfun.com/products/24606 Qwiic OLED 1.3in https://www.sparkfun.com/products/23453 + Qwiic OLED 1.5in https://www.sparkfun.com/products/29530 Written by Kirk Benell @ SparkFun Electronics, March 2022 @@ -30,7 +31,7 @@ QwiicMicroOLED myOLED; //QwiicTransparentOLED myOLED; //QwiicNarrowOLED myOLED; //Qwiic1in3OLED myOLED; - +//Qwiic1in5OLED myOLED; void setup() { diff --git a/examples/Example-02_Shapes/Example-02_Shapes.ino b/examples/Example-02_Shapes/Example-02_Shapes.ino index ca4d2ee..0907234 100644 --- a/examples/Example-02_Shapes/Example-02_Shapes.ino +++ b/examples/Example-02_Shapes/Example-02_Shapes.ino @@ -14,6 +14,7 @@ Transparent OLED https://www.sparkfun.com/products/15173 "Narrow" OLED https://www.sparkfun.com/products/24606 Qwiic OLED 1.3in https://www.sparkfun.com/products/23453 + Qwiic OLED 1.5in https://www.sparkfun.com/products/29530 Written by Kirk Benell @ SparkFun Electronics, March 2022 @@ -35,6 +36,7 @@ QwiicMicroOLED myOLED; //QwiicTransparentOLED myOLED; //QwiicNarrowOLED myOLED; //Qwiic1in3OLED myOLED; +//Qwiic1in5OLED myOLED; // Global variables - used to stash our screen size diff --git a/examples/Example-03_Bitmap/Example-03_Bitmap.ino b/examples/Example-03_Bitmap/Example-03_Bitmap.ino index 511f08b..2c07a21 100644 --- a/examples/Example-03_Bitmap/Example-03_Bitmap.ino +++ b/examples/Example-03_Bitmap/Example-03_Bitmap.ino @@ -14,6 +14,7 @@ Transparent OLED https://www.sparkfun.com/products/15173 "Narrow" OLED https://www.sparkfun.com/products/24606 Qwiic OLED 1.3in https://www.sparkfun.com/products/23453 + Qwiic OLED 1.5in https://www.sparkfun.com/products/29530 Written by Kirk Benell @ SparkFun Electronics, March 2022 @@ -35,6 +36,7 @@ QwiicMicroOLED myOLED; //QwiicTransparentOLED myOLED; //QwiicNarrowOLED myOLED; //Qwiic1in3OLED myOLED; +//Qwiic1in5OLED myOLED; // Let's draw a truck - use our built in bitmap #include "res/qw_bmp_truck.h" diff --git a/examples/Example-04_Text/Example-04_Text.ino b/examples/Example-04_Text/Example-04_Text.ino index 7ea723d..66f2e7a 100644 --- a/examples/Example-04_Text/Example-04_Text.ino +++ b/examples/Example-04_Text/Example-04_Text.ino @@ -14,6 +14,7 @@ Transparent OLED https://www.sparkfun.com/products/15173 "Narrow" OLED https://www.sparkfun.com/products/24606 Qwiic OLED 1.3in https://www.sparkfun.com/products/23453 + Qwiic OLED 1.5in https://www.sparkfun.com/products/29530 Written by Kirk Benell @ SparkFun Electronics, March 2022 @@ -35,6 +36,7 @@ QwiicMicroOLED myOLED; //QwiicTransparentOLED myOLED; //QwiicNarrowOLED myOLED; //Qwiic1in3OLED myOLED; +//Qwiic1in5OLED myOLED; // Fonts #include diff --git a/examples/Example-05_ScrollFlip/Example-05_ScrollFlip.ino b/examples/Example-05_ScrollFlip/Example-05_ScrollFlip.ino index 32135a4..f913d50 100644 --- a/examples/Example-05_ScrollFlip/Example-05_ScrollFlip.ino +++ b/examples/Example-05_ScrollFlip/Example-05_ScrollFlip.ino @@ -16,6 +16,7 @@ Transparent OLED https://www.sparkfun.com/products/15173 "Narrow" OLED https://www.sparkfun.com/products/24606 Qwiic OLED 1.3in https://www.sparkfun.com/products/23453 + Qwiic OLED 1.5in https://www.sparkfun.com/products/29530 Written by Kirk Benell @ SparkFun Electronics, March 2022 @@ -37,6 +38,12 @@ QwiicMicroOLED myOLED; //QwiicTransparentOLED myOLED; //QwiicNarrowOLED myOLED; //Qwiic1in3OLED myOLED; +//Qwiic1in5OLED myOLED; + +// Get the end page based on the type of myOLED. If it is 1.5" OLED with 128 rows (15 pages), +// the end page is 15 otherwise we'll pass 7 (for displays with only 64 rows) +int endPage = 7; +// int endPage = 15; // Only use for 1.5" OLED int yoffset; @@ -44,25 +51,25 @@ int yoffset; void scrollRight(void) { myOLED.scrollStop(); - myOLED.scrollRight(0, 7, SCROLL_INTERVAL_2_FRAMES); + myOLED.scrollRight(0, endPage, SCROLL_INTERVAL_2_FRAMES); } void scrollRightVertical(void) { myOLED.scrollStop(); - myOLED.scrollVertRight(0, 7, SCROLL_INTERVAL_3_FRAMES); + myOLED.scrollVertRight(0, endPage, SCROLL_INTERVAL_3_FRAMES); } void scrollLeft(void) { myOLED.scrollStop(); - myOLED.scrollLeft(0, 7, SCROLL_INTERVAL_4_FRAMES); + myOLED.scrollLeft(0, endPage, SCROLL_INTERVAL_4_FRAMES); } void scrollLeftVertical(void) { myOLED.scrollStop(); - myOLED.scrollVertLeft(0, 7, SCROLL_INTERVAL_5_FRAMES); + myOLED.scrollVertLeft(0, endPage, SCROLL_INTERVAL_5_FRAMES); } void scrollStop(void) @@ -107,9 +114,9 @@ typedef struct _testRoutines static const testRoutine testFunctions[] = { {scrollRight, "Right>"}, - {scrollRightVertical, "^Right-Up>"}, + {scrollRightVertical, "^Vertical-Mode1>"}, // Right-Up for most displays, Up for 1.5" display {scrollLeft, ""}, {flipHorizontal, "-Flip-Horz-"}, {flipVertical, "|Flip-Vert|"}, diff --git a/examples/Example-06_Clock/Example-06_Clock.ino b/examples/Example-06_Clock/Example-06_Clock.ino index 843f9ea..2e5d910 100644 --- a/examples/Example-06_Clock/Example-06_Clock.ino +++ b/examples/Example-06_Clock/Example-06_Clock.ino @@ -14,6 +14,7 @@ Transparent OLED https://www.sparkfun.com/products/15173 "Narrow" OLED https://www.sparkfun.com/products/24606 Qwiic OLED 1.3in https://www.sparkfun.com/products/23453 + Qwiic OLED 1.5in https://www.sparkfun.com/products/29530 Written by Jim Lindblom @ SparkFun Electronics @@ -37,6 +38,7 @@ QwiicMicroOLED myOLED; //QwiicTransparentOLED myOLED; //QwiicNarrowOLED myOLED; //Qwiic1in3OLED myOLED; +//Qwiic1in5OLED myOLED; // Use these variables to set the initial time int hours = 11; diff --git a/examples/Example-07_Cube/Example-07_Cube.ino b/examples/Example-07_Cube/Example-07_Cube.ino index 4c8771e..62fbf91 100644 --- a/examples/Example-07_Cube/Example-07_Cube.ino +++ b/examples/Example-07_Cube/Example-07_Cube.ino @@ -14,6 +14,7 @@ Transparent OLED https://www.sparkfun.com/products/15173 "Narrow" OLED https://www.sparkfun.com/products/24606 Qwiic OLED 1.3in https://www.sparkfun.com/products/23453 + Qwiic OLED 1.5in https://www.sparkfun.com/products/29530 Written by Jim Lindblom @ SparkFun Electronics @@ -36,6 +37,7 @@ QwiicMicroOLED myOLED; //QwiicTransparentOLED myOLED; //QwiicNarrowOLED myOLED; //Qwiic1in3OLED myOLED; +//Qwiic1in5OLED myOLED; int width; int height; diff --git a/examples/Example-08_Multi/Example-08_Multi.ino b/examples/Example-08_Multi/Example-08_Multi.ino index 841af06..88de2c0 100644 --- a/examples/Example-08_Multi/Example-08_Multi.ino +++ b/examples/Example-08_Multi/Example-08_Multi.ino @@ -14,6 +14,7 @@ Transparent OLED https://www.sparkfun.com/products/15173 "Narrow" OLED https://www.sparkfun.com/products/24606 Qwiic OLED 1.3in https://www.sparkfun.com/products/23453 + Qwiic OLED 1.5in https://www.sparkfun.com/products/29530 Updated from example writtin by Paul Clark @ SparkFun Electronics Original Creation Date: December 11th, 2020 @@ -36,6 +37,7 @@ QwiicMicroOLED myOLED; //QwiicTransparentOLED myOLED; //QwiicNarrowOLED myOLED; //Qwiic1in3OLED myOLED; +//Qwiic1in5OLED myOLED; int width; int height; diff --git a/src/SparkFun_Qwiic_OLED.h b/src/SparkFun_Qwiic_OLED.h index 6169c84..e75263a 100644 --- a/src/SparkFun_Qwiic_OLED.h +++ b/src/SparkFun_Qwiic_OLED.h @@ -276,8 +276,8 @@ template class QwiicOLEDBaseClass : public Print // NOTE: // // Parameter Description // --------- ----------------------------- - // start The start page address of the scroll - valid values are 0 thru 7 - // stop The stop/end page address of the scroll - valid values are 0 thru 7 + // start The start page address of the scroll - valid values are 0 thru 15 (only 0 thru 7 valid for most small displays w/ SSD1306) + // stop The stop/end page address of the scroll - valid values are 0 thru 15 (only 0 thru 7 valid for most small displays w/ SSD1306) // interval The time interval between scroll step - values listed below // // Defined values for the interval parameter: @@ -308,8 +308,8 @@ template class QwiicOLEDBaseClass : public Print // NOTE: // // Parameter Description // --------- ----------------------------- - // start The start page address of the scroll - valid values are 0 thru 7 - // stop The stop/end page address of the scroll - valid values are 0 thru 7 + // start The start page address of the scroll - valid values are 0 thru 15 (only 0 thru 7 valid for most small displays w/ SSD1306) + // stop The stop/end page address of the scroll - valid values are 0 thru 15 (only 0 thru 7 valid for most small displays w/ SSD1306) // interval The time interval between scroll step - values listed in scrollRight() void scrollVertRight(uint8_t start, uint8_t stop, uint8_t interval) @@ -327,8 +327,8 @@ template class QwiicOLEDBaseClass : public Print // NOTE: // // Parameter Description // --------- ----------------------------- - // start The start page address of the scroll - valid values are 0 thru 7 - // stop The stop/end page address of the scroll - valid values are 0 thru 7 + // start The start page address of the scroll - valid values are 0 thru 15 (only 0 thru 7 valid for most small displays w/ SSD1306) + // stop The stop/end page address of the scroll - valid values are 0 thru 15 (only 0 thru 7 valid for most small displays w/ SSD1306) // interval The time interval between scroll step - values listed in scrollRight() void scrollLeft(uint8_t start, uint8_t stop, uint8_t interval) @@ -346,8 +346,8 @@ template class QwiicOLEDBaseClass : public Print // NOTE: // // Parameter Description // --------- ----------------------------- - // start The start page address of the scroll - valid values are 0 thru 7 - // stop The stop/end page address of the scroll - valid values are 0 thru 7 + // start The start page address of the scroll - valid values are 0 thru 15 (only 0 thru 7 valid for most small displays w/ SSD1306) + // stop The stop/end page address of the scroll - valid values are 0 thru 15 (only 0 thru 7 valid for most small displays w/ SSD1306) // interval The time interval between scroll step - values listed in scrollRight() void scrollVertLeft(uint8_t start, uint8_t stop, uint8_t interval) diff --git a/src/qwiic_grch1120.cpp b/src/qwiic_grch1120.cpp index e9090f0..797d4d7 100644 --- a/src/qwiic_grch1120.cpp +++ b/src/qwiic_grch1120.cpp @@ -54,6 +54,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "qwiic_grch1120.h" +#include ///////////////////////////////////////////////////////////////////////////// // Class that implements graphics support for devices that use the CH1120 @@ -138,7 +139,7 @@ // #define kDefaultHorizontalAddressing ((uint8_t)0x00) // default #define kDefaultHorizontalAddressing ((uint8_t)0x01) // default #define kDefaultContrast ((uint8_t)0xC8) // default = 0x80, upper end = 0xFF -#define kDefaultRotateDisplayNinety ((uint8_t)0x01) // Default is 0 degrees +#define kDefaultRotateDisplayNinety ((uint8_t)0x01) // Default is 0 degrees (NOTE: start and end values for row/column need adjusting if rotate=0) #define kDefaultMultiplexRatio ((uint8_t)0x7F) // default is 0x9F #define kDefaultDisplayOffset ((uint8_t)0x10) // default is 0x00 #define kDefaultDivideRatio ((uint8_t)0x00) // default @@ -149,12 +150,6 @@ #define kDefaultVCOMDeselect ((uint8_t)0x3F) // default #define kDefaultExternalIREF ((uint8_t)0x02) -// When we flip, we begin displaying data between our viewport and the maximum viewport of 160 -#define kPartialDisplayStartDefault ((uint8_t) 0) -#define kPartialDisplayEndDefault ((uint8_t) 127) -#define kPartialDisplayStartFlipped ((uint8_t) 32) -#define kPartialDisplayEndFlipped ((uint8_t) 159) - //////////////////////////////////////////////////////////////////////////////////// // Pixel write/set operations // @@ -188,6 +183,26 @@ static const rasterOPsFn m_rasterOps[] = { // Always White [](uint8_t *dst, uint8_t src, uint8_t mask) -> void { *dst = mask | *dst; }}; + +///////////////////////////////////////////////////////////////////////////// +// Map for scrolling. See qwiic_grcommon.h for original definitions and descriptions +///////////////////////////////////////////////////////////////////////////// + +const std::map scrollIntervals = { + {SCROLL_INTERVAL_6_FRAMES, 0x00}, + {SCROLL_INTERVAL_32_FRAMES, 0x01}, + {SCROLL_INTERVAL_64_FRAMES, 0x02}, + {SCROLL_INTERVAL_128_FRAMES, 0x03}, + {SCROLL_INTERVAL_3_FRAMES, 0x04}, + {SCROLL_INTERVAL_4_FRAMES, 0x05}, + {SCROLL_INTERVAL_5_FRAMES, 0x06}, + {SCROLL_INTERVAL_2_FRAMES, 0x07}, + // The following intervals are not directly available for the CH1120 + // so we will include their closest mappings for compatiblity with other driver/examples + {SCROLL_INTERVAL_25_FRAMES, 0x01}, // closest to 25 frames is 32 frames (0x01) + {SCROLL_INTERVAL_256_FRAMES, 0x03} // closest to 256 frames is 128 frames (0x03) +}; + //////////////////////////////////////////////////////////////////////////////////// // setup defaults - called from constructors // @@ -347,51 +362,44 @@ void QwGrCH1120::setupOLEDDevice(bool clearDisplay){ //sendDevCommand(kCmdColStartEnd, 0x08, 0x27); // from columns 32 - 160 ( 32 / 4 = 8 and 160 / 4 - 1 = 39 = 0x27) sendDevCommand(kCmdStartLine, kDefaultDisplayStart); + // sendDevCommand(kCmdStartLine, 30); // sendDevCommand(kCmdStartLine, 5); - // Instead of the startRow/endRow and startColumn/endColumn above, we will try here manually setting them at once with the - // "partial display" command - // uint8_t partialDisplayCmd[5] = { - // 0x2D, // partial display ON - // kPartialDisplayStartDefault, // Column Start (might have to change to follow weird paradigm above for all of these, for now: 0 - 128) - // kPartialDisplayEndDefault, // Column End - // kPartialDisplayStartDefault, // Row Start - // kPartialDisplayEndDefault, // Row End - // }; - - // sendDevCommand(partialDisplayCmd, 5); - + // ELI HAD THIS AS 0x0F, 200... sendDevCommand(kCmdContrastControl, m_initContrast); sendDevCommand(kCmdGrayMono, kDefaultMonoMode); sendDevCommand(kCmdHorizAddressing, kDefaultHorizontalAddressing); sendDevCommand(kCmdSegRemapDown); - sendDevCommand(kCmdComOutScan0Last); - // sendDevCommand(kCmdDisplayRotation, kDefaultRotateDisplayNinety); - sendDevCommand(kCmdDisplayRotation, 0x00); + // sendDevCommand(kCmdComOutScan0Last); + sendDevCommand(kCmdComOutScan0First); + sendDevCommand(kCmdDisplayRotation, kDefaultRotateDisplayNinety); + // sendDevCommand(kCmdDisplayRotation, 0x00); sendDevCommand(kCmdDisableEntireDisplay); - sendDevCommand(kCmdNormalDisplay); - sendDevCommand(kCmdMultiplexRatio, kDefaultMultiplexRatio); + + // sendDevCommand(kCmdNormalDisplay); + // sendDevCommand(kCmdMultiplexRatio, kDefaultMultiplexRatio); sendDevCommand(kCmdDisplayOffset, kDefaultDisplayOffset); + // sendDevCommand(kCmdDisplayOffset, 0); sendDevCommand(kCmdDischargeFront, kDefaultDischargeFront); sendDevCommand(kCmdDischargeBack, kDefaultDischargeBack); sendDevCommand(kCmdPreCharge, m_initPreCharge); sendDevCommand(kCmdSEGpads, kDefaultSegPads); sendDevCommand(kCmdVCOMDeselectLevel, kDefaultVCOMDeselect); sendDevCommand(kCmdExternalIREF, kDefaultExternalIREF); - + + if (clearDisplay) + // now, turn it back on + sendDevCommand(kCmdDisplayOn); + // // TODO: In Eli's example, we also performed additional commands after the manufacturer setup: // // writeCommandParameter(0xB0, 0x00); // // writeCommand(0x00); // // writeCommand(0x10); - // sendDevCommand(0xB0, 0x00); - // sendDevCommand(0x00); - // sendDevCommand(0x10); + sendDevCommand(0xB0, 0x00); + sendDevCommand(0x00); + sendDevCommand(0x10); // // setScreenBufferAddress(0,0); // // Is this needed? - - if (clearDisplay) - // now, turn it back on - sendDevCommand(kCmdDisplayOn); } //////////////////////////////////////////////////////////////////////////////////// @@ -477,6 +485,7 @@ void QwGrCH1120::initBuffers(void) void QwGrCH1120::resendGraphics(void) { + // erase(); display(); // push bits to screen buffer } @@ -489,30 +498,10 @@ void QwGrCH1120::resendGraphics(void) // Flip the onscreen graphics vertically. void QwGrCH1120::flipVert(bool bFlip){ // If we are already formatted for the flipped display, just return - // if (rowStart == kPartialDisplayStartFlipped && rowEnd == kPartialDisplayEndFlipped) - // return; - sendDevCommand(bFlip ? kCmdSegRemapUp : kCmdSegRemapDown); - - // if (bFlip) { - // // Set the row start/end for flipped display - // sendDevCommand(kCmdRowStartEnd, kPartialDisplayStartFlipped, kPartialDisplayEndFlipped); - - // rowStart = kPartialDisplayStartFlipped; - // rowEnd = kPartialDisplayEndFlipped; - - // sendDevCommand(kCmdComOutScan0First); - // } - // else{ - // // Set the row start/end for normal (not flipped) display - // sendDevCommand(kCmdRowStartEnd, kPartialDisplayStartDefault, kPartialDisplayEndDefault); - - // rowStart = kPartialDisplayStartDefault; - // rowEnd = kPartialDisplayEndDefault; - - // sendDevCommand(kCmdComOutScan0Last); - // } + // sendDevCommand(bFlip ? kCmdSegRemapUp : kCmdSegRemapDown); + sendDevCommand(bFlip ? kCmdComOutScan0Last : kCmdComOutScan0First); - // resendGraphics(); + resendGraphics(); } //////////////////////////////////////////////////////////////////////////////////// @@ -522,22 +511,28 @@ void QwGrCH1120::flipVert(bool bFlip){ // graphics data to the device/screen buffer. void QwGrCH1120::flipHorz(bool bFlip){ - sendDevCommand(bFlip ? kCmdComOutScan0First : kCmdComOutScan0Last); - // // follow a similar process to the flipVert above (but with columns being set to default or flipped instead of rows) - // if (bFlip) { - // // Set the column start/end for flipped display - // sendDevCommand(kCmdColStartEnd, kPartialDisplayStartFlipped, kPartialDisplayEndFlipped); + // sendDevCommand(kCmdDisplayOff); // TODO: verify is this necessary? + + if (bFlip){ + // If we are flipping to horizontal, we need to adjust row start and end to the end of the display memory + // This is because when we horizontally flip, if our viewport is smaller than the total area, we will flip in some garbage + // When flipping we need to offset by the max width minus the viewport width (or height, I've kind of lost the plot on which is which with this square display in 90 degree rotation mode) + uint8_t offset = kMaxCH1120Width - m_viewport.width; // TODO: Should these really be widths or heights? + sendDevCommand(kCmdRowStartEnd, kDefaultRowStart + offset, kDefaultRowEnd + offset); - // sendDevCommand(kCmdSegRemapUp); - // } - // else { - // // Set the column start/end for normal (not flipped) display - // sendDevCommand(kCmdColStartEnd, kPartialDisplayStartDefault, kPartialDisplayEndDefault); + sendDevCommand(kCmdSegRemapUp); + } - // sendDevCommand(kCmdSegRemapDown); - // } + else{ + // If in normal mode, just set to the defaults + sendDevCommand(kCmdRowStartEnd, kDefaultRowStart, kDefaultRowEnd); - // resendGraphics(); + sendDevCommand(kCmdSegRemapDown); + } + + // sendDevCommand(kCmdDisplayOn); // TODO: verify is this necessary? + + resendGraphics(); } //////////////////////////////////////////////////////////////////////////////////// @@ -571,7 +566,7 @@ void QwGrCH1120::stopScroll(void) //////////////////////////////////////////////////////////////////////////////////// // scroll() // -// Set scroll parametes on the device and start scrolling +// Set scroll parameters on the device and start scrolling // void QwGrCH1120::scroll(uint16_t scroll_type, uint8_t start, uint8_t stop, uint8_t interval) { @@ -592,38 +587,41 @@ void QwGrCH1120::scroll(uint16_t scroll_type, uint8_t start, uint8_t stop, uint8 // 5) Time Interval Set (X + B0) where B0 is 0b000 (6frames) to 0b111 (2 frames) (with several options up to 128 frames in between) uint8_t commands[n_commands] = {kCmdRightHorizontalScroll, // default scroll right - 0x00, // default 0x00 start column - 0x80, // let's default to 128 for the 128x128 display - 0x00, // default 0x00 start row - 0x80, // let's default to 128 for the 128x128 display - 0x00 // default of 6 frames + 0x00, // default 0x00 start column + 0x7F, // let's default to 128 for the 128x128 display + 0x00, // default 0x00 start row + 0x7F, // let's default to 128 for the 128x128 display + scrollIntervals.at(interval) // map passed interval to correct value }; // Which way to scroll + // Note how we multiply the start/stop values passed in by 8 such that it more closely aligns with + // the behavior of the other SSD1306 driver (which had 64 rows as 8 pages), and example code can be similar between them + // We add 7 to the stop value so it is the last row of the page switch (scroll_type) { case SCROLL_LEFT: commands[0] = kCmdLeftHorizontalScroll; - commands[1] = start; - commands[2] = stop; + commands[1] = start * 8; + commands[2] = stop * 8 + 7; break; - case SCROLL_UP: + case SCROLL_VERT_MODE1: commands[0] = kCmdUpVerticalScroll; - commands[3] = start; - commands[4] = stop; + commands[3] = start * 8; + commands[4] = stop * 8 + 7; break; - case SCROLL_DOWN: + case SCROLL_VERT_MODE2: commands[0] = kCmdDownVerticalScroll; - commands[3] = start; - commands[4] = stop; + commands[3] = start * 8; + commands[4] = stop * 8 + 7; break; case SCROLL_RIGHT: default: // Note that the 1306 (and thus the higher level driver) support scroll right vert etc. which we don't. We'll stick w/ horizontal right in this case commands[0] = kCmdRightHorizontalScroll; - commands[1] = start; - commands[2] = stop; + commands[1] = start * 8; + commands[2] = stop * 8 + 7; } // send the scroll commands to the device diff --git a/src/qwiic_grch1120.h b/src/qwiic_grch1120.h index b1f01b1..f1aa718 100644 --- a/src/qwiic_grch1120.h +++ b/src/qwiic_grch1120.h @@ -42,24 +42,8 @@ #include "res/qwiic_resdef.h" #include "qwiic_grcommon.h" -///////////////////////////////////////////////////////////////////////////// -// Flags for scrolling -///////////////////////////////////////////////////////////////////////////// -#define SCROLL_RIGHT 0x02 -#define SCROLL_LEFT 0x04 -#define SCROLL_UP 0x08 -#define SCROLL_DOWN 0x10 - -#define SCROLL_INTERVAL_6_FRAMES 0x00 -#define SCROLL_INTERVAL_32_FRAMES 0x01 -#define SCROLL_INTERVAL_64_FRAMES 0x02 -#define SCROLL_INTERVAL_128_FRAMES 0x03 -#define SCROLL_INTERVAL_3_FRAMES 0x04 -#define SCROLL_INTERVAL_4_FRAMES 0x05 -#define SCROLL_INTERVAL_5_FRAMES 0x06 -#define SCROLL_INTERVAL_2_FRAMES 0x07 - -#define kMaxPageNumber 16 +#define kMaxPageNumber 20 +#define kMaxCH1120Width 160 ///////////////////////////////////////////////////////////////////////////// // Buffer Management diff --git a/src/qwiic_grcommon.h b/src/qwiic_grcommon.h index ddf4713..db15d6e 100644 --- a/src/qwiic_grcommon.h +++ b/src/qwiic_grcommon.h @@ -1,6 +1,5 @@ // Common defines for multiple // TODO: Probably should rework how this looks eventually... -// possibly we will define another abstraction class and can also have these defines here... ///////////////////////////////////////////////////////////////////////////// // The graphics Raster Operator functions (ROPS) @@ -31,3 +30,39 @@ typedef struct int16_t xmin; int16_t xmax; } pageState_t; + +// Scrolling Macros: Note these ultimately get exposed to the user and used in examples etc. for scrolling +// These are the defines for the SSD1306, but we'll also treat them as the +// common defines for other drivers such that that the user can pass these in +#define SCROLL_VERTICAL 0x01 +#define SCROLL_RIGHT 0x02 +#define SCROLL_LEFT 0x04 + +// SCROLL_VERT_RIGHT on SSDS1306, SCROLL_UP on CH1120 +#define SCROLL_VERT_MODE1 (SCROLL_VERTICAL | SCROLL_RIGHT) +//SCROLL_VERT_LEFT on SSDS1306, SCROLL_DOWN on CH1120 +#define SCROLL_VERT_MODE2 (SCROLL_VERTICAL | SCROLL_LEFT) + +// More readable aliases for the vertical scrolling modes above +// For the CH1120 (1.5 inch display) +#define SCROLL_UP SCROLL_VERT_MODE1 +#define SCROLL_DOWN SCROLL_VERT_MODE2 + +// For the SSD1306 (all other displays) +#define SCROLL_VERT_RIGHT SCROLL_VERT_MODE1 +#define SCROLL_VERT_LEFT SCROLL_VERT_MODE2 + +// Reminder these are only valid for the SSD1306, other drivers should observe if these are passed +// in and map them to the correct constants to actually send to their displays +#define SCROLL_INTERVAL_5_FRAMES 0x00 +#define SCROLL_INTERVAL_64_FRAMES 0x01 +#define SCROLL_INTERVAL_128_FRAMES 0x02 +#define SCROLL_INTERVAL_256_FRAMES 0x03 // NOTE: NOT AVAILABLE FOR CH1120 +#define SCROLL_INTERVAL_3_FRAMES 0x04 +#define SCROLL_INTERVAL_4_FRAMES 0x05 +#define SCROLL_INTERVAL_25_FRAMES 0x06 // NOTE: NOT AVAILABLE FOR CH1120 +#define SCROLL_INTERVAL_2_FRAMES 0x07 + +// These are valid in the CH1120 driver and not present in the SSD1306 driver +#define SCROLL_INTERVAL_6_FRAMES 0x00 +#define SCROLL_INTERVAL_32_FRAMES 0x01 \ No newline at end of file diff --git a/src/qwiic_grssd1306.h b/src/qwiic_grssd1306.h index d27aac6..5dbd231 100644 --- a/src/qwiic_grssd1306.h +++ b/src/qwiic_grssd1306.h @@ -68,46 +68,6 @@ #define kDefaultVCOMDeselect 0x40 #define kDefaultContrast 0x8F -///////////////////////////////////////////////////////////////////////////// -// The graphics Raster Operator functions (ROPS) -///////////////////////////////////////////////////////////////////////////// -// - Copy - copy the pixel value in to the buffer (default) -// - Not Copy - copy the not of the pixel value to buffer -// - Not - Set the buffer value to not it's current value -// - XOR - XOR of color and current pixel value -// - Black - Set value to always be black -// - White - set value to always be white - -// moved to common for now... -// typedef enum gr_op_funcs_ -// { -// grROPCopy = 0, -// grROPNotCopy = 1, -// grROPNot = 2, -// grROPXOR = 3, -// grROPBlack = 4, -// grROPWhite = 5 -// } grRasterOp_t; - -///////////////////////////////////////////////////////////////////////////// -// Flags for scrolling -///////////////////////////////////////////////////////////////////////////// - -#define SCROLL_VERTICAL 0x01 -#define SCROLL_RIGHT 0x02 -#define SCROLL_LEFT 0x04 -#define SCROLL_VERT_RIGHT SCROLL_VERTICAL | SCROLL_RIGHT -#define SCROLL_VERT_LEFT SCROLL_VERTICAL | SCROLL_LEFT - -#define SCROLL_INTERVAL_5_FRAMES 0x00 -#define SCROLL_INTERVAL_64_FRAMES 0x01 -#define SCROLL_INTERVAL_128_FRAMES 0x02 -#define SCROLL_INTERVAL_256_FRAMES 0x03 -#define SCROLL_INTERVAL_3_FRAMES 0x04 -#define SCROLL_INTERVAL_4_FRAMES 0x05 -#define SCROLL_INTERVAL_25_FRAMES 0x06 -#define SCROLL_INTERVAL_2_FRAMES 0x07 - ///////////////////////////////////////////////////////////////////////////// // Buffer Management /////////////////////////////////////////////////////////////////////////////