/* $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 __KERNEL_RCSID(0, "$NetBSD: ssdfb_i2c.c,v 1.11 2021/08/05 22:31:20 tnn Exp $"); #include #include #include #include #include #include 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; }