3

For a hobby project, I am trying to make some sort of little game with an ATtiny85 and an SSD1306 OLED display, mostly to see how much I can do with the least hardware possible. Something like Captain Rectangle rescues the Circle Princess from the Evil Triangles.

I have been seeing what I can do with an SSD1306 (datasheet), specifically horizontal scrolling. It has a feature to scroll the display horizontally (left or right) at a certain rate, i.e. 1 pixel per given number of frames. This is achieved by issuing three commands:

  1. The first command sets up the scroll.
  2. The second command starts the scroll.
  3. The third command stops the scroll.

While letting the display unit take care of scrolling all by itself sure takes a load off the minimalist MCU, what I really want is to just scroll 1 pixel right away (OK if it takes the display milliseconds to do this). Is there a reliable way to do that?

Some things I have tried:

  • Draw a column of pixels, setup and start the scroll, stop the scroll right away, delay 10ms, repeat. The screen does not do much scrolling--I think it only scrolls every x number of attempts, depending on the timing of when the OLED wants to do scrolling vs. when I tell it to stop.
  • Stop scrolling, draw a column of pixels, setup and start the scroll, delay 10ms. In a preliminary demo, this appears to mostly work, but obviously I will have to read more about the timing of the scrolling to really know, and besides this is a fragile solution--if the OLED scrolls 1 too many or too few pixels, then the MCU program's understanding of what should be on the screen will not line up with that of the display.
  • I also looked at the "Set Column Address" command, which specifies column start address and end address of the display data RAM, but that has no effect on data already displayed--it does not move visibly on the screen, but only affects subsequent writes--where the address pointer points, and what columns it may point to.

BTW, I am using vertical memory address mode, i.e. I send data one column of pixels at a time, since that is what would have to be drawn when scrolling horizontally 1 pixel.

I see there are more options to try with vertical scrolling, though my screen size (128x64) is not conducive to that.

I could also try re-writing everything on the display every 10ms, but first, did I overlook anything in the datasheet?

jose can u c
6,9742 gold badges16 silver badges27 bronze badges
asked Apr 2, 2018 at 21:21
1
  • the SSD1306 datasheet that you posted ... search for the word "vertical" in the datasheet Commented Apr 3, 2018 at 1:42

1 Answer 1

1

Yes, instead of the usual continual horizontal scroll commands (0x26 and 0x27, section 10.2.1 of the datasheet1), there are two undocumented commands that scroll by just one pixel. These are 0x2C and 0x2D.

If you want to scroll by n number of pixels, then just repeatedly issue the one-pixel scroll commands, n times. Note that you should pause a few milliseconds, before each repeated call - see below for an example.

From this answer to Scroll long text on OLED display:

The SSD1306 chip provides commands to enable both continuous scrolling and 1 pixel scroll. For our purpose of scrolling long text, the continuous scroll is not useful, as we want to scroll exactly one pixel. Therefore, we need to use the 1 pixel scroll command. The commands take the same parameters, except for the first opcode byte. For some reason in the widely circulated (and pretty old) datasheet online, the 1 pixel scroll command is missing, but it is there in HW.

#define CMD_CONTINUOUS_SCROLL_H_RIGHT 0x26
#define CMD_CONTINUOUS_SCROLL_H_LEFT 0x27
#define CMD_ONE_COLUMN_SCROLL_H_RIGHT 0x2C
#define CMD_ONE_COLUMN_SCROLL_H_LEFT 0x2D

Use the latter two.

You need to incorporate these two commands into your sketch - use whatever method you like.


I have made a couple of libraries that are derived from the Adafruit_SSD1306 and SSD1306_I2C libraries, which add single pixel scroll commands, plus three "new" commands which are documented in v1.5 (but not v1.1) of the datasheet: zoom, fade and blink:

You can use these in exactly the same way as the parent classes, they inherit all of the methods from their parents.

Or... you can manually modify the Adafruit_SSD1306 and SSD1306_I2C libraries, to add two new public member functions to the former (startscrollrightone() and startscrollleftone()) and one new public member function to the latter (setupScrollHOne()).

Modifying Adafruit_SSD1306 library

Add to Adafruit_SSD1306.h the following #define lines:

#define SSD1306_RIGHT_HORIZONTAL_SCROLL_ONE 0x2C ///< Init right scroll one pixel
#define SSD1306_LEFT_HORIZONTAL_SCROLL_ONE 0x2D ///< Init left scroll one pixel

and the following two declarations:

 void startscrollrightone(uint8_t start, uint8_t stop);
 void startscrollleftone(uint8_t start, uint8_t stop);

Add to Adafruit_SSD1306.cpp the following lines

