UNPKG

oled-rpi-i2c-bus-async

Version:

Asynchronous NodeJS module for controlling oled devices on the Raspbery Pi (including the SSD1306 and SH1106 OLED screens)

427 lines (366 loc) 15.7 kB
// SH1106 OLED driver implementation import BaseOLED from './base-oled.mjs'; import { createLogger } from '../utils/logger.mjs'; class SH1106 extends BaseOLED { constructor(i2c, opts) { super(i2c, opts); // Create driver-specific logger this.logger = createLogger('SH1106'); // Set logger level if provided if (opts?.logLevel) { this.logger.setLevel(opts.logLevel); } // SH1106 specific command definitions this.SET_START_LINE = 0x40; this.CHARGE_PUMP = 0xad; this.COLUMN_LOW_START_ADDR = 0x02; this.COLUMN_HIGH_START_ADDR = 0x10; this.PAGE_ADDR = 0xb0; this.SEG_REMAP = 0xa1; this.COM_SCAN_DEC = 0xc8; this.SET_COM_PINS = 0xda; // Additional SH1106 specific commands for hardware acceleration this.DC_DC_CONTROL = 0xad; // DC-DC control this.DISPLAY_ENHANCEMENT = 0xd6; // Display enhancement this.PUMP_VOLTAGE_SET = 0x30; // Pump voltage set // Page addressing commands this.SET_PAGE_ADDRESSING = 0x20; // Memory addressing mode this.PAGE_ADDRESSING_MODE = 0x02; // Page addressing mode (better for partial updates) this.HORIZONTAL_ADDRESSING_MODE = 0x00; // Horizontal addressing mode (optimal for full updates) const config = { '128x32': { multiplex: 0x1f, compins: 0x02, coloffset: 0x02, }, '128x64': { multiplex: 0x3f, compins: 0x12, coloffset: 0x02, }, '96x16': { multiplex: 0x0f, compins: 0x02, coloffset: 0x02, }, }; const screenSize = `${this.WIDTH}x${this.HEIGHT}`; this.screenConfig = config[screenSize]; // Initialize the display this._initialise(); } /* ################################################################################################## * SH1106 specific implementations * ################################################################################################## */ // SH1106 does not support scrolling startScroll = async (dir, start, stop) => { this.logger.warn('SH1106 does not support scrolling'); }; // SH1106 does not support scrolling stopScroll = async () => { this.logger.warn('SH1106 does not support scrolling'); }; // Override base methods with SH1106-specific optimizations dimDisplay = async (bool) => { // Use the enhanced base implementation with SH1106-specific contrast values await this._enhancedDimDisplay(bool, { brightContrast: 0xCF, // Use a high but not maximum contrast for better visibility dimContrast: 0x0F // Use a more moderate dimming value for better visibility }); }; // Override base invertDisplay with SH1106-specific optimizations invertDisplay = async (bool) => { // Use the enhanced base implementation with SH1106-specific contrast values await this._enhancedInvertDisplay(bool, { normalContrast: 0x80, // Default contrast value invertedContrast: 0xA0 // Higher contrast value works better for inverted SH1106 display }); }; // Draw a column of a page on the SH1106 with batch commands drawPageCol = async (page, col, byte) => { // Wait for oled to be ready await this._waitUntilReady(); // Set the start and end byte locations for oled display update const bufferIndex = col + page * this.WIDTH; this.buffer[bufferIndex] = byte; // Ensure that column is only 0..127. col &= 0x7f; col += this.screenConfig.coloffset; // Column Bias for a SH1106. // Configure hardware-accelerated page addressing mode for single pixel updates const lowAddress = col & 0x0f; const highAddress = this.COLUMN_HIGH_START_ADDR | (col >>> 4); // Prepare command sequence - optimized for hardware acceleration // Using a single batch command with page and column selection reduces I2C overhead const displaySeq = [ this.SET_PAGE_ADDRESSING, // Set addressing mode command this.PAGE_ADDRESSING_MODE, // Use page addressing for single pixel updates this.PAGE_ADDR + page, // Set page address lowAddress, // Set low nibble of column highAddress // Set high nibble of column ]; // Send commands as a single batch - improves performance by reducing I2C transactions await this._transferBatch('cmd', displaySeq); // Send data byte - could be batched if multiple consecutive columns need updating await this._transfer('data', this.buffer[bufferIndex]); }; // Send the entire framebuffer to the oled with optimized hardware acceleration update = async () => { // Wait for oled to be ready await this._waitUntilReady(); // Optimize for full frame update by configuring hardware addressing mode // The SH1106 can use different addressing modes for different update patterns await this._transferBatch('cmd', [ this.SET_PAGE_ADDRESSING, // Set addressing mode this.HORIZONTAL_ADDRESSING_MODE, // Horizontal addressing mode is faster for full updates 0xA8, // Set MUX ratio command this.screenConfig.multiplex // Using screen-specific multiplex value ]); // Send display position commands as a batch // This enables auto-increment for both column and page addresses await this._transferBatch('cmd', [ this.PAGE_ADDR, // Start at page 0 this.COLUMN_LOW_START_ADDR, // Start at column 2 (accounting for SH1106 offset) this.COLUMN_HIGH_START_ADDR // High nibble of start address ]); // Send the entire buffer in one large transfer for maximum efficiency // This takes advantage of the auto-increment feature of the SH1106 // The SH1106 will automatically wrap to the next page when reaching the end of a row await this.wire.i2cWrite( this.ADDRESS, this.buffer.length + 1, // +1 for the control byte Buffer.concat([Buffer.from([0x40]), this.buffer]) // Prepend data control byte ); }; /* ################################################################################################## * Implementation of required base methods * ################################################################################################## */ // Initialize the SH1106 display with optimized batch commands _initialise = async () => { // Create initialization sequences with named batches for better organization const initSequences = [ { name: 'initial', commands: [ this.DISPLAY_OFF, this.SET_DISPLAY_CLOCK_DIV, 0x80 // Use optimized clock divider for efficiency ] }, { name: 'multiplex', commands: [ this.SET_MULTIPLEX, this.screenConfig.multiplex, // Set dynamically based on screen size this.SET_DISPLAY_OFFSET, 0x00, this.SET_START_LINE ] }, { name: 'charge_pump', commands: [ this.CHARGE_PUMP, 0x8b, // Enable charge pump for better efficiency this.SET_PAGE_ADDRESSING, this.HORIZONTAL_ADDRESSING_MODE // Default to horizontal addressing mode ] }, { name: 'com_config', commands: [ this.SEG_REMAP, // Screen orientation this.COM_SCAN_DEC, // Screen orientation - change to INC to flip this.SET_COM_PINS, this.screenConfig.compins // Com pins val sets dynamically for screen size ] }, { name: 'contrast', commands: [ this.SET_CONTRAST, 0x80, // Contrast val this.SET_PRECHARGE, 0x1F // Optimized precharge period (shorter - from datasheet p.28) ] }, { name: 'final', commands: [ this.SET_VCOM_DETECT, 0x40, // Optimized VCOM deselect level for SH1106 (datasheet p.29) this.PUMP_VOLTAGE_SET | 0x2, // Set pump output to 8.0V (datasheet optimal value) this.DISPLAY_ENHANCEMENT, 0x00, // Enhanced display quality without changing API 0xC0, // Set internal DC-DC operation mode to high-efficiency mode (Datasheet p.25) this.DISPLAY_ALL_ON_RESUME, // Outputs RAM content (more power efficient than A5) this.NORMAL_DISPLAY, this.DISPLAY_ON ] } ]; // Use the common base initialization method await this._baseInitialize(initSequences); }; // Update dirty bytes - optimized version with page grouping and batch commands _updateDirtyBytes = async (dirtyByteArray) => { try { const dirtyByteArrayLen = dirtyByteArray.length; // If there are no dirty bytes, nothing to do if (dirtyByteArrayLen === 0) { return; } // Check if full update would be more efficient using common method if (this._shouldDoFullUpdate(dirtyByteArray)) { // More efficient to do a full update await this.update(); // Now that all bytes are synced, reset dirty state this.dirtyBytes = []; return; } // Wait for display to be ready await this._waitUntilReady(); // Configure for page addressing mode - better for partial updates await this._transferBatch('cmd', [ this.SET_PAGE_ADDRESSING, this.PAGE_ADDRESSING_MODE ]); // Group dirty bytes by page using common method const pageGroups = this._groupDirtyBytesByPage(dirtyByteArray); // Now update each page's dirty bytes with optimized hardware access for (const [page, bytes] of pageGroups.entries()) { // Sort by column for sequential access (reduces I2C operations) bytes.sort((a, b) => a.col - b.col); // Optimize for consecutive columns by grouping them let currentStart = null; let currentEnd = null; let currentData = []; const flushCurrentRange = async () => { if (currentStart !== null) { // Compute column addresses for SH1106 let adjustedStart = currentStart & 0x7f; adjustedStart += this.screenConfig.coloffset; // Set page and column address in a single batch command // This takes advantage of hardware auto-increment within the page await this._transferBatch('cmd', [ this.PAGE_ADDR + page, adjustedStart & 0x0f, // Low nibble of column address this.COLUMN_HIGH_START_ADDR | (adjustedStart >>> 4) // High nibble of column address ]); // Send all data bytes for this range in a batch for better efficiency await this._transferBatch('data', currentData); // Reset tracking variables currentStart = null; currentEnd = null; currentData = []; } }; // Process each byte with optimized consecutive column detection for (let i = 0; i < bytes.length; i++) { const { col, byteIndex } = bytes[i]; // If this is a new range or not consecutive with previous column if (currentStart === null || col !== currentEnd + 1) { // Flush the current range if any await flushCurrentRange(); // Start a new range currentStart = col; } // Update the end of the range currentEnd = col; // Add data to the current batch currentData.push(this.buffer[byteIndex]); } // Flush any remaining range await flushCurrentRange(); } // Reset dirty bytes this.dirtyBytes = []; } catch (err) { this._handleError('updating dirty bytes', err); } }; // Optimized _transfer method that implements data prefetching for SH1106 _transfer = async (type, val) => { let control; if (type === 'data') { control = 0x40; } else if (type === 'cmd') { control = 0x00; } else { return; } // Create buffer for the transfer const bufferForSend = Buffer.from([control, val]); // Send control and actual val with hardware-optimized timing await this.wire.i2cWrite(this.ADDRESS, 2, bufferForSend); }; // Optimized batch transfer with hardware acceleration for SH1106 _transferBatch = async (type, values) => { // Early return for empty arrays if (!values || values.length === 0) return; const control = type === 'data' ? 0x40 : 0x00; // For single values, use the simpler _transfer method if (values.length === 1) { await this._transfer(type, values[0]); return; } // Create buffer with optimal layout for SH1106 hardware // SH1106 can process commands faster when properly spaced const buffer = Buffer.alloc(values.length * 2); // Fill buffer with control bytes and data bytes in an alternating pattern // This matches the SH1106's internal processing timing for (let i = 0; i < values.length; i++) { buffer[i * 2] = control; buffer[i * 2 + 1] = values[i]; } // Use a single I2C transaction to send all data // This reduces overhead and is more efficient for the SH1106 await this.wire.i2cWrite(this.ADDRESS, buffer.length, buffer); }; // Optimized wait mechanism for SH1106 that uses proper hardware polling _waitUntilReady = async () => { // SH1106 has a faster polling capability than the generic implementation // By checking the status register bits specifically defined in the SH1106 datasheet const tick = async () => { const byte = await this._readI2C(); // Check bit 6 which indicates "Command in Progress" on SH1106 // This is more reliable than the generic bit 7 used by other controllers const busy = (byte >> 6) & 1; if (!busy) { return; } else { // Use zero-timeout to yield to event loop without excessive delay await new Promise((resolve) => setTimeout(resolve, 0)); await tick(); } }; await tick(); }; // Implement overrides for base methods with SH1106-specific optimizations turnOnDisplay = async () => { // Use the enhanced base implementation with SH1106-specific options await this._enhancedTurnOnDisplay({ contrast: 0x80, // Include additional SH1106-specific power-on commands additionalCommands: [ 0xAE, // Display off during changes this.CHARGE_PUMP, // Set charge pump 0x8B, // Enable charge pump 0xA8, // Set MUX ratio this.screenConfig.multiplex // Screen-specific multiplex ] }); }; // Turn OLED off with power-optimized sequence turnOffDisplay = async () => { // Use the enhanced base implementation with SH1106-specific power-down commands await this._enhancedTurnOffDisplay({ additionalCommands: [ this.CHARGE_PUMP, // Set charge pump 0x8A // Disable charge pump to save power when display is off ] }); }; } export default SH1106;