From 7b79680de28c97d045c4255ce038ef3383972119 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 7 Feb 2023 17:09:17 +0000 Subject: [PATCH] First stab at HAL-installable OLED Display Driver. --- IO_OLEDDisplay.h | 178 +++++++++++++++++++++++++++++++++++++++++++++++ SSD1306Ascii.cpp | 11 ++- 2 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 IO_OLEDDisplay.h diff --git a/IO_OLEDDisplay.h b/IO_OLEDDisplay.h new file mode 100644 index 0000000..e158cd9 --- /dev/null +++ b/IO_OLEDDisplay.h @@ -0,0 +1,178 @@ +/* + * © 2023, Neil McKechnie. All rights reserved. + * + * This file is part of DCC++EX API + * + * This is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * It is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CommandStation. If not, see . + */ + +/* + * This driver provides a more immediate interface into the OLED display + * than the one installed through the config.h file. When an LCD(...) call + * is made, the text is output immediately to the specified display line, + * without waiting for the next 2.5 second refresh. However, no scrolling + * takes place, so if the line specified is off the screen then the text + * will instead be shown on the bottom line of the screen. + * + * To install, use the following command in myHal.cpp: + + * OLEDDisplay::create(address, width, height); + * + * where address is the I2C address (0x3c or 0x3d), + * width is the width in pixels of the display, and + * height is the height in pixels of the display. + * + * Valid width and height are 128x32 (SSD1306 controller), + * 128x64 (SSD1306) and 132x64 (SH1106). The driver uses + * a 5x7 character set in a 6x8 pixel cell. + */ + + +#ifndef IO_OLEDDISPLAY_H +#define IO_OLEDDISPLAY_H + +#include "IODevice.h" +#include "DisplayInterface.h" +#include "SSD1306Ascii.h" +#include "version.h" + +class OLEDDisplay : public IODevice, DisplayInterface { +private: + // Here we define the device-specific variables. + uint8_t _height; // in pixels + uint8_t _width; // in pixels + SSD1306AsciiWire *oled; + uint8_t _rowNo = 0; // Row number being written by caller + uint8_t _colNo = 0; // Position in line being written by caller + uint8_t _numRows; + uint8_t _numCols; + char *_buffer = NULL; + uint8_t *_rowGeneration = NULL; + uint8_t *_lastRowGeneration = NULL; + uint8_t _rowNoToScreen = 0; + uint8_t _charPosToScreen = 0; + +public: + // Static function to handle "OLEDDisplay::create(...)" calls. + static void create(I2CAddress i2cAddress, int width = 128, int height=64) { + /* if (checkNoOverlap(i2cAddress)) */ new OLEDDisplay(i2cAddress, width, height); + } + +protected: + // Constructor + OLEDDisplay(I2CAddress i2cAddress, int width, int height) { + _I2CAddress = i2cAddress; + _width = width; + _height = height; + _numCols = _width / 6; // character block 6 x 8 + _numRows = _height / 8; + + // Allocate arrays + _buffer = (char *)calloc(_numRows*_numCols, sizeof(char)); + _rowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t)); + _lastRowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t)); + + addDevice(this); + } + + // Device-specific initialisation + void _begin() override { + // Create OLED driver + oled = new SSD1306AsciiWire(); + // Initialise device + if (oled->begin(_I2CAddress, _width, _height)) { + // Store pointer to this object into CS display hook, so that we + // will intercept any subsequent calls to lcdDisplay methods. + DisplayInterface::lcdDisplay = this; + + DIAG(F("OLEDDisplay installed on address x%x"), (int)_I2CAddress); + + // First clear the entire screen + oled->clearNative(); + + // Set first two lines on screen + LCD(0,F("DCC++ EX v%S"),F(VERSION)); + LCD(1,F("Lic GPLv3")); + } + } + + ///////////////////////////////////////////////// + // DisplayInterface functions + // + // TODO: Limit to one call to setRowNative or writeNative + // per entry so that the function doesn't block waiting + // for I2C to complete. + ///////////////////////////////////////////////// + DisplayInterface* loop2(bool force) override { + + // Loop through the buffer and if a row has changed + // (rowGeneration[row] is changed) then start writing the + // characters from the buffer, one character per entry, + // to the screen until that row has been refreshed. + // TODO: Currently this is done all in one go! Split + // it up so that at most one call is made to either + // setRowNative or writeNative per loop entry - then + // we shan't have to wait for I2C. + for (uint8_t row = 0; row < _numRows; row++) { + if (_rowGeneration[row] != _lastRowGeneration[row]) { + // Row has been modified, write to screen + oled->setRowNative(row); + for (uint8_t _col = 0; _col < _numCols; _col++) { + oled->writeNative(_buffer[(uint16_t)row*_numCols+_col]); + } + _lastRowGeneration[row] = _rowGeneration[row]; + } + } + return NULL; + } + + // Position on nominated line number (0 to number of lines -1) + // Clear the line in the buffer ready for updating + void setRow(byte line) override { + if (line >= _numRows) line = _numRows-1; + _rowNo = line; + // Fill line with blanks + for (_colNo = 0; _colNo < _numCols; _colNo++) + _buffer[_rowNo*_numCols+_colNo] = ' '; + _colNo = 0; + // Mark that the buffer has been touched. It will be + // sent to the screen on the next loop entry. + _rowGeneration[_rowNo]++; + } + + // Write blanks to all of the screen (blocks until complete) + void clear () override { + // Clear buffer + for (_rowNo = 0; _rowNo < _numRows; _rowNo++) { + setRow(_rowNo); + } + _rowNo = 0; + } + + // Write one character + size_t write(uint8_t c) override { + // Write character to buffer (if space) + if (_colNo < _numCols) + _buffer[_rowNo*_numCols+_colNo++] = c; + return 1; + } + + // Display information about the device. + void _display() { + DIAG(F("OLEDDisplay Configured addr x%x"), (int)_I2CAddress); + } + +}; + +#endif // IO_OLEDDISPLAY_H \ No newline at end of file diff --git a/SSD1306Ascii.cpp b/SSD1306Ascii.cpp index 7373a74..12d8702 100644 --- a/SSD1306Ascii.cpp +++ b/SSD1306Ascii.cpp @@ -144,7 +144,11 @@ const uint8_t FLASH SSD1306AsciiWire::SH1106_132x64init[] = { //------------------------------------------------------------------------------ // Constructor -SSD1306AsciiWire::SSD1306AsciiWire() {} +SSD1306AsciiWire::SSD1306AsciiWire() { + I2CManager.begin(); + I2CManager.setClock(400000L); // Set max supported I2C speed + +} // CS auto-detect and configure constructor SSD1306AsciiWire::SSD1306AsciiWire(int width, int height) { @@ -193,7 +197,7 @@ bool SSD1306AsciiWire::begin(I2CAddress address, int width, int height) { return false; } // Device found - DIAG(F("%dx%d OLED display configured on I2C:x%x"), m_displayWidth, m_displayHeight, (uint8_t)m_i2cAddr); + DIAG(F("%dx%d OLED display configured on I2C:x%x"), m_displayWidth, m_displayHeight, (int)m_i2cAddr); clear(); return true; } @@ -204,7 +208,8 @@ void SSD1306AsciiWire::clearNative() { for (uint8_t r = 0; r <= m_displayHeight/8 - 1; r++) { setRowNative(r); // Position at start of row to be erased for (uint8_t c = 0; c <= m_displayWidth - 1; c += maxBytes-1) { - uint8_t len = min(m_displayWidth-c, maxBytes-1) + 1; + uint8_t len = m_displayWidth-c+1; + if (len > maxBytes) len = maxBytes; I2CManager.write_P(m_i2cAddr, blankPixels, len); // Write a number of blank columns } }