NetBSD/sys/dev/i2c/ssdfb_i2c.c

419 lines
12 KiB
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;
}