root/sys/dev/i2c/ssdfb_i2c.c
/* $NetBSD: ssdfb_i2c.c,v 1.11 2021/08/05 22:31:20 tnn Exp $ */

/*
 * Copyright (c) 2019 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Tobias Nygren.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: ssdfb_i2c.c,v 1.11 2021/08/05 22:31:20 tnn Exp $");

#include <sys/param.h>
#include <sys/device.h>
#include <dev/wscons/wsdisplayvar.h>
#include <dev/rasops/rasops.h>
#include <dev/i2c/i2cvar.h>
#include <dev/ic/ssdfbvar.h>

struct ssdfb_i2c_softc {
        struct          ssdfb_softc sc;
        i2c_tag_t       sc_i2c_tag;
        i2c_addr_t      sc_i2c_addr;
        size_t          sc_transfer_size;
};

static int      ssdfb_i2c_match(device_t, cfdata_t, void *);
static void     ssdfb_i2c_attach(device_t, device_t, void *);
static int      ssdfb_i2c_detach(device_t, int);

static int      ssdfb_i2c_probe_transfer_size(struct ssdfb_i2c_softc *, bool);
static int      ssdfb_i2c_transfer(struct ssdfb_i2c_softc *, uint8_t, uint8_t *,
                                size_t, int);
static int      ssdfb_i2c_cmd(void *, uint8_t *, size_t, bool);
static int      ssdfb_i2c_transfer_rect(void *, uint8_t, uint8_t, uint8_t,
                                uint8_t, uint8_t *, size_t, bool);
static int      ssdfb_i2c_transfer_rect_ssd1306(void *, uint8_t, uint8_t,
                                uint8_t, uint8_t, uint8_t *, size_t, bool);
static int      ssdfb_i2c_transfer_rect_sh1106(void *, uint8_t, uint8_t,
                                uint8_t, uint8_t, uint8_t *, size_t, bool);
static int      ssdfb_smbus_transfer_rect(void *, uint8_t, uint8_t, uint8_t,
                                uint8_t, uint8_t *, size_t, bool);

CFATTACH_DECL_NEW(ssdfb_iic, sizeof(struct ssdfb_i2c_softc),
    ssdfb_i2c_match, ssdfb_i2c_attach, ssdfb_i2c_detach, NULL);

static const struct device_compatible_entry compat_data[] = {
        { .compat = "solomon,ssd1306fb-i2c",
          .value = SSDFB_PRODUCT_SSD1306_GENERIC },

        { .compat = "sino,sh1106fb-i2c",
          .value = SSDFB_PRODUCT_SH1106_GENERIC },

        DEVICE_COMPAT_EOL
};

static int
ssdfb_i2c_match(device_t parent, cfdata_t match, void *aux)
{
        struct i2c_attach_args *ia = aux;
        int match_result;

        if (iic_use_direct_match(ia, match, compat_data, &match_result))
                return match_result;

        switch (ia->ia_addr) {
        case SSDFB_I2C_DEFAULT_ADDR:
        case SSDFB_I2C_ALTERNATIVE_ADDR:
                return I2C_MATCH_ADDRESS_ONLY;
        }

        return 0;
}

static void
ssdfb_i2c_attach(device_t parent, device_t self, void *aux)
{
        struct ssdfb_i2c_softc *sc = device_private(self);
        struct cfdata *cf = device_cfdata(self);
        struct i2c_attach_args *ia = aux;
        int flags = cf->cf_flags;

        if ((flags & SSDFB_ATTACH_FLAG_PRODUCT_MASK) == SSDFB_PRODUCT_UNKNOWN) {
                const struct device_compatible_entry *dce =
                    iic_compatible_lookup(ia, compat_data);
                if (dce != NULL) {
                        flags |= (int)dce->value;
                }
        }
        if ((flags & SSDFB_ATTACH_FLAG_PRODUCT_MASK) == SSDFB_PRODUCT_UNKNOWN)
                flags |= SSDFB_PRODUCT_SSD1306_GENERIC;

        flags |= SSDFB_ATTACH_FLAG_MPSAFE;
        sc->sc.sc_dev = self;
        sc->sc_i2c_tag = ia->ia_tag;
        sc->sc_i2c_addr = ia->ia_addr;
        sc->sc.sc_cookie = (void *)sc;
        sc->sc.sc_cmd = ssdfb_i2c_cmd;
        sc->sc.sc_transfer_rect = ssdfb_i2c_transfer_rect;

        ssdfb_attach(&sc->sc, flags);
}

static int
ssdfb_i2c_detach(device_t self, int flags)
{
        struct ssdfb_i2c_softc *sc = device_private(self);

        return ssdfb_detach(&sc->sc);
}

static int
ssdfb_i2c_probe_transfer_size(struct ssdfb_i2c_softc *sc, bool usepoll)
{
        int flags = usepoll ? I2C_F_POLL : 0;
        uint8_t cb = SSDFB_I2C_CTRL_BYTE_DATA_MASK;
        int error;
        uint8_t buf[128];
        size_t len;

        error = iic_acquire_bus(sc->sc_i2c_tag, flags);
        if (error)
                return error;
        len = sizeof(buf);
        memset(buf, 0, len);
        while (len > 0) {
                error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                    sc->sc_i2c_addr, &cb, sizeof(cb), buf, len, flags);
                if (!error) {
                        break;
                }
                len >>= 1;
        }
        if (!error && len < 2) {
                error = E2BIG;
        } else {
                sc->sc_transfer_size = len;
        }
        (void) iic_release_bus(sc->sc_i2c_tag, flags);

        return error;
}

static int
ssdfb_i2c_transfer(struct ssdfb_i2c_softc *sc, uint8_t cb, uint8_t *data,
                   size_t len, int flags)
{
        int error;
        size_t xfer_size = sc->sc_transfer_size;

        while (len >= xfer_size) {
                error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                    sc->sc_i2c_addr, &cb, sizeof(cb), data, xfer_size, flags);
                if (error)
                        return error;
                len -= xfer_size;
                data += xfer_size;
        }
        if (len > 0) {
                error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                    sc->sc_i2c_addr, &cb, sizeof(cb), data, len, flags);
        }

        return error;
}

static int
ssdfb_i2c_cmd(void *cookie, uint8_t *cmd, size_t len, bool usepoll)
{
        struct ssdfb_i2c_softc *sc = (struct ssdfb_i2c_softc *)cookie;
        int flags = usepoll ? I2C_F_POLL : 0;
        uint8_t cb = 0;
        int error;

        error = iic_acquire_bus(sc->sc_i2c_tag, flags);
        if (error)
                return error;
        error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
            sc->sc_i2c_addr, &cb, sizeof(cb), cmd, len, flags);
        (void) iic_release_bus(sc->sc_i2c_tag, flags);

        return error;
}

static int
ssdfb_i2c_transfer_rect(void *cookie, uint8_t fromcol, uint8_t tocol,
    uint8_t frompage, uint8_t topage, uint8_t *p, size_t stride, bool usepoll)
{
        struct ssdfb_i2c_softc *sc = (struct ssdfb_i2c_softc *)cookie;
        uint8_t cmd[2];
        int error;

        /*
         * Test if large transfers are supported by the parent i2c bus and
         * pick the fastest transfer routine for subsequent invocations.
         */
        switch (sc->sc.sc_p->p_controller_id) {
        case SSDFB_CONTROLLER_SSD1306:
                sc->sc.sc_transfer_rect = ssdfb_i2c_transfer_rect_ssd1306;
                break;
        case SSDFB_CONTROLLER_SH1106:
                sc->sc.sc_transfer_rect = ssdfb_i2c_transfer_rect_sh1106;
                break;
        default:
                sc->sc.sc_transfer_rect = ssdfb_smbus_transfer_rect;
                break;
        }

        if (sc->sc.sc_transfer_rect != ssdfb_smbus_transfer_rect) {
                error = ssdfb_i2c_probe_transfer_size(sc, usepoll);
                if (error)
                        return error;
                aprint_verbose_dev(sc->sc.sc_dev, "%zd-byte transfers\n",
                    sc->sc_transfer_size);
                if (sc->sc_transfer_size == 2) {
                        sc->sc.sc_transfer_rect = ssdfb_smbus_transfer_rect;
                }
        }

        /*
         * Set addressing mode for SSD1306.
         */
        if (sc->sc.sc_p->p_controller_id == SSDFB_CONTROLLER_SSD1306) {
                cmd[0] = SSD1306_CMD_SET_MEMORY_ADDRESSING_MODE;
                cmd[1] = sc->sc.sc_transfer_rect
                    == ssdfb_i2c_transfer_rect_ssd1306
                    ? SSD1306_MEMORY_ADDRESSING_MODE_HORIZONTAL
                    : SSD1306_MEMORY_ADDRESSING_MODE_PAGE;
                error = ssdfb_i2c_cmd(cookie, cmd, sizeof(cmd), usepoll);
                if (error)
                        return error;
        }

        return sc->sc.sc_transfer_rect(cookie, fromcol, tocol, frompage, topage,
            p, stride, usepoll);
}