/*!
 @brief Activate a 1 pixel right-handed scroll for all or part of the display.
 @param start
 First row.
 @param stop
 Last row.
 @return None (void).
*/
// To scroll the whole display, run: display.startscrollrightone(0x00, 0x0F)
void Adafruit_SSD1306::startscrollrightone(uint8_t start, uint8_t stop) {
 TRANSACTION_START
 static const uint8_t PROGMEM scrollList1a[] = {
 SSD1306_RIGHT_HORIZONTAL_SCROLL_ONE, 0X00};
 ssd1306_commandList(scrollList1a, sizeof(scrollList1a));
 ssd1306_command1(start);
 ssd1306_command1(0X00);
 ssd1306_command1(stop);
 static const uint8_t PROGMEM scrollList1b[] = {0X00, 0XFF,
 SSD1306_ACTIVATE_SCROLL};
 ssd1306_commandList(scrollList1b, sizeof(scrollList1b));
 TRANSACTION_END
}
/*!
 @brief Activate a 1 pixel left-handed scroll for all or part of the display.
 @param start
 First row.
 @param stop
 Last row.
 @return None (void).
*/
// To scroll the whole display, run: display.startscrollleftone(0x00, 0x0F)
void Adafruit_SSD1306::startscrollleftone(uint8_t start, uint8_t stop) {
 TRANSACTION_START
 static const uint8_t PROGMEM scrollList2a[] = {SSD1306_LEFT_HORIZONTAL_SCROLL_ONE,
 0X00};
 ssd1306_commandList(scrollList2a, sizeof(scrollList2a));
 ssd1306_command1(start);
 ssd1306_command1(0X00);
 ssd1306_command1(stop);
 static const uint8_t PROGMEM scrollList2b[] = {0X00, 0XFF,
 SSD1306_ACTIVATE_SCROLL};
 ssd1306_commandList(scrollList2b, sizeof(scrollList2b));
 TRANSACTION_END
}
Modifying SSD1306_I2C library

Add to SSD1306_I2C.h

 void setupScrollHOne(bool dir, uint8_t start, uint8_t end, uint8_t interval);

Add to SSD1306_I2C.cpp

// dir: 0 - right, 1 - left
void SSD1306::setupScrollHOne(bool dir, uint8_t start, uint8_t end, uint8_t interval) {
 stopScroll();
 sendCommand(0x2C + int(dir));
 sendCommand(0x00);
 sendCommand(start);
 sendCommand(interval);
 sendCommand(end);
 sendCommand(0x00);
 sendCommand(0xFF);
}
Example for Adafruit_SSD1306 library

Scrolls some text by 50 pixels to the right:

void testscrolltext_by50pixels(void) {
 display.clearDisplay();
 display.setTextSize(2); // Draw 2X-scale text
 display.setTextColor(SSD1306_WHITE);
 display.setCursor(10, 0);
 display.println(F("scroll"));
 display.display(); // Show initial text
 delay(100);
 int wait_time = 12; // For the Xiao: 15 is the minimum (maybe 12... but definitely not 11)
 for (int i = 0; i < 50; i++) {
 display.startscrollrightone(0x00, 0x0F);
 delay(wait_time);
 }
 delay(2000);
}

Note: I found, on the Seeeduino Xiao (SAMD21) at least, that a delay is required, immediately after issuing a 1-pixel scroll command, or when repeatedly issuing 1-pixel scroll commands.

 display.startscrollrightone(0x00, 0x0F);
 delay(wait_time);

Without any delay(wait_time) then no scrolling occurred at all. If wait_time is less than 12 ms delay, then that causes some of the scrolls to fail, and the full 50 pixels are not scrolled. The closer that the delay is to zero, the less distance, in pixels scrolled, the text/images actually move. The minimum length of the required delay probably depends upon the frequency of the μController, so you may need to experiment for your particular setup.

Example for SSD1306_I2C library

Modify the example file SSD13306_I2C/examples/DisplayBitmap.ino, by adding the following to the end of setup(), to scroll the fish bitmap right by one pixel

 display.setupScrollHOne(RIGHT, 0, 7, FRAMES_2);
 display.startScroll(); // Begin scroll
 delay(500); // Wait half a second between scrolling
A scrolling landscape

