diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk index c976b8296d5a..3ca1af40c351 100644 --- a/builddefs/common_features.mk +++ b/builddefs/common_features.mk @@ -658,6 +658,10 @@ endif VALID_OLED_DRIVER_TYPES := SSD1306 custom OLED_DRIVER ?= SSD1306 + +VALID_OLED_DRIVER_BUS_TYPES := spi i2c +OLED_DRIVER_BUS ?= i2c + ifeq ($(strip $(OLED_ENABLE)), yes) ifeq ($(filter $(OLED_DRIVER),$(VALID_OLED_DRIVER_TYPES)),) $(call CATASTROPHIC_ERROR,Invalid OLED_DRIVER,OLED_DRIVER="$(OLED_DRIVER)" is not a valid OLED driver) @@ -668,7 +672,17 @@ ifeq ($(strip $(OLED_ENABLE)), yes) OPT_DEFS += -DOLED_DRIVER_$(strip $(shell echo $(OLED_DRIVER) | tr '[:lower:]' '[:upper:]')) ifeq ($(strip $(OLED_DRIVER)), SSD1306) SRC += ssd1306_sh1106.c - QUANTUM_LIB_SRC += i2c_master.c + ifeq ($(filter $(OLED_DRIVER_BUS),$(VALID_OLED_DRIVER_BUS_TYPES)),) + $(call CATASTROPHIC_ERROR,Invalid OLED_DRIVER_BUS,OLED_DRIVER_BUS="$(OLED_DRIVER_BUS)" is not a valid OLED driver) + else + ifeq ($(strip $(OLED_DRIVER_BUS)), i2c) + QUANTUM_LIB_SRC += i2c_master.c + OPT_DEFS += -DOLED_BUS=0 + else + QUANTUM_LIB_SRC += spi_master.c + OPT_DEFS += -DOLED_BUS=1 + endif + endif endif endif endif diff --git a/docs/feature_oled_driver.md b/docs/feature_oled_driver.md index 0d04f007f8bc..a4bf395ec8a1 100644 --- a/docs/feature_oled_driver.md +++ b/docs/feature_oled_driver.md @@ -2,7 +2,7 @@ ## Supported Hardware -OLED modules using SSD1306 or SH1106 driver ICs, communicating over I2C. +OLED modules using SSD1306 or SH1106 driver ICs, communicating over I2C or SPI. Tested combinations: |IC |Size |Platform|Notes | @@ -18,7 +18,7 @@ Hardware configurations using Arm-based microcontrollers or different sizes of O ## Usage -To enable the OLED feature, there are two steps. First, when compiling your keyboard, you'll need to add the following to your `rules.mk`: +To enable the OLED feature, there are typically two steps. First, when compiling your keyboard, you'll need to add the following to your `rules.mk`: ```make OLED_ENABLE = yes @@ -34,6 +34,17 @@ e.g. OLED_DRIVER = SSD1306 ``` +## OLED bus +|OLED Bus |Tested Device | +|-------------------|---------------------------| +|I2C (default) |SSD1306 and SH1106 | +|SPI |SH1106 | + +e.g. +```make +OLED_DRIVER_BUS = i2c +``` + Then in your `keymap.c` file, implement the OLED task call. This example assumes your keymap has three layers named `_QWERTY`, `_FN` and `_ADJ`: ```c @@ -174,6 +185,23 @@ These configuration options should be placed in `config.h`. Example: |`OLED_BRIGHTNESS` |`255` |The default brightness level of the OLED, from 0 to 255. | |`OLED_UPDATE_INTERVAL` |`0` |Set the time interval for updating the OLED display in ms. This will improve the matrix scan rate. | + +## Configuring a SPI OLED + +If using a SPI OLED display, you'll need to define the following pins in your board's `config.h` and configure the (SPI Master Driver)[spi_driver.md]` + +|Defines |Description | +|-------------------|----------------------------------------------------------------| +|OLED_DC_PIN |Pin that determines whether the data pins are data or command | +|OLED_CS_PIN |Pin that is used to select the chip | +|OLED_RST_PIN |Pin to reset the display | + +You can also define the mode and divisor using the following defines: + +|Define |Default |Description | +|---------------------------|-----------------|-----------------------------------------------------------| +|`OLED_SPI_MODE` |`3` |The SPI mode to use with the OLED Display | +|`OLED_SPI_DIVISOR` |`2` |The SPI clock divisoR to be used with the OLED Display | ## 128x64 & Custom sized OLED Displays The default display size for this feature is 128x32 and all necessary defines are precalculated with that in mind. We have added a define, `OLED_DISPLAY_128X64`, to switch all the values to be used in a 128x64 display, as well as added a custom define, `OLED_DISPLAY_CUSTOM`, that allows you to provide the necessary values to the driver. diff --git a/drivers/oled/oled_driver.h b/drivers/oled/oled_driver.h index 918b837f07e8..7b184f562d19 100644 --- a/drivers/oled/oled_driver.h +++ b/drivers/oled/oled_driver.h @@ -23,6 +23,32 @@ along with this program. If not, see . #define OLED_IC_SSD1306 0 #define OLED_IC_SH1106 1 +#define OLED_BUS_I2C 0 +#define OLED_BUS_SPI 1 + +// Define the bus for the chip +#ifndef OLED_BUS +# define OLED_BUS OLED_BUS_I2C //Defaults to I2C +#endif + +#if (OLED_BUS == OLED_BUS_SPI) +# ifndef OLED_DC_PIN +# error "The OLED driver in SPI needs a D/C pin defined" +# endif +# ifndef OLED_CS_PIN +# error "The OLED driver in SPI needs a CS pin defined" +# endif +# ifndef OLED_CS_PIN +# error "The OLED driver in SPI needs a CS pin defined" +# endif +# ifndef OLED_SPI_MODE +# define OLED_SPI_MODE 3 +# endif +# ifndef OLED_SPI_DIVISOR +# define OLED_SPI_DIVISOR 2 +# endif +#endif + #if defined(OLED_DISPLAY_CUSTOM) // Expected user to implement the necessary defines #elif defined(OLED_DISPLAY_128X64) diff --git a/drivers/oled/ssd1306_sh1106.c b/drivers/oled/ssd1306_sh1106.c index 30cfeb5648dd..e0bcf02c2d87 100644 --- a/drivers/oled/ssd1306_sh1106.c +++ b/drivers/oled/ssd1306_sh1106.c @@ -14,8 +14,16 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#include "i2c_master.h" #include "oled_driver.h" + +#if (OLED_BUS == OLED_BUS_I2C) +# include "i2c_master.h" +#endif +#if (OLED_BUS == OLED_BUS_SPI) +# include "spi_master.h" +#endif + +#include #include OLED_FONT_H #include "timer.h" #include "print.h" @@ -92,16 +100,123 @@ along with this program. If not, see . #define OLED_ALL_BLOCKS_MASK (((((OLED_BLOCK_TYPE)1 << (OLED_BLOCK_COUNT - 1)) - 1) << 1) | 1) -// i2c defines -#define I2C_CMD 0x00 -#define I2C_DATA 0x40 -#if defined(__AVR__) -# define I2C_TRANSMIT_P(data) i2c_transmit_P((OLED_DISPLAY_ADDRESS << 1), &data[0], sizeof(data), OLED_I2C_TIMEOUT) -#else // defined(__AVR__) -# define I2C_TRANSMIT_P(data) i2c_transmit((OLED_DISPLAY_ADDRESS << 1), &data[0], sizeof(data), OLED_I2C_TIMEOUT) -#endif // defined(__AVR__) -#define I2C_TRANSMIT(data) i2c_transmit((OLED_DISPLAY_ADDRESS << 1), &data[0], sizeof(data), OLED_I2C_TIMEOUT) -#define I2C_WRITE_REG(mode, data, size) i2c_writeReg((OLED_DISPLAY_ADDRESS << 1), mode, data, size, OLED_I2C_TIMEOUT) +#define ARRAY_SIZE(arr) sizeof(arr)/sizeof(arr[0]) + +#if OLED_BUS == OLED_BUS_I2C // i2c defines +# define I2C_CMD 0x00 +# define I2C_DATA 0x40 +# define OLED_STATUS_SUCCESS I2C_STATUS_SUCCESS + +// Internal variables to reduce math instructions +# if defined(__AVR__) +// identical to i2c_transmit, but for PROGMEM since all initialization is in PROGMEM arrays currently +// probably should move this into i2c_master... +static i2c_status_t i2c_transmit_P(uint8_t address, const uint8_t *data, uint16_t length, uint16_t timeout) { + i2c_status_t status = i2c_start(address | I2C_WRITE, timeout); + + for (uint16_t i = 0; i < length && status >= 0; i++) { + status = i2c_write(pgm_read_byte((const char *)data++), timeout); + if (status) break; + } + + i2c_stop(); + + return status; +} +# endif +#else // spi defines +# define OLED_STATUS_SUCCESS SPI_STATUS_SUCCESS + + void oled_spi_init(void) { + spi_init(); + + setPinOutput(OLED_CS_PIN); + writePinHigh(OLED_CS_PIN); + + setPinOutput(OLED_DC_PIN); + writePinLow(OLED_DC_PIN); + } + + void oled_spi_start(void) { + spi_start(OLED_CS_PIN, false, OLED_SPI_MODE, OLED_SPI_DIVISOR); + } + + void oled_spi_stop(void) { + spi_stop(); + } +#endif + + +// Transmit/Write Funcs. +bool oled_cmd(const uint8_t *data, uint16_t size) { +#if (OLED_BUS == OLED_BUS_I2C) + // Send the I2C command + if(i2c_transmit((OLED_DISPLAY_ADDRESS << 1), I2C_CMD, 1, OLED_I2C_TIMEOUT) != OLED_STATUS_SUCCESS){ + return false; + } + // Send the commands + if(i2c_transmit((OLED_DISPLAY_ADDRESS << 1), data, size, OLED_I2C_TIMEOUT) != OLED_STATUS_SUCCESS){ + return false; + } + + return true; +#else + oled_spi_start(); + // Command Mode + writePinLow(OLED_DC_PIN); + // Send the commands + if(spi_transmit(data, size) != OLED_STATUS_SUCCESS){ + oled_spi_stop(); + return false; + } + oled_spi_stop(); + return true; +#endif // (OLED_BUS == OLED_BUS_I2C) +} + +bool oled_cmd_p(const uint8_t *data, uint16_t size) { +#if (OLED_BUS == OLED_BUS_I2C && defined(__AVR__)) + // Send the I2C command + if(i2c_transmit_P((OLED_DISPLAY_ADDRESS << 1), I2C_CMD, 1, OLED_I2C_TIMEOUT) != OLED_STATUS_SUCCESS){ + return false; + } + // Send the commands + if(i2c_transmit_P((OLED_DISPLAY_ADDRESS << 1), I2C_CMD, size, OLED_I2C_TIMEOUT) != OLED_STATUS_SUCCESS){ + return false; + } + + return true; +#else + return oled_cmd(data, size); +#endif // (OLED_BUS == OLED_BUS_I2C) && defined(__AVR__) +} + +bool oled_write_reg(const uint8_t *data, uint16_t size) +{ +#if (OLED_BUS == OLED_BUS_I2C) + // Send the I2C command + if(i2c_transmit((OLED_DISPLAY_ADDRESS << 1), I2C_CMD, 1, OLED_I2C_TIMEOUT) != OLED_STATUS_SUCCESS){ + return false; + } + // Send the data + if(i2c_transmit((OLED_DISPLAY_ADDRESS << 1), data, size, OLED_I2C_TIMEOUT) != OLED_STATUS_SUCCESS){ + return false; + } + + return true; +#else + oled_spi_start(); + // Command Mode + writePinHigh(OLED_DC_PIN); + // Send the commands + if(spi_transmit(data, size) != OLED_STATUS_SUCCESS){ + oled_spi_stop(); + return false; + } + oled_spi_stop(); + return true; +#endif // (OLED_BUS == OLED_BUS_I2C) +} #define HAS_FLAGS(bits, flags) ((bits & flags) == flags) @@ -132,25 +247,6 @@ uint32_t oled_scroll_timeout; uint16_t oled_update_timeout; #endif -// Internal variables to reduce math instructions - -#if defined(__AVR__) -// identical to i2c_transmit, but for PROGMEM since all initialization is in PROGMEM arrays currently -// probably should move this into i2c_master... -static i2c_status_t i2c_transmit_P(uint8_t address, const uint8_t *data, uint16_t length, uint16_t timeout) { - i2c_status_t status = i2c_start(address | I2C_WRITE, timeout); - - for (uint16_t i = 0; i < length && status >= 0; i++) { - status = i2c_write(pgm_read_byte((const char *)data++), timeout); - if (status) break; - } - - i2c_stop(); - - return status; -} -#endif - // Flips the rendering bits for a character at the current cursor position static void InvertCharacter(uint8_t *cursor) { const uint8_t *end = cursor + OLED_FONT_WIDTH; @@ -173,10 +269,23 @@ bool oled_init(oled_rotation_t rotation) { } else { oled_rotation_width = OLED_DISPLAY_HEIGHT; } + +#if (OLED_BUS == OLED_BUS_I2C) i2c_init(); +#else + oled_spi_init(); +#endif + +#ifdef OLED_RST_PIN + /* Reset device */ + setPinOutput(OLED_RST_PIN); + writePinLow(OLED_RST_PIN); + wait_ms(20); + writePinHigh(OLED_RST_PIN); + wait_ms(20); +#endif static const uint8_t PROGMEM display_setup1[] = { - I2C_CMD, DISPLAY_OFF, DISPLAY_CLOCK, 0x80, @@ -193,27 +302,28 @@ bool oled_init(oled_rotation_t rotation) { 0x00, // Horizontal addressing mode #endif }; - if (I2C_TRANSMIT_P(display_setup1) != I2C_STATUS_SUCCESS) { + + if (!oled_cmd_p(display_setup1, ARRAY_SIZE(display_setup1))) { print("oled_init cmd set 1 failed\n"); return false; } if (!HAS_FLAGS(oled_rotation, OLED_ROTATION_180)) { - static const uint8_t PROGMEM display_normal[] = {I2C_CMD, SEGMENT_REMAP_INV, COM_SCAN_DEC}; - if (I2C_TRANSMIT_P(display_normal) != I2C_STATUS_SUCCESS) { + static const uint8_t PROGMEM display_normal[] = {SEGMENT_REMAP_INV, COM_SCAN_DEC}; + if (!oled_cmd_p(display_normal, ARRAY_SIZE(display_normal))) { print("oled_init cmd normal rotation failed\n"); return false; } } else { - static const uint8_t PROGMEM display_flipped[] = {I2C_CMD, SEGMENT_REMAP, COM_SCAN_INC}; - if (I2C_TRANSMIT_P(display_flipped) != I2C_STATUS_SUCCESS) { + static const uint8_t PROGMEM display_flipped[] = {SEGMENT_REMAP, COM_SCAN_INC}; + if (!oled_cmd_p(display_flipped, ARRAY_SIZE(display_flipped))) { print("display_flipped failed\n"); return false; } } - static const uint8_t PROGMEM display_setup2[] = {I2C_CMD, COM_PINS, OLED_COM_PINS, CONTRAST, OLED_BRIGHTNESS, PRE_CHARGE_PERIOD, 0xF1, VCOM_DETECT, 0x20, DISPLAY_ALL_ON_RESUME, NORMAL_DISPLAY, DEACTIVATE_SCROLL, DISPLAY_ON}; - if (I2C_TRANSMIT_P(display_setup2) != I2C_STATUS_SUCCESS) { + static const uint8_t PROGMEM display_setup2[] = {COM_PINS, OLED_COM_PINS, CONTRAST, OLED_BRIGHTNESS, PRE_CHARGE_PERIOD, 0xF1, VCOM_DETECT, 0x20, DISPLAY_ALL_ON_RESUME, NORMAL_DISPLAY, DEACTIVATE_SCROLL, DISPLAY_ON}; + if (!oled_cmd_p(display_setup2, ARRAY_SIZE(display_setup2))) { print("display_setup2 failed\n"); return false; } @@ -308,22 +418,22 @@ void oled_render(void) { } // Set column & page position - static uint8_t display_start[] = {I2C_CMD, COLUMN_ADDR, 0, OLED_DISPLAY_WIDTH - 1, PAGE_ADDR, 0, OLED_DISPLAY_HEIGHT / 8 - 1}; + static uint8_t display_start[] = {COLUMN_ADDR, 0, OLED_DISPLAY_WIDTH - 1, PAGE_ADDR, 0, OLED_DISPLAY_HEIGHT / 8 - 1}; if (!HAS_FLAGS(oled_rotation, OLED_ROTATION_90)) { - calc_bounds(update_start, &display_start[1]); // Offset from I2C_CMD byte at the start + calc_bounds(update_start, display_start); } else { - calc_bounds_90(update_start, &display_start[1]); // Offset from I2C_CMD byte at the start + calc_bounds_90(update_start, display_start); } // Send column & page position - if (I2C_TRANSMIT(display_start) != I2C_STATUS_SUCCESS) { + if (!oled_cmd(display_start, ARRAY_SIZE(display_start))) { print("oled_render offset command failed\n"); return; } if (!HAS_FLAGS(oled_rotation, OLED_ROTATION_90)) { // Send render data chunk as is - if (I2C_WRITE_REG(I2C_DATA, &oled_buffer[OLED_BLOCK_SIZE * update_start], OLED_BLOCK_SIZE) != I2C_STATUS_SUCCESS) { + if (!oled_write_reg(&oled_buffer[OLED_BLOCK_SIZE * update_start], OLED_BLOCK_SIZE)) { print("oled_render data failed\n"); return; } @@ -339,7 +449,7 @@ void oled_render(void) { } // Send render data chunk after rotating - if (I2C_WRITE_REG(I2C_DATA, &temp_buffer[0], OLED_BLOCK_SIZE) != I2C_STATUS_SUCCESS) { + if (!oled_write_reg(temp_buffer, OLED_BLOCK_SIZE)) { print("oled_render90 data failed\n"); return; } @@ -563,13 +673,13 @@ bool oled_on(void) { static const uint8_t PROGMEM display_on[] = #ifdef OLED_FADE_OUT - {I2C_CMD, FADE_BLINK, 0x00}; + {FADE_BLINK, 0x00}; #else - {I2C_CMD, DISPLAY_ON}; + {DISPLAY_ON}; #endif if (!oled_active) { - if (I2C_TRANSMIT_P(display_on) != I2C_STATUS_SUCCESS) { + if (!oled_cmd_p(display_on, ARRAY_SIZE(display_on))) { print("oled_on cmd failed\n"); return oled_active; } @@ -585,13 +695,13 @@ bool oled_off(void) { static const uint8_t PROGMEM display_off[] = #ifdef OLED_FADE_OUT - {I2C_CMD, FADE_BLINK, ENABLE_FADE | OLED_FADE_OUT_INTERVAL}; + {FADE_BLINK, ENABLE_FADE | OLED_FADE_OUT_INTERVAL}; #else - {I2C_CMD, DISPLAY_OFF}; + {DISPLAY_OFF}; #endif if (oled_active) { - if (I2C_TRANSMIT_P(display_off) != I2C_STATUS_SUCCESS) { + if (!oled_cmd_p(display_off, ARRAY_SIZE(display_off))) { print("oled_off cmd failed\n"); return oled_active; } @@ -609,9 +719,9 @@ uint8_t oled_set_brightness(uint8_t level) { return oled_brightness; } - uint8_t set_contrast[] = {I2C_CMD, CONTRAST, level}; + uint8_t set_contrast[] = { CONTRAST, level}; if (oled_brightness != level) { - if (I2C_TRANSMIT(set_contrast) != I2C_STATUS_SUCCESS) { + if (!oled_cmd(set_contrast, ARRAY_SIZE(set_contrast))) { print("set_brightness cmd failed\n"); return oled_brightness; } @@ -657,8 +767,8 @@ bool oled_scroll_right(void) { // Dont enable scrolling if we need to update the display // This prevents scrolling of bad data from starting the scroll too early after init if (!oled_dirty && !oled_scrolling) { - uint8_t display_scroll_right[] = {I2C_CMD, SCROLL_RIGHT, 0x00, oled_scroll_start, oled_scroll_speed, oled_scroll_end, 0x00, 0xFF, ACTIVATE_SCROLL}; - if (I2C_TRANSMIT(display_scroll_right) != I2C_STATUS_SUCCESS) { + uint8_t display_scroll_right[] = {SCROLL_RIGHT, 0x00, oled_scroll_start, oled_scroll_speed, oled_scroll_end, 0x00, 0xFF, ACTIVATE_SCROLL}; + if (!oled_cmd(display_scroll_right, ARRAY_SIZE(display_scroll_right))) { print("oled_scroll_right cmd failed\n"); return oled_scrolling; } @@ -675,8 +785,8 @@ bool oled_scroll_left(void) { // Dont enable scrolling if we need to update the display // This prevents scrolling of bad data from starting the scroll too early after init if (!oled_dirty && !oled_scrolling) { - uint8_t display_scroll_left[] = {I2C_CMD, SCROLL_LEFT, 0x00, oled_scroll_start, oled_scroll_speed, oled_scroll_end, 0x00, 0xFF, ACTIVATE_SCROLL}; - if (I2C_TRANSMIT(display_scroll_left) != I2C_STATUS_SUCCESS) { + uint8_t display_scroll_left[] = {SCROLL_LEFT, 0x00, oled_scroll_start, oled_scroll_speed, oled_scroll_end, 0x00, 0xFF, ACTIVATE_SCROLL}; + if (!oled_cmd(display_scroll_left, ARRAY_SIZE(display_scroll_left))) { print("oled_scroll_left cmd failed\n"); return oled_scrolling; } @@ -691,8 +801,8 @@ bool oled_scroll_off(void) { } if (oled_scrolling) { - static const uint8_t PROGMEM display_scroll_off[] = {I2C_CMD, DEACTIVATE_SCROLL}; - if (I2C_TRANSMIT_P(display_scroll_off) != I2C_STATUS_SUCCESS) { + static const uint8_t PROGMEM display_scroll_off[] = {DEACTIVATE_SCROLL}; + if (!oled_cmd_p(display_scroll_off, ARRAY_SIZE(display_scroll_off))) { print("oled_scroll_off cmd failed\n"); return oled_scrolling; } @@ -712,15 +822,15 @@ bool oled_invert(bool invert) { } if (invert && !oled_inverted) { - static const uint8_t PROGMEM display_inverted[] = {I2C_CMD, INVERT_DISPLAY}; - if (I2C_TRANSMIT_P(display_inverted) != I2C_STATUS_SUCCESS) { + static const uint8_t PROGMEM display_inverted[] = {INVERT_DISPLAY}; + if (!oled_cmd_p(display_inverted, ARRAY_SIZE(display_inverted))) { print("oled_invert cmd failed\n"); return oled_inverted; } oled_inverted = true; } else if (!invert && oled_inverted) { - static const uint8_t PROGMEM display_normal[] = {I2C_CMD, NORMAL_DISPLAY}; - if (I2C_TRANSMIT_P(display_normal) != I2C_STATUS_SUCCESS) { + static const uint8_t PROGMEM display_normal[] = {NORMAL_DISPLAY}; + if (!oled_cmd_p(display_normal, ARRAY_SIZE(display_normal))) { print("oled_invert cmd failed\n"); return oled_inverted; }