├── .gitignore ├── README.md ├── animationdriver.cpp ├── animationdriver.h ├── doc ├── CHIP-SSD1306.jpg └── OLED-qml.jpg ├── main.cpp ├── main.qml ├── oledrenderer.cpp ├── oledrenderer.h ├── qml-oled-renderer.pro ├── qml.qrc ├── ssd1306driver.cpp ├── ssd1306driver.h ├── ui2c-ssd1306.c └── window.qml /.gitignore: -------------------------------------------------------------------------------- 1 | *.pro.user 2 | *.qmlc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QML OLED Renderer 2 | 3 | Renders a QML application to a **SSD1306 OLED display**. 4 | 5 | ![OLED Display](./doc/OLED-qml.jpg) 6 | 7 | ```qml 8 | import QtQuick 2.6 9 | 10 | Item { 11 | id: root 12 | visible: true 13 | width: 128 14 | height: 64 15 | 16 | Rectangle { 17 | id: indicator 18 | anchors.bottom: parent.bottom 19 | anchors.bottomMargin: 5 20 | x: 5 21 | width: 15 22 | height: width 23 | radius: width / 2 24 | color: "black" 25 | } 26 | 27 | Text { 28 | anchors.centerIn: parent 29 | text: "Hello World!" 30 | } 31 | } 32 | ``` 33 | 34 | Tested on the [CHIP single board computer](https://getchip.com/) conected to TWI2. 35 | 36 | ![CHIP Setup](./doc/CHIP-SSD1306.jpg) 37 | 38 | ## Install 39 | 40 | Requires Qt Version 5.4 or higher. For the Debian package install this means that you need at least the packages from Debian Stretch. 41 | 42 | ```bash 43 | sudo apt install qtdeclarative5-dev qt5-default qtchooser qtbase5-dev qml-module-qtquick2 44 | ``` 45 | 46 | Then you can install the `qml-oled-renderer` with: 47 | ```bash 48 | qmake 49 | make 50 | sudo make install 51 | ``` 52 | 53 | ## Usage 54 | 55 | ``` 56 | sage: qml-oled-renderer [options] source 57 | Renders QML applications to a SSD1306 OLED display 58 | 59 | Options: 60 | -h, --help Displays this help. 61 | -w, --width OLED screen width 62 | -b, --bus I2C bus to which the OLED is connected 63 | -a, --address
I2C address of the OLED screen 64 | -f, --fps Number of frames to render per second 65 | 66 | Arguments: 67 | source QML source file` 68 | ``` 69 | 70 | The OLED renderer does not work without any display device. You can easily create visual framebuffer device using `XVfb`: 71 | 72 | ```bash 73 | sudo apt-get install xvfb 74 | ``` 75 | 76 | And the start the application as follows: 77 | ```bash 78 | Xvfb -shmem -screen 0 128x64x16 & 79 | DISPLAY=:0 qml-oled-renderer main.qml 80 | ``` 81 | -------------------------------------------------------------------------------- /animationdriver.cpp: -------------------------------------------------------------------------------- 1 | #include "animationdriver.h" 2 | 3 | AnimationDriver::AnimationDriver(int msPerStep) 4 | : m_step(msPerStep) 5 | , m_elapsed(0) 6 | { 7 | 8 | } 9 | 10 | void AnimationDriver::advance() 11 | { 12 | m_elapsed += m_step; 13 | advanceAnimation(); 14 | } 15 | 16 | qint64 AnimationDriver::elapsed() const 17 | { 18 | return m_elapsed; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /animationdriver.h: -------------------------------------------------------------------------------- 1 | #ifndef ANIMATIONDRIVER_H 2 | #define ANIMATIONDRIVER_H 3 | 4 | #include 5 | 6 | class AnimationDriver : public QAnimationDriver 7 | { 8 | public: 9 | AnimationDriver(int msPerStep); 10 | 11 | void advance() override; 12 | qint64 elapsed() const override; 13 | 14 | private: 15 | int m_step; 16 | qint64 m_elapsed; 17 | 18 | }; 19 | 20 | #endif // ANIMATIONDRIVER_H 21 | -------------------------------------------------------------------------------- /doc/CHIP-SSD1306.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machinekoder/qml-oled-renderer/626a09788be9e9e79bcce43420cadf745c0eccbd/doc/CHIP-SSD1306.jpg -------------------------------------------------------------------------------- /doc/OLED-qml.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machinekoder/qml-oled-renderer/626a09788be9e9e79bcce43420cadf745c0eccbd/doc/OLED-qml.jpg -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "oledrenderer.h" 5 | #include "ssd1306driver.h" 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | QGuiApplication app(argc, argv); 10 | qApp->setApplicationName("QML OLED Renderer"); 11 | 12 | QCommandLineParser parser; 13 | parser.setApplicationDescription("Renders QML applications to a SSD1306 OLED display"); 14 | parser.addHelpOption(); 15 | parser.addPositionalArgument("source", "QML source file"); 16 | parser.addOptions({ 17 | {{"w", "width"}, "OLED screen width", "weight"}, 18 | {{"h", "height"}, "OLED screen height", "height"}, 19 | {{"b", "bus"}, "I2C bus to which the OLED is connected", "bus"}, 20 | {{"a", "address"}, "I2C address of the OLED screen", "address"}, 21 | {{"f", "fps"}, "Number of frames to render per second", "fps"} 22 | }); 23 | 24 | parser.process(app); 25 | 26 | const QStringList args = parser.positionalArguments(); 27 | if (args.length() < 1) { 28 | qCritical() << "please specify a source file"; 29 | return -1; 30 | } 31 | QString sourceFile = args.first(); 32 | 33 | int width = parser.isSet("w") ? parser.value("w").toInt() : 128; 34 | int height = parser.isSet("h") ? parser.value("h").toInt() : 64; 35 | int bus = parser.isSet("b") ? parser.value("b").toInt() : 2; 36 | int address = parser.isSet("a") ? parser.value("a").toInt() : 0x3c; 37 | int fps = parser.isSet("f") ? parser.value("f").toInt() : 10; 38 | 39 | Ssd1306Driver driver; 40 | if (!driver.openDevice(QSize(width, height), bus, address)) { 41 | qCritical() << "cannot open OLED display"; 42 | return -1; 43 | } 44 | QObject::connect(qApp, &QGuiApplication::aboutToQuit, &driver, &Ssd1306Driver::clearScreen); 45 | 46 | OledRenderer renderer; 47 | QObject::connect(&renderer, &OledRenderer::imageRendered, [&driver](const QImage &image) { 48 | const auto mono = image.convertToFormat(QImage::Format_Mono, Qt::MonoOnly | Qt::ThresholdDither); 49 | driver.writeImage(mono); 50 | }); 51 | renderer.loadQmlFile(sourceFile, QSize(width, height), 1.0, fps); 52 | 53 | return app.exec(); 54 | } 55 | -------------------------------------------------------------------------------- /main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | 3 | Item { 4 | id: root 5 | visible: true 6 | width: 128 7 | height: 64 8 | 9 | Rectangle { 10 | property bool isRight: false 11 | 12 | id: indicator 13 | anchors.bottom: parent.bottom 14 | anchors.bottomMargin: 5 15 | x: (isRight ? 5 : (root.width - width) - 5) 16 | width: 15 17 | height: width 18 | radius: width / 2 19 | color: "black" 20 | 21 | Behavior on x { 22 | PropertyAnimation { 23 | duration: 500 24 | } 25 | } 26 | 27 | Timer { 28 | id: moveTimer 29 | running: true 30 | repeat: true 31 | interval: 500 32 | onTriggered: indicator.isRight = !indicator.isRight 33 | } 34 | } 35 | 36 | Text { 37 | anchors.centerIn: parent 38 | text: "Hello World!" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /oledrenderer.cpp: -------------------------------------------------------------------------------- 1 | #include "oledrenderer.h" 2 | 3 | #include 4 | 5 | OledRenderer::OledRenderer(QObject *parent) 6 | : QObject(parent) 7 | , m_context(nullptr) 8 | , m_offscreenSurface(nullptr) 9 | , m_renderControl(nullptr) 10 | , m_quickWindow(nullptr) 11 | , m_qmlEngine(nullptr) 12 | , m_qmlComponent(nullptr) 13 | , m_rootItem(nullptr) 14 | , m_fbo(nullptr) 15 | , m_animationDriver(nullptr) 16 | , m_status(NotRunning) 17 | , m_renderTimer(nullptr) 18 | { 19 | QSurfaceFormat format; 20 | // Qt Quick may need a depth and stencil buffer. Always make sure these are available. 21 | format.setDepthBufferSize(16); 22 | format.setStencilBufferSize(8); 23 | format.setSamples(1); 24 | 25 | m_context = new QOpenGLContext; 26 | m_context->setFormat(format); 27 | m_context->create(); 28 | 29 | m_offscreenSurface = new QOffscreenSurface; 30 | m_offscreenSurface->setFormat(m_context->format()); 31 | m_offscreenSurface->create(); 32 | 33 | m_renderControl = new QQuickRenderControl(this); 34 | m_quickWindow = new QQuickWindow(m_renderControl); 35 | 36 | m_qmlEngine = new QQmlEngine; 37 | if (!m_qmlEngine->incubationController()) 38 | m_qmlEngine->setIncubationController(m_quickWindow->incubationController()); 39 | 40 | m_context->makeCurrent(m_offscreenSurface); 41 | m_renderControl->initialize(m_context); 42 | } 43 | 44 | OledRenderer::~OledRenderer() 45 | { 46 | m_context->makeCurrent(m_offscreenSurface); 47 | delete m_renderControl; 48 | delete m_qmlComponent; 49 | delete m_quickWindow; 50 | delete m_qmlEngine; 51 | delete m_fbo; 52 | 53 | m_context->doneCurrent(); 54 | 55 | delete m_offscreenSurface; 56 | delete m_context; 57 | delete m_animationDriver; 58 | delete m_renderTimer; 59 | } 60 | 61 | void OledRenderer::loadQmlFile(const QString &qmlFile, const QSize &size, qreal devicePixelRatio, int fps) 62 | { 63 | if (m_status != NotRunning) { 64 | return; 65 | } 66 | 67 | m_size = size; 68 | m_dpr = devicePixelRatio; 69 | m_fps = fps; 70 | 71 | if (!loadQml(qmlFile, size)) { 72 | return; 73 | } 74 | 75 | start(); 76 | } 77 | 78 | void OledRenderer::start() 79 | { 80 | m_status = Running; 81 | createFbo(); 82 | 83 | if (!m_context->makeCurrent(m_offscreenSurface)) { 84 | return; 85 | } 86 | 87 | int renderInterval = 1000 / m_fps; 88 | // Render each frame of movie 89 | m_animationDriver = new AnimationDriver(renderInterval); 90 | m_animationDriver->install(); 91 | 92 | // Start the renderer 93 | m_renderTimer = new QTimer; 94 | m_renderTimer->setInterval(renderInterval); 95 | connect(m_renderTimer, &QTimer::timeout, this, &OledRenderer::renderNext); 96 | m_renderTimer->start(); 97 | renderNext(); 98 | } 99 | 100 | void OledRenderer::cleanup() 101 | { 102 | m_animationDriver->uninstall(); 103 | delete m_animationDriver; 104 | m_animationDriver = nullptr; 105 | 106 | if (m_renderTimer != nullptr) { 107 | delete m_renderTimer; 108 | m_renderTimer = nullptr; 109 | } 110 | 111 | destroyFbo(); 112 | } 113 | 114 | void OledRenderer::createFbo() 115 | { 116 | m_fbo = new QOpenGLFramebufferObject(m_size * m_dpr, QOpenGLFramebufferObject::CombinedDepthStencil); 117 | m_quickWindow->setRenderTarget(m_fbo); 118 | } 119 | 120 | void OledRenderer::destroyFbo() 121 | { 122 | delete m_fbo; 123 | m_fbo = nullptr; 124 | } 125 | 126 | bool OledRenderer::loadQml(const QString &qmlFile, const QSize &size) 127 | { 128 | if (m_qmlComponent != nullptr) { 129 | delete m_qmlComponent; 130 | } 131 | m_qmlComponent = new QQmlComponent(m_qmlEngine, QUrl(qmlFile), QQmlComponent::PreferSynchronous); 132 | 133 | if (m_qmlComponent->isError()) { 134 | const QList errorList = m_qmlComponent->errors(); 135 | for (const QQmlError &error : errorList) 136 | qWarning() << error.url() << error.line() << error; 137 | return false; 138 | } 139 | 140 | QObject *rootObject = m_qmlComponent->create(); 141 | if (m_qmlComponent->isError()) { 142 | const QList errorList = m_qmlComponent->errors(); 143 | for (const QQmlError &error : errorList) 144 | qWarning() << error.url() << error.line() << error; 145 | return false; 146 | } 147 | 148 | m_rootItem = qobject_cast(rootObject); 149 | if (!m_rootItem) { 150 | qWarning("run: Not a QQuickItem"); 151 | delete rootObject; 152 | return false; 153 | } 154 | 155 | // The root item is ready. Associate it with the window. 156 | m_rootItem->setParentItem(m_quickWindow->contentItem()); 157 | 158 | m_rootItem->setWidth(size.width()); 159 | m_rootItem->setHeight(size.height()); 160 | 161 | m_quickWindow->setGeometry(0, 0, size.width(), size.height()); 162 | 163 | return true; 164 | } 165 | 166 | void OledRenderer::renderNext() 167 | { 168 | // Polish, synchronize and render the next frame (into our fbo). 169 | m_renderControl->polishItems(); 170 | m_renderControl->sync(); 171 | m_renderControl->render(); 172 | 173 | m_context->functions()->glFlush(); 174 | 175 | emit imageRendered(m_fbo->toImage()); 176 | 177 | m_animationDriver->advance(); 178 | } 179 | 180 | bool OledRenderer::isRunning() 181 | { 182 | return m_status == Running; 183 | } 184 | 185 | QQuickItem *OledRenderer::rootItem() 186 | { 187 | return m_rootItem; 188 | } 189 | -------------------------------------------------------------------------------- /oledrenderer.h: -------------------------------------------------------------------------------- 1 | #ifndef OLEDRENDERER_H 2 | #define OLEDRENDERER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "animationdriver.h" 17 | 18 | class OledRenderer : public QObject 19 | { 20 | Q_OBJECT 21 | public: 22 | enum Status { 23 | NotRunning, 24 | Running 25 | }; 26 | 27 | explicit OledRenderer(QObject *parent = 0); 28 | 29 | ~OledRenderer(); 30 | 31 | void loadQmlFile(const QString &qmlFile, const QSize &size, qreal devicePixelRatio = 1.0, int fps = 24); 32 | 33 | bool isRunning(); 34 | 35 | QQuickItem * rootItem(); 36 | 37 | signals: 38 | void imageRendered(const QImage &image); 39 | 40 | private slots: 41 | void start(); 42 | void cleanup(); 43 | 44 | void createFbo(); 45 | void destroyFbo(); 46 | bool loadQml(const QString &qmlFile, const QSize &size); 47 | 48 | void renderNext(); 49 | 50 | private: 51 | QOpenGLContext *m_context; 52 | QOffscreenSurface *m_offscreenSurface; 53 | QQuickRenderControl *m_renderControl; 54 | QQuickWindow *m_quickWindow; 55 | QQmlEngine *m_qmlEngine; 56 | QQmlComponent *m_qmlComponent; 57 | QQuickItem *m_rootItem; 58 | QOpenGLFramebufferObject *m_fbo; 59 | qreal m_dpr; 60 | QSize m_size; 61 | AnimationDriver *m_animationDriver; 62 | 63 | Status m_status; 64 | int m_fps; 65 | QTimer *m_renderTimer; 66 | }; 67 | 68 | #endif // OLEDRENDERER_H 69 | -------------------------------------------------------------------------------- /qml-oled-renderer.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += qml quick 4 | CONFIG += c++11 5 | 6 | SOURCES += main.cpp \ 7 | oledrenderer.cpp \ 8 | animationdriver.cpp \ 9 | ui2c-ssd1306.c \ 10 | ssd1306driver.cpp 11 | 12 | HEADERS += \ 13 | oledrenderer.h \ 14 | animationdriver.h \ 15 | ssd1306driver.h 16 | 17 | # Additional import path used to resolve QML modules in Qt Creator's code model 18 | QML_IMPORT_PATH = 19 | 20 | # Additional import path used to resolve QML modules just for Qt Quick Designer 21 | QML_DESIGNER_IMPORT_PATH = 22 | 23 | # The following define makes your compiler emit warnings if you use 24 | # any feature of Qt which as been marked deprecated (the exact warnings 25 | # depend on your compiler). Please consult the documentation of the 26 | # deprecated API in order to know how to port your code away from it. 27 | DEFINES += QT_DEPRECATED_WARNINGS 28 | 29 | # You can also make your code fail to compile if you use deprecated APIs. 30 | # In order to do so, uncomment the following line. 31 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 32 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 33 | 34 | # Default rules for deployment. 35 | qnx: target.path = $$PREFIX/bin 36 | else: unix:!android: target.path = $$PREFIX/bin 37 | !isEmpty(target.path): INSTALLS += target 38 | 39 | -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | window.qml 5 | 6 | 7 | -------------------------------------------------------------------------------- /ssd1306driver.cpp: -------------------------------------------------------------------------------- 1 | #include "ssd1306driver.h" 2 | #include 3 | #include 4 | #include 5 | 6 | extern "C" { 7 | int i2c_open(int bus); 8 | int i2c_select(int file, int addr); 9 | int ssd1306_init(int file, int col, int line); 10 | int ssd1306_cls(int file, int col, int line); 11 | int i2c_write_data(int file, uint8_t data[], size_t len); 12 | } 13 | 14 | Ssd1306Driver::Ssd1306Driver(QObject *parent) 15 | : QObject(parent) 16 | , m_file(-1) 17 | { 18 | 19 | } 20 | 21 | bool Ssd1306Driver::openDevice(QSize size, int busId, int address) 22 | { 23 | m_file = i2c_open(busId); 24 | if (m_file < 0) { 25 | return false; 26 | } 27 | 28 | int res = i2c_select(m_file, address); 29 | if (res < 0) { 30 | return false; 31 | } 32 | 33 | ssd1306_init(m_file, size.width(), size.height()); 34 | ssd1306_cls(m_file, size.width(), size.height()); // SSD1306 may have a SRAM-based GDDRAM, some parts of the graphic are perserved after power cycle. 35 | 36 | m_size = size; 37 | 38 | return true; 39 | } 40 | 41 | void Ssd1306Driver::clearScreen() 42 | { 43 | if (m_file > -1) { 44 | ssd1306_cls(m_file, m_size.width(), m_size.height()); 45 | } 46 | } 47 | 48 | void Ssd1306Driver::close() 49 | { 50 | m_file = -1; // TODO: close file ? 51 | } 52 | 53 | void Ssd1306Driver::writeImage(const QImage &image) 54 | { 55 | const uint8_t SSD1306_CONT_DATA_HDR = 0x40; 56 | 57 | if (m_file < 0) { 58 | return; 59 | } 60 | 61 | int height = m_size.height() / 8; 62 | int width = m_size.width(); 63 | int len = height * width + 1; 64 | 65 | QVector vector(len); 66 | int pos = 0; 67 | vector[pos] = SSD1306_CONT_DATA_HDR; 68 | pos++; 69 | 70 | for (int y = 0; y < height; ++y) { 71 | for (int x = 0; x < width; ++x) { 72 | uint8_t pixel = 0u; 73 | for (int i = 0; i < 8; ++i) { 74 | pixel |= static_cast(image.pixelIndex(x, y * 8 + i) == 1) << i; 75 | } 76 | vector[pos] = pixel; 77 | pos++; 78 | } 79 | } 80 | 81 | i2c_write_data(m_file, vector.data(), static_cast(vector.size())); 82 | } 83 | -------------------------------------------------------------------------------- /ssd1306driver.h: -------------------------------------------------------------------------------- 1 | #ifndef SSD1306DRIVER_H 2 | #define SSD1306DRIVER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class Ssd1306Driver : public QObject 9 | { 10 | Q_OBJECT 11 | public: 12 | explicit Ssd1306Driver(QObject *parent = 0); 13 | 14 | bool openDevice(QSize size, int bus_id = 2, int address = 0x3c); 15 | void close(); 16 | 17 | public slots: 18 | void writeImage(const QImage &image); 19 | void clearScreen(); 20 | 21 | private: 22 | QSize m_size; 23 | int m_file; 24 | }; 25 | 26 | #endif // SSD1306DRIVER_H 27 | -------------------------------------------------------------------------------- /ui2c-ssd1306.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | /* 20 | * Driver is a adapted version of https://github.com/dword1511/ui2cutils 21 | * TODO: since malloc is involved, use valgrind to check memory leaks. 22 | * TODO: very high sys cpu usage. find a way to optimize. 23 | * TODO: fold consecutive calls into loops with array 24 | * TODO: try SMBus block write and quick write 25 | */ 26 | 27 | /* I2C functions */ 28 | 29 | #define SSD1306_DEVAD_A (0x3c) 30 | #define SSD1306_DEVAD_B (0x3d) 31 | #define SSD1306_CTRL_DATA (1<<6) 32 | #define SSD1306_CTRL_CMD (0<<6) 33 | #define SSD1306_CTRL_CONT (1<<7) 34 | 35 | int i2c_open(int bus) { 36 | const int fn_len = 20; 37 | char fn[fn_len]; 38 | int res, file; 39 | 40 | if (bus < 0) { 41 | return -EINVAL; 42 | } 43 | 44 | /* Open i2c-dev file */ 45 | snprintf(fn, fn_len, "/dev/i2c-%d", bus); 46 | if ((file = open(fn, O_RDWR)) < 0) { 47 | perror("open() failed (make sure i2c_dev is loaded and you have the permission)"); 48 | return file; 49 | } 50 | 51 | /* Query functions */ 52 | unsigned long funcs; 53 | if ((res = ioctl(file, I2C_FUNCS, &funcs)) < 0) { 54 | perror("ioctl() I2C_FUNCS failed"); 55 | return res; 56 | } 57 | fprintf(stdout, "Device: %s (", fn); 58 | if (funcs & I2C_FUNC_I2C) { 59 | fputs("I2C_FUNC_I2C ", stdout); 60 | } 61 | if (funcs & I2C_FUNC_SMBUS_BYTE) { 62 | fputs("I2C_FUNC_SMBUS_BYTE ", stdout); 63 | } 64 | fputs("\b)\n", stdout); 65 | fflush(stdout); 66 | 67 | return file; 68 | } 69 | 70 | int i2c_select(int file, int addr) { 71 | /* addr in [0x00, 0x7f] */ 72 | int res; 73 | 74 | if ((res = ioctl(file, I2C_SLAVE, addr)) < 0) { 75 | perror("ioctl() I2C_SLAVE failed"); 76 | } 77 | 78 | return res; 79 | } 80 | 81 | /****************************************************************************** 82 | * Device is mostly write-only. 83 | * Frame format: address control data 84 | * Control: CONT D/C 0 0 0 0 0 0 85 | * Data : Encapsulated 8-bit command, parameter or data 86 | * Repeat with CONT set until all command and data are sent. 87 | *****************************************************************************/ 88 | 89 | int i2c_write_cmd_1b(int file, uint8_t cmd) { 90 | int res; 91 | uint8_t buf[2] = {SSD1306_CTRL_CMD, cmd}; 92 | 93 | if ((res = (int)write(file, buf, 2)) < 0) { 94 | perror("write() command failed"); 95 | return res; 96 | } 97 | 98 | return 0; 99 | } 100 | 101 | #define SSD1306_CONT_DATA_HDR (0x40) 102 | /* To avoid copying, caller should prepare the header. */ 103 | int i2c_write_data(int file, uint8_t data[], size_t len) { 104 | int res; 105 | 106 | if (NULL == data) { 107 | return -EINVAL; 108 | } 109 | if (SSD1306_CONT_DATA_HDR != data[0]) { 110 | return -EINVAL; 111 | } 112 | 113 | if ((res = (int)write(file, data, len)) < 0) { 114 | perror("write() data failed"); 115 | return res; 116 | } 117 | 118 | return 0; 119 | } 120 | 121 | int i2c_read_byte(int file, uint8_t *data) { 122 | if (NULL == data) { 123 | return -EFAULT; 124 | } 125 | 126 | int res; 127 | 128 | if ((res = (int)read(file, data, 1)) < 0) { 129 | perror("read() data failed"); 130 | return res; 131 | } 132 | 133 | return 0; 134 | } 135 | 136 | /* Device functions */ 137 | /* Due to the complicated and variable command structure, use functions instead of macros. */ 138 | 139 | int ssd1306_set_contrast(int file, uint8_t contrast) { 140 | int res; 141 | 142 | if ((res = i2c_write_cmd_1b(file, 0x80)) < 0 ) { 143 | return res; 144 | } 145 | if ((res = i2c_write_cmd_1b(file, contrast)) < 0) { 146 | return res; 147 | } 148 | 149 | return 0; 150 | } 151 | 152 | int ssd1306_reset_contrast(int file) { 153 | return ssd1306_set_contrast(file, 0x7f); 154 | } 155 | 156 | int ssd1306_set_display_test(int file, bool enable) { 157 | return i2c_write_cmd_1b(file, enable ? 0xa5 : 0xa4); 158 | } 159 | 160 | int ssd1306_reset_display_test(int file) { 161 | return ssd1306_set_display_test(file, false); 162 | } 163 | 164 | int ssd1306_set_inverse(int file, bool enable) { 165 | return i2c_write_cmd_1b(file, enable ? 0xa7 : 0xa6); 166 | } 167 | 168 | int ssd1306_reset_inverse(int file) { 169 | return ssd1306_set_inverse(file, false); 170 | } 171 | 172 | int ssd1306_set_power(int file, bool enable) { 173 | return i2c_write_cmd_1b(file, enable ? 0xaf : 0xae); 174 | } 175 | 176 | int ssd1306_reset_power(int file) { 177 | return ssd1306_set_power(file, false); 178 | } 179 | 180 | int ssd1306_interval_to_param(int interval, uint8_t *param) { 181 | if (NULL == param) { 182 | return -EFAULT; 183 | } 184 | 185 | switch (interval) { 186 | case 2: { 187 | *param = 0x07; 188 | return 0; 189 | } 190 | case 3: { 191 | *param = 0x04; 192 | return 0; 193 | } 194 | case 4: { 195 | *param = 0x05; 196 | return 0; 197 | } 198 | case 5: { 199 | *param = 0x00; 200 | return 0; 201 | } 202 | case 25: { 203 | *param = 0x06; 204 | return 0; 205 | } 206 | case 64: { 207 | *param = 0x01; 208 | return 0; 209 | } 210 | case 128: { 211 | *param = 0x02; 212 | return 0; 213 | } 214 | case 256: { 215 | *param = 0x03; 216 | return 0; 217 | } 218 | 219 | default: { 220 | return -EINVAL; 221 | } 222 | } 223 | } 224 | 225 | int ssd1306_setup_horiz_scroll(int file, bool left, uint8_t start_page, uint8_t end_page, int interval) { 226 | int res; 227 | 228 | if ((start_page > 0x07) || (end_page > 0x07) || (start_page > end_page)) { 229 | return -EINVAL; 230 | } 231 | 232 | uint8_t interval_param; 233 | if ((res = ssd1306_interval_to_param(interval, &interval_param)) < 0) { 234 | return res; 235 | } 236 | 237 | if ((res = i2c_write_cmd_1b(file, left ? 0x27 : 0x26)) < 0 ) { 238 | return res; 239 | } 240 | if ((res = i2c_write_cmd_1b(file, 0x00)) < 0 ) { 241 | return res; 242 | } 243 | if ((res = i2c_write_cmd_1b(file, start_page)) < 0 ) { 244 | return res; 245 | } 246 | if ((res = i2c_write_cmd_1b(file, interval_param)) < 0 ) { 247 | return res; 248 | } 249 | if ((res = i2c_write_cmd_1b(file, end_page)) < 0 ) { 250 | return res; 251 | } 252 | if ((res = i2c_write_cmd_1b(file, 0x00)) < 0 ) { 253 | return res; 254 | } 255 | if ((res = i2c_write_cmd_1b(file, 0xff)) < 0 ) { 256 | return res; 257 | } 258 | 259 | return 0; 260 | } 261 | 262 | int ssd1306_setup_scroll(int file, bool left, uint8_t start_page, uint8_t end_page, int interval, uint8_t vertical_offset) { 263 | int res; 264 | 265 | /* 266 | * NOTE: this is horizontal + vertical scroll mode. 267 | * There is no vertical-only scroll mode. 268 | */ 269 | 270 | if ((start_page > 0x07) || (end_page > 0x07) || (start_page > end_page)) { 271 | return -EINVAL; 272 | } 273 | 274 | if (vertical_offset > 0x3f) { 275 | return -EINVAL; 276 | } 277 | 278 | uint8_t interval_param; 279 | if ((res = ssd1306_interval_to_param(interval, &interval_param)) < 0) { 280 | return res; 281 | } 282 | 283 | if ((res = i2c_write_cmd_1b(file, left ? 0x2a : 0x29)) < 0 ) { 284 | return res; 285 | } 286 | if ((res = i2c_write_cmd_1b(file, 0x00)) < 0 ) { 287 | return res; 288 | } 289 | if ((res = i2c_write_cmd_1b(file, start_page)) < 0 ) { 290 | return res; 291 | } 292 | if ((res = i2c_write_cmd_1b(file, interval_param)) < 0 ) { 293 | return res; 294 | } 295 | if ((res = i2c_write_cmd_1b(file, end_page)) < 0 ) { 296 | return res; 297 | } 298 | if ((res = i2c_write_cmd_1b(file, vertical_offset)) < 0 ) { 299 | return res; 300 | } 301 | 302 | return 0; 303 | } 304 | 305 | int ssd1306_set_scroll(int file, bool enable) { 306 | /* 307 | * NOTE: after disabling the scrolling, "the ram data needs to be 308 | * rewritten." 309 | * The lastest scrolling setting will take effect once scrolling is enabled. 310 | */ 311 | 312 | return i2c_write_cmd_1b(file, enable ? 0x2f : 0x2e); 313 | } 314 | 315 | int ssd1306_set_vertical_scroll_area(int file, uint8_t row_title, uint8_t roll_scroll) { 316 | int res; 317 | 318 | /* 319 | * NOTE: additional constraints apply, hardware may reject without notice. 320 | * row_title + roll_scroll < MUX_RATIO 321 | * vertical_offset < roll_scroll 322 | * display_start < roll_scroll 323 | * 324 | * row_title: fixed roll of pixels on the top that will not scroll. 325 | * roll_scroll: roll of pixels that are scrolling. 326 | * (any leftovers will become fixed bottom roll of pixels.) 327 | */ 328 | 329 | if ((row_title > 0x3f) || (roll_scroll > 0x7f)) { 330 | return -EINVAL; 331 | } 332 | 333 | if ((res = i2c_write_cmd_1b(file, 0xa3)) < 0 ) { 334 | return res; 335 | } 336 | if ((res = i2c_write_cmd_1b(file, row_title)) < 0 ) { 337 | return res; 338 | } 339 | if ((res = i2c_write_cmd_1b(file, roll_scroll)) < 0 ) { 340 | return res; 341 | } 342 | 343 | return 0; 344 | } 345 | 346 | int ssd1306_reset_vertical_scroll_area(int file) { 347 | return ssd1306_set_vertical_scroll_area(file, 0, 64); 348 | } 349 | 350 | int ssd1306_set_col_start(int file, uint8_t col) { 351 | int res; 352 | 353 | /* 354 | * NOTE: this is a 2-step process requiring splitting the parameter into high 355 | * and low half-byte and embedding the parameter in 2 commands and sending 356 | * them. 357 | * For page addressing mode only. 358 | */ 359 | 360 | if ((res = i2c_write_cmd_1b(file, 0x00 | (col & 0x0f))) < 0 ) { 361 | return res; 362 | } 363 | if ((res = i2c_write_cmd_1b(file, 0x01 | (col > 4))) < 0 ) { 364 | return res; 365 | } 366 | 367 | return 0; 368 | } 369 | 370 | int ssd1306_reset_col_start(int file) { 371 | return ssd1306_set_col_start(file, 0); 372 | } 373 | 374 | #define SSD1306_MEMMODE_H (0x00) /* Horizontally placed 1x8 blocks, not pixels! */ 375 | #define SSD1306_MEMMODE_V (0x01) 376 | #define SSD1306_MEMMODE_PAGE (0x02) 377 | 378 | int ssd1306_set_mem_addr_mode(int file, uint8_t mode) { 379 | int res; 380 | 381 | if (mode > 0x02) { 382 | return -EINVAL; 383 | } 384 | 385 | if ((res = i2c_write_cmd_1b(file, 0x20)) < 0 ) { 386 | return res; 387 | } 388 | if ((res = i2c_write_cmd_1b(file, mode)) < 0 ) { 389 | return res; 390 | } 391 | 392 | return 0; 393 | } 394 | 395 | int ssd1306_reset_mem_addr_mode(int file) { 396 | return ssd1306_set_mem_addr_mode(file, SSD1306_MEMMODE_PAGE); 397 | } 398 | 399 | int ssd1306_set_col_addr(int file, uint8_t start, uint8_t end) { 400 | int res; 401 | 402 | /* NOTE: for horizontal or vertical addressing mode only. */ 403 | if ((start > 0x7f) || (end > 0x7f) || (start > end)) { 404 | return -EINVAL; 405 | } 406 | 407 | if ((res = i2c_write_cmd_1b(file, 0x21)) < 0 ) { 408 | return res; 409 | } 410 | if ((res = i2c_write_cmd_1b(file, start)) < 0 ) { 411 | return res; 412 | } 413 | if ((res = i2c_write_cmd_1b(file, end)) < 0 ) { 414 | return res; 415 | } 416 | 417 | return 0; 418 | } 419 | 420 | int ssd1306_reset_col_addr(int file) { 421 | return ssd1306_set_col_addr(file, 0, 127); 422 | } 423 | 424 | int ssd1306_set_page_addr(int file, uint8_t start, uint8_t end) { 425 | int res; 426 | 427 | /* 428 | * NOTE: "for horizontal or vertical addressing mode", or should be for page 429 | * mode? 430 | */ 431 | if ((start > 0x07) || (end > 0x07) || (start > end)) { 432 | return -EINVAL; 433 | } 434 | 435 | if ((res = i2c_write_cmd_1b(file, 0x22)) < 0 ) { 436 | return res; 437 | } 438 | if ((res = i2c_write_cmd_1b(file, start)) < 0 ) { 439 | return res; 440 | } 441 | if ((res = i2c_write_cmd_1b(file, end)) < 0 ) { 442 | return res; 443 | } 444 | 445 | return 0; 446 | } 447 | 448 | int ssd1306_reset_page_addr(int file) { 449 | return ssd1306_set_page_addr(file, 0, 7); 450 | } 451 | 452 | int ssd1306_set_page_start(int file, uint8_t page) { 453 | /* NOTE: "set GDDRAM page start address", for page addressing mode only. */ 454 | 455 | if (page > 0x07) { 456 | return -EINVAL; 457 | } 458 | 459 | return i2c_write_cmd_1b(file, 0xb0 | (page & 0x07)); 460 | } 461 | 462 | int ssd1306_set_start_line(int file, uint8_t line) { 463 | if (line > 0x3f) { 464 | return -EINVAL; 465 | } 466 | 467 | return i2c_write_cmd_1b(file, 0x40 | (line & 0x3f)); 468 | } 469 | 470 | int ssd1306_reset_start_line(int file) { 471 | return ssd1306_set_start_line(file, 0); 472 | } 473 | 474 | int ssd1306_set_segment_remap(int file, bool reverse) { 475 | /* NOTE: normal = col 0 is seg 0, reverse = col 127 is seg 0. */ 476 | 477 | return i2c_write_cmd_1b(file, reverse ? 0xa1 : 0xa0); 478 | } 479 | 480 | int ssd1306_reset_segment_remap(int file) { 481 | return ssd1306_set_segment_remap(file, false); 482 | } 483 | 484 | int ssd1306_set_mux_ratio(int file, int ratio) { 485 | int res; 486 | 487 | /* NOTE: controlled by how many line (COM) your display has. */ 488 | 489 | if (ratio > 0x40) { 490 | return -EINVAL; 491 | } 492 | 493 | if ((res = i2c_write_cmd_1b(file, 0xa8)) < 0 ) { 494 | return res; 495 | } 496 | if ((res = i2c_write_cmd_1b(file, (ratio - 1) & 0x3f)) < 0 ) { 497 | return res; 498 | } 499 | 500 | return 0; 501 | } 502 | 503 | int ssd1306_reset_mux_ratio(int file) { 504 | return ssd1306_set_mux_ratio(file, 64); 505 | } 506 | 507 | int ssd1306_set_com_scan(int file, bool reverse) { 508 | /* NOTE: normal = line 0 is com 0, reverse = line (mux_ratio - 1) is com 0. */ 509 | 510 | return i2c_write_cmd_1b(file, reverse ? 0xc8 : 0xc0); 511 | } 512 | 513 | int ssd1306_reset_com_scan(int file) { 514 | return ssd1306_set_com_scan(file, false); 515 | } 516 | 517 | int ssd1306_set_display_offset(int file, uint8_t offset) { 518 | int res; 519 | 520 | /* NOTE: start display on line . */ 521 | 522 | if (offset > 0x3f) { 523 | return -EINVAL; 524 | } 525 | 526 | if ((res = i2c_write_cmd_1b(file, 0xd3)) < 0 ) { 527 | return res; 528 | } 529 | if ((res = i2c_write_cmd_1b(file, offset)) < 0 ) { 530 | return res; 531 | } 532 | 533 | return 0; 534 | } 535 | 536 | int ssd1306_reset_display_offset(int file) { 537 | return ssd1306_set_display_offset(file, 0); 538 | } 539 | 540 | int ssd1306_set_com_pin(int file, bool alternate, bool remap) { 541 | int res; 542 | 543 | /* NOTE: highly hardware-specific. */ 544 | 545 | if ((res = i2c_write_cmd_1b(file, 0xda)) < 0 ) { 546 | return res; 547 | } 548 | if ((res = i2c_write_cmd_1b(file, 0x02 | (alternate ? 0x10 : 0x00) | (remap ? 0x20 : 0x00))) < 0 ) { 549 | return res; 550 | } 551 | 552 | return 0; 553 | } 554 | 555 | int ssd1306_reset_com_pin(int file) { 556 | return ssd1306_set_com_pin(file, true, false); 557 | } 558 | 559 | int ssd1306_set_clkdiv(int file, uint8_t ratio, uint8_t fosc) { 560 | int res; 561 | 562 | if ((ratio > 0x10) || (0 == ratio) || (fosc > 0x0f)) { 563 | return -EINVAL; 564 | } 565 | 566 | if ((res = i2c_write_cmd_1b(file, 0xd5)) < 0 ) { 567 | return res; 568 | } 569 | if ((res = i2c_write_cmd_1b(file, (fosc << 4) | (ratio - 1))) < 0 ) { 570 | return res; 571 | } 572 | 573 | return 0; 574 | } 575 | 576 | int ssd1306_reset_clkdiv(int file) { 577 | return ssd1306_set_clkdiv(file, 1, 8); 578 | } 579 | 580 | int ssd1306_set_precharge(int file, uint8_t phase1, uint8_t phase2) { 581 | int res; 582 | 583 | /* NOTE: phase1 and phase2 has unit of clock cycles. */ 584 | if ((0 == phase1) || (0 == phase2) || (phase1 > 0x0f) || (phase1 > 0x0f)) { 585 | return -EINVAL; 586 | } 587 | 588 | if ((res = i2c_write_cmd_1b(file, 0xd9)) < 0 ) { 589 | return res; 590 | } 591 | if ((res = i2c_write_cmd_1b(file, (phase2 << 4) | phase1)) < 0 ) { 592 | return res; 593 | } 594 | 595 | return 0; 596 | } 597 | 598 | int ssd1306_reset_precharge(int file) { 599 | return ssd1306_set_precharge(file, 2, 2); 600 | } 601 | 602 | #define SSD1306_VCOMH_LEVEL_650MV 0 603 | #define SSD1306_VCOMH_LEVEL_770MV 2 604 | #define SSD1306_VCOMH_LEVEL_830MV 3 605 | 606 | int ssd1306_set_vcomh_desel(int file, uint8_t level_code) { 607 | int res; 608 | 609 | /* 610 | * NOTE: although datasheet only gives voltages for 3 configurations, all 611 | * from 0~7 are possible. 612 | */ 613 | 614 | if (level_code > 0x07) { 615 | return -EINVAL; 616 | } 617 | 618 | if ((res = i2c_write_cmd_1b(file, 0xdb)) < 0 ) { 619 | return res; 620 | } 621 | if ((res = i2c_write_cmd_1b(file, (level_code & 0x07) << 4)) < 0 ) { 622 | return res; 623 | } 624 | 625 | return 0; 626 | } 627 | 628 | int ssd1306_reset_vcomh_desel(int file) { 629 | return ssd1306_set_vcomh_desel(file, SSD1306_VCOMH_LEVEL_770MV); 630 | } 631 | 632 | int ssd1306_send_nop(int file) { 633 | return i2c_write_cmd_1b(file, 0xe3); 634 | } 635 | 636 | /* 637 | * NOTE: the following 3 are added in the new versions of the datasheet, 638 | * however, the charge pump enable is essential for most modules to operate. 639 | */ 640 | int ssd1306_set_fade(int file, bool fade_out, bool fade_in, uint8_t fade_interval) { 641 | int res; 642 | 643 | if (fade_interval > 128) { 644 | return -EINVAL; 645 | } 646 | if (fade_interval < 8) { 647 | fade_interval = 8; 648 | } 649 | 650 | if ((res = i2c_write_cmd_1b(file, 0x23)) < 0 ) { 651 | return res; 652 | } 653 | if ((res = i2c_write_cmd_1b(file, (fade_out ? 0x20 : 0x00) | (fade_in ? 0x10 : 0x00) | ((fade_interval / 8 - 1) & 0x0f))) < 0 ) { 654 | return res; 655 | } 656 | 657 | return 0; 658 | } 659 | 660 | int ssd1306_reset_fade(int file) { 661 | /* NOTE: default does not include fade_interval. */ 662 | 663 | return ssd1306_set_fade(file, false, false, 8); 664 | } 665 | 666 | int ssd1306_set_zoom(int file, bool enable) { 667 | int res; 668 | 669 | /* NOTE: for panels in alternate COM configuration only. */ 670 | 671 | if ((res = i2c_write_cmd_1b(file, 0xd6)) < 0 ) { 672 | return res; 673 | } 674 | if ((res = i2c_write_cmd_1b(file, enable ? 0x01 : 0x00)) < 0 ) { 675 | return res; 676 | } 677 | 678 | return 0; 679 | } 680 | 681 | int ssd1306_reset_zoom(int file) { 682 | return ssd1306_set_zoom(file, false); 683 | } 684 | 685 | int ssd1306_set_charge_pump(int file, bool enable) { 686 | int res; 687 | 688 | if ((res = i2c_write_cmd_1b(file, 0x8d)) < 0 ) { 689 | return res; 690 | } 691 | if ((res = i2c_write_cmd_1b(file, 0x10 | (enable ? 0x04 : 0x00))) < 0 ) { 692 | return res; 693 | } 694 | 695 | return 0; 696 | } 697 | 698 | int ssd1306_reset_charge_pump(int file) { 699 | return ssd1306_set_charge_pump(file, false); 700 | } 701 | 702 | #define SSD1306_STATUS_DISP_OFF (1 << 6) 703 | /* NOTE: all other bits in the reg are reserved. */ 704 | int ssd1306_read_status(int file, uint8_t *reg) { 705 | return i2c_read_byte(file, reg); 706 | } 707 | 708 | /* NOTE: "No data read is provided in serial mode operation." */ 709 | 710 | int ssd1306_soft_reset(int file) { 711 | int res, i; 712 | 713 | /* 714 | * Longest command has 6 parameters. 715 | * Send 6 NOPs to finish any currently pending command. 716 | */ 717 | 718 | for (i = 0; i < 6; i ++) { 719 | if ((res = ssd1306_send_nop(file)) < 0 ) { 720 | return res; 721 | } 722 | } 723 | 724 | /* Fundamentals. TODO: consider sequence. */ 725 | if ((res = ssd1306_reset_power(file)) < 0 ) { 726 | return res; 727 | } 728 | if ((res = ssd1306_reset_charge_pump(file)) < 0 ) { 729 | return res; 730 | } 731 | if ((res = ssd1306_reset_contrast(file)) < 0 ) { 732 | return res; 733 | } 734 | if ((res = ssd1306_reset_display_test(file)) < 0 ) { 735 | return res; 736 | } 737 | if ((res = ssd1306_reset_inverse(file)) < 0 ) { 738 | return res; 739 | } 740 | 741 | /* 742 | * Scrolling. 743 | * NOTE: Scrolling parameters are not reset. 744 | * Assuming scrolling is disabled after POR. 745 | */ 746 | if ((res = ssd1306_reset_vertical_scroll_area(file)) < 0 ) { 747 | return res; 748 | } 749 | if ((res = ssd1306_set_scroll(file, false)) < 0 ) { 750 | return res; 751 | } 752 | 753 | /* Addressing */ 754 | if ((res = ssd1306_reset_col_start(file)) < 0 ) { 755 | return res; 756 | } 757 | if ((res = ssd1306_reset_mem_addr_mode(file)) < 0 ) { 758 | return res; 759 | } 760 | if ((res = ssd1306_reset_col_addr(file)) < 0 ) { 761 | return res; 762 | } 763 | if ((res = ssd1306_reset_page_addr(file)) < 0 ) { 764 | return res; 765 | } 766 | 767 | /* Hardware */ 768 | if ((res = ssd1306_reset_start_line(file)) < 0 ) { 769 | return res; 770 | } 771 | if ((res = ssd1306_reset_segment_remap(file)) < 0 ) { 772 | return res; 773 | } 774 | if ((res = ssd1306_reset_mux_ratio(file)) < 0 ) { 775 | return res; 776 | } 777 | if ((res = ssd1306_reset_com_scan(file)) < 0 ) { 778 | return res; 779 | } 780 | if ((res = ssd1306_reset_display_offset(file)) < 0 ) { 781 | return res; 782 | } 783 | if ((res = ssd1306_reset_com_pin(file)) < 0 ) { 784 | return res; 785 | } 786 | 787 | /* Clocking */ 788 | if ((res = ssd1306_reset_clkdiv(file)) < 0 ) { 789 | return res; 790 | } 791 | if ((res = ssd1306_reset_precharge(file)) < 0 ) { 792 | return res; 793 | } 794 | if ((res = ssd1306_reset_vcomh_desel(file)) < 0 ) { 795 | return res; 796 | } 797 | 798 | /* VFX */ 799 | if ((res = ssd1306_reset_fade(file)) < 0 ) { 800 | return res; 801 | } 802 | if ((res = ssd1306_reset_zoom(file)) < 0 ) { 803 | return res; 804 | } 805 | 806 | return 0; 807 | } 808 | 809 | int ssd1306_init(int file, int col, int line) { 810 | int res; 811 | 812 | /* NOTE: use defualts whenever we can. is not used. */ 813 | 814 | if ((line <= 0) || (col <= 0)) { 815 | return -EINVAL; 816 | } 817 | if ((line > 64) || (col > 128)) { 818 | return -EINVAL; 819 | } 820 | if (((line % 8) != 0) || ((col % 8) != 0)) { 821 | return -EINVAL; 822 | } 823 | 824 | if ((res = ssd1306_soft_reset(file)) < 0 ) { 825 | return res; 826 | } 827 | 828 | /* Should be already off, just ensuring. */ 829 | if ((res = ssd1306_set_power(file, false)) < 0 ) { 830 | return res; 831 | } 832 | if ((res = ssd1306_set_mux_ratio(file, line)) < 0 ) { 833 | return res; 834 | } 835 | if ((res = ssd1306_set_mem_addr_mode(file, SSD1306_MEMMODE_H)) < 0 ) { 836 | return res; 837 | } 838 | if ((res = ssd1306_set_segment_remap(file, true)) < 0 ) { 839 | return res; 840 | } 841 | if ((res = ssd1306_set_com_scan(file, true)) < 0 ) { 842 | return res; 843 | } 844 | 845 | if ((res = ssd1306_set_charge_pump(file, true)) < 0 ) { 846 | return res; 847 | } 848 | if ((res = ssd1306_set_power(file, true)) < 0 ) { 849 | return res; 850 | } 851 | 852 | return 0; 853 | } 854 | 855 | int ssd1306_cls(int file, int col, int line) { 856 | int res; 857 | uint8_t *buf = NULL; 858 | const size_t len = (size_t)line * (size_t)col / 8 + 1; 859 | 860 | if ((line <= 0) || (col <= 0)) { 861 | return -EINVAL; 862 | } 863 | if ((line > 64) || (col > 128)) { 864 | return -EINVAL; 865 | } 866 | if (((line % 8) != 0) || ((col % 8) != 0)) { 867 | return -EINVAL; 868 | } 869 | 870 | buf = (uint8_t *)malloc(len); 871 | if (NULL == buf) { 872 | perror("malloc"); 873 | return -ENOMEM; 874 | } 875 | 876 | bzero(buf, len); 877 | buf[0] = SSD1306_CONT_DATA_HDR; 878 | res = i2c_write_data(file, buf, len); 879 | 880 | free(buf); 881 | return res; 882 | } 883 | -------------------------------------------------------------------------------- /window.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Window 2.0 3 | 4 | Window { 5 | id: window 6 | width: 128 7 | height: 64 8 | visible: true 9 | 10 | Loader { 11 | source: "main.qml" 12 | } 13 | } 14 | --------------------------------------------------------------------------------