Making use of the pages feature of the SSD1306 - in conjunction with varying single pixel scroll rates - here is a scrolling landscape:

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
// The pins for I2C are defined by the Wire-library.
// On an arduino UNO: A4(SDA), A5(SCL)
// On an arduino MEGA 2560: 20(SDA), 21(SCL)
// On an arduino LEONARDO: 2(SDA), 3(SCL), ...
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define LOGO_HEIGHT 64
#define LOGO_WIDTH 128
// Generated from https://javl.github.io/image2cpp/
// 'StarWarsLandscape', 128x64px
const unsigned char epd_bitmap_StarWarsLandscape_WonB[] PROGMEM = {
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x01, 0xc1, 0x80, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x03, 0x00, 0x80, 0x00, 0x0f, 0x8e, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
 0x00, 0x0e, 0x00, 0xc0, 0x00, 0x18, 0xf8, 0x30, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xc8, 0xf0, 0x00,
 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x60, 0x10, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x0d, 0x08, 0x00,
 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x07, 0x0c, 0x00,
 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x10, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x04, 0x00,
 0x00, 0x40, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x30, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x8c, 0x00,
 0x00, 0x20, 0x00, 0xc0, 0x01, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x80, 0x01, 0x80, 0x78, 0x00,
 0x00, 0x20, 0x20, 0x80, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x30, 0x00,
 0x00, 0x18, 0x31, 0x80, 0x01, 0x20, 0x00, 0x18, 0x00, 0x00, 0x00, 0xc0, 0x05, 0x00, 0x20, 0x00,
 0x00, 0x0c, 0x6b, 0x00, 0x01, 0xc0, 0x00, 0x08, 0x00, 0x00, 0x00, 0x60, 0x01, 0x80, 0x60, 0x00,
 0x00, 0x03, 0x8e, 0x00, 0x00, 0x80, 0x10, 0x08, 0x00, 0x00, 0x00, 0x40, 0x03, 0xc1, 0xc0, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x18, 0x18, 0x00, 0x00, 0x00, 0x60, 0x0e, 0x7f, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x38, 0x70, 0x00, 0x00, 0x00, 0x3e, 0xf8, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0xc1, 0xec, 0xc0, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00,
 0x00, 0x00, 0x03, 0x88, 0x00, 0x03, 0xff, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x18, 0x00,
 0x00, 0x00, 0x06, 0x08, 0x00, 0x06, 0x00, 0x80, 0x00, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x38, 0x00,
 0x80, 0x00, 0x0c, 0x0f, 0x00, 0x0c, 0x00, 0xc0, 0x00, 0x02, 0x1f, 0x80, 0x00, 0x01, 0xe8, 0x00,
 0xe0, 0x00, 0x78, 0x00, 0xe0, 0x18, 0x00, 0x40, 0x00, 0x06, 0x00, 0xc0, 0x00, 0x03, 0x0c, 0x01,
 0x20, 0x00, 0xc0, 0x00, 0x30, 0x30, 0x00, 0x40, 0x00, 0x04, 0x00, 0x40, 0x00, 0x0c, 0x07, 0xc3,
 0x30, 0x03, 0x00, 0x00, 0x0c, 0x60, 0x00, 0x20, 0x00, 0x0c, 0x00, 0x40, 0x00, 0x18, 0x00, 0x76,
 0x10, 0x02, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x20, 0x00, 0x18, 0x00, 0x7c, 0x00, 0x30, 0x00, 0x1c,
 0x18, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x30, 0x00, 0x07, 0x80, 0x20, 0x00, 0x04,
 0x0f, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x60, 0x00, 0x00, 0xc0, 0x20, 0x00, 0x00,
 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0xc0, 0x00, 0x00, 0x20, 0xc0, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x01, 0x80, 0x00, 0x00, 0x1f, 0x40, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x18, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x18, 0x3c, 0x0e, 0x03, 0x0c, 0x00, 0x1c, 0x00, 0x00, 0x3f, 0x8e, 0x00, 0x07, 0xe0, 0x1e, 0x00,
 0x2e, 0x63, 0xb9, 0xce, 0x04, 0x67, 0xe6, 0xe0, 0x00, 0x60, 0xcf, 0x00, 0x6d, 0xf8, 0x73, 0x80,
 0xe3, 0xc0, 0xe0, 0x78, 0x07, 0xfc, 0x03, 0xbe, 0x01, 0xc0, 0x79, 0xfd, 0xf8, 0xcf, 0xc0, 0xff,
 0x41, 0x80, 0x40, 0x30, 0x00, 0x18, 0x03, 0x03, 0xff, 0x00, 0x19, 0x87, 0x00, 0x03, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
void setup() {
 Serial.begin(9600);
 // by default, we'll generate the high voltage from the 3.3v line internally! (neat!)
 //display.begin(SSD1306_SWITCHCAPVCC); // Waaay too basic
 // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
 if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
 Serial.println(F("SSD1306 allocation failed"));
 for (;;)
 ; // Don't proceed, loop forever
 }
 // init done
 // Clear the buffer.
 display.clearDisplay();
 // bitmap display
 display.drawBitmap(0, 0, epd_bitmap_StarWarsLandscape_WonB, 128, 64, 1);
 display.display();
}
void loop() {
 scroll_landscape();
}
void scroll_landscape(void) {
 int wait_time = 12; // For the Xiao: 15 is the minimum (maybe 12... but definitely not 11)
 for (int j = 0; j < 2; j++) {
 for (int i = 0; i < 2; i++) {
 display.startscrollleftone(0x06, 0x07);
 delay(wait_time);
 }
 display.startscrollleftone(0x03, 0x05);
 delay(wait_time);
 }
 display.startscrollleftone(0x00, 0x02);
 delay(wait_time);
}
Footnote

1 Datasheets:

Further reading
Github repos
answered Jul 26 at 19:44

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.