static int
ssdfb_i2c_transfer_rect_ssd1306(void *cookie, uint8_t fromcol, uint8_t tocol,
    uint8_t frompage, uint8_t topage, uint8_t *p, size_t stride, bool usepoll)
{
        struct ssdfb_i2c_softc *sc = (struct ssdfb_i2c_softc *)cookie;
        int flags = usepoll ? I2C_F_POLL : 0;
        uint8_t cc = 0;
        uint8_t cb = SSDFB_I2C_CTRL_BYTE_DATA_MASK;
        size_t len = tocol + 1 - fromcol;
        int error;
        /*
         * SSD1306 does not implement the Continuation bit correctly.
         * The SH1106 protocol defines that a control byte WITH Co
         * set must be inserted between each command. But SSD1306
         * fails to parse the commands if we do that.
         */
        uint8_t cmds[] = {
                SSD1306_CMD_SET_COLUMN_ADDRESS,
                fromcol, tocol,
                SSD1306_CMD_SET_PAGE_ADDRESS,
                frompage, topage
        };

        error = iic_acquire_bus(sc->sc_i2c_tag, flags);
        if (error)
                return error;
        error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
            sc->sc_i2c_addr, &cc, sizeof(cc), cmds, sizeof(cmds), flags);
        if (error)
                goto out;
        while (frompage <= topage) {
                error = ssdfb_i2c_transfer(sc, cb, p, len, flags);
                if (error)
                        goto out;
                frompage++;
                p += stride;
        }
out:
        (void) iic_release_bus(sc->sc_i2c_tag, flags);

        return error;
}

static int
ssdfb_i2c_transfer_rect_sh1106(void *cookie, uint8_t fromcol, uint8_t tocol,
    uint8_t frompage, uint8_t topage, uint8_t *p, size_t stride, bool usepoll)
{
        struct ssdfb_i2c_softc *sc = (struct ssdfb_i2c_softc *)cookie;
        int flags = usepoll ? I2C_F_POLL : 0;
        uint8_t cb = SSDFB_I2C_CTRL_BYTE_DATA_MASK;
        uint8_t cc = SSDFB_I2C_CTRL_BYTE_CONTINUATION_MASK;
        size_t len = tocol + 1 - fromcol;
        int error;
        uint8_t cmds[] = {
                SSDFB_CMD_SET_PAGE_START_ADDRESS_BASE + frompage,
                SSDFB_I2C_CTRL_BYTE_CONTINUATION_MASK,
                SSDFB_CMD_SET_HIGHER_COLUMN_START_ADDRESS_BASE + (fromcol >> 4),
                SSDFB_I2C_CTRL_BYTE_CONTINUATION_MASK,
                SSDFB_CMD_SET_LOWER_COLUMN_START_ADDRESS_BASE + (fromcol & 0xf)
        };

        error = iic_acquire_bus(sc->sc_i2c_tag, flags);
        if (error)
                return error;
        while (frompage <= topage) {
                cmds[0] = SSDFB_CMD_SET_PAGE_START_ADDRESS_BASE + frompage;
                error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                    sc->sc_i2c_addr, &cc, sizeof(cc), cmds, sizeof(cmds), flags);
                if (error)
                        goto out;
                error = ssdfb_i2c_transfer(sc, cb, p, len, flags);
                if (error)
                        goto out;
                frompage++;
                p += stride;
        }
out:
        (void) iic_release_bus(sc->sc_i2c_tag, flags);

        return error;
}

/*
 * If the parent is an SMBus, then we can only send 2 bytes
 * of payload per txn. The SSD1306 triple byte commands are
 * not available so we have to use PAGE addressing mode
 * and split data into multiple txns.
 * This is ugly and slow but it's the best we can do.
 */
static int
ssdfb_smbus_transfer_rect(void *cookie, uint8_t fromcol, uint8_t tocol,
    uint8_t frompage, uint8_t topage, uint8_t *p, size_t stride, bool usepoll)
{
        struct ssdfb_i2c_softc *sc = (struct ssdfb_i2c_softc *)cookie;
        int flags = usepoll ? I2C_F_POLL : 0;
        uint8_t cb = SSDFB_I2C_CTRL_BYTE_DATA_MASK;
        uint8_t cc = 0;
        size_t len = tocol + 1 - fromcol;
        uint8_t cmd_higher_col =
            SSDFB_CMD_SET_HIGHER_COLUMN_START_ADDRESS_BASE + (fromcol >> 4);
        uint8_t cmd_lower_col =
            SSDFB_CMD_SET_LOWER_COLUMN_START_ADDRESS_BASE + (fromcol & 0xf);
        uint8_t cmd_page;
        uint8_t data[2];
        uint8_t *colp;
        uint8_t *endp;
        int error;

        error = iic_acquire_bus(sc->sc_i2c_tag, flags);
        if (error)
                return error;
        while (frompage <= topage) {
                error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                    sc->sc_i2c_addr, &cc, sizeof(cc),
                    &cmd_higher_col, sizeof(cmd_higher_col), flags);
                if (error)
                        goto out;
                error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                    sc->sc_i2c_addr, &cc, sizeof(cc),
                    &cmd_lower_col, sizeof(cmd_lower_col), flags);
                if (error)
                        goto out;
                cmd_page = SSDFB_CMD_SET_PAGE_START_ADDRESS_BASE + frompage;
                error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                    sc->sc_i2c_addr, &cc, sizeof(cc),
                    &cmd_page, sizeof(cmd_page), flags);
                if (error)
                        goto out;
                colp = p;
                endp = colp + len;
                if (len & 1) {
                        data[0] = *colp++;
                        error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                            sc->sc_i2c_addr, &cb, sizeof(cb), data, 1, flags);
                        if (error)
                                goto out;
                }
                while (colp < endp) {
                        /*
                         * Send two bytes at a time. We can't use colp directly
                         * because i2c controllers sometimes have data alignment
                         * requirements.
                         */
                        data[0] = *colp++;
                        data[1] = *colp++;
                        error = iic_exec(sc->sc_i2c_tag, I2C_OP_WRITE_WITH_STOP,
                            sc->sc_i2c_addr, &cb, sizeof(cb), data, 2, flags);
                        if (error)
                                goto out;
                }
                frompage++;
                p += stride;
        }
out:
        (void) iic_release_bus(sc->sc_i2c_tag, flags);

        return error;
}