root/sys/dev/drm2/drm_dp_iic_helper.c
/*
 * Copyright © 2009 Keith Packard
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that copyright
 * notice and this permission notice appear in supporting documentation, and
 * that the name of the copyright holders not be used in advertising or
 * publicity pertaining to distribution of the software without specific,
 * written prior permission.  The copyright holders make no representations
 * about the suitability of this software for any purpose.  It is provided "as
 * is" without express or implied warranty.
 *
 * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
 * EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
 * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
 * OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/kobj.h>
#include <sys/bus.h>
#include <dev/iicbus/iic.h>
#include "iicbus_if.h"
#include <dev/iicbus/iiconf.h>
#include <dev/drm2/drmP.h>
#include <dev/drm2/drm_dp_helper.h>

static int
iic_dp_aux_transaction(device_t idev, int mode, uint8_t write_byte,
    uint8_t *read_byte)
{
        struct iic_dp_aux_data *aux_data;
        int ret;

        aux_data = device_get_softc(idev);
        ret = (*aux_data->aux_ch)(idev, mode, write_byte, read_byte);
        if (ret < 0)
                return (ret);
        return (0);
}

/*
 * I2C over AUX CH
 */

/*
 * Send the address. If the I2C link is running, this 'restarts'
 * the connection with the new address, this is used for doing
 * a write followed by a read (as needed for DDC)
 */
static int
iic_dp_aux_address(device_t idev, u16 address, bool reading)
{
        struct iic_dp_aux_data *aux_data;
        int mode, ret;

        aux_data = device_get_softc(idev);
        mode = MODE_I2C_START;
        if (reading)
                mode |= MODE_I2C_READ;
        else
                mode |= MODE_I2C_WRITE;
        aux_data->address = address;
        aux_data->running = true;
        ret = iic_dp_aux_transaction(idev, mode, 0, NULL);
        return (ret);
}

/*
 * Stop the I2C transaction. This closes out the link, sending
 * a bare address packet with the MOT bit turned off
 */
static void
iic_dp_aux_stop(device_t idev, bool reading)
{
        struct iic_dp_aux_data *aux_data;
        int mode;

        aux_data = device_get_softc(idev);
        mode = MODE_I2C_STOP;
        if (reading)
                mode |= MODE_I2C_READ;
        else
                mode |= MODE_I2C_WRITE;
        if (aux_data->running) {
                (void)iic_dp_aux_transaction(idev, mode, 0, NULL);
                aux_data->running = false;
        }
}

/*
 * Write a single byte to the current I2C address, the
 * the I2C link must be running or this returns -EIO
 */
static int
iic_dp_aux_put_byte(device_t idev, u8 byte)
{
        struct iic_dp_aux_data *aux_data;
        int ret;

        aux_data = device_get_softc(idev);

        if (!aux_data->running)
                return (-EIO);

        ret = iic_dp_aux_transaction(idev, MODE_I2C_WRITE, byte, NULL);
        return (ret);
}

/*
 * Read a single byte from the current I2C address, the
 * I2C link must be running or this returns -EIO
 */
static int
iic_dp_aux_get_byte(device_t idev, u8 *byte_ret)
{
        struct iic_dp_aux_data *aux_data;
        int ret;

        aux_data = device_get_softc(idev);

        if (!aux_data->running)
                return (-EIO);

        ret = iic_dp_aux_transaction(idev, MODE_I2C_READ, 0, byte_ret);
        return (ret);
}

static int
iic_dp_aux_xfer(device_t idev, struct iic_msg *msgs, uint32_t num)
{
        u8 *buf;
        int b, m, ret;
        u16 len;
        bool reading;

        ret = 0;
        reading = false;

        for (m = 0; m < num; m++) {
                len = msgs[m].len;
                buf = msgs[m].buf;
                reading = (msgs[m].flags & IIC_M_RD) != 0;
                ret = iic_dp_aux_address(idev, msgs[m].slave >> 1, reading);
                if (ret < 0)
                        break;
                if (reading) {
                        for (b = 0; b < len; b++) {
                                ret = iic_dp_aux_get_byte(idev, &buf[b]);
                                if (ret != 0)
                                        break;
                        }
                } else {
                        for (b = 0; b < len; b++) {
                                ret = iic_dp_aux_put_byte(idev, buf[b]);
                                if (ret < 0)
                                        break;
                        }
                }
                if (ret != 0)
                        break;
        }
        iic_dp_aux_stop(idev, reading);
        DRM_DEBUG_KMS("dp_aux_xfer return %d\n", ret);
        return (-ret);
}

static void
iic_dp_aux_reset_bus(device_t idev)
{

        (void)iic_dp_aux_address(idev, 0, false);
        (void)iic_dp_aux_stop(idev, false);
}

static int
iic_dp_aux_reset(device_t idev, u_char speed, u_char addr, u_char *oldaddr)
{

        iic_dp_aux_reset_bus(idev);
        return (0);
}

static int
iic_dp_aux_prepare_bus(device_t idev)
{

        /* adapter->retries = 3; */
        iic_dp_aux_reset_bus(idev);
        return (0);
}

static int
iic_dp_aux_probe(device_t idev)
{

        return (BUS_PROBE_DEFAULT);
}

static int
iic_dp_aux_attach(device_t idev)
{
        struct iic_dp_aux_data *aux_data;

        aux_data = device_get_softc(idev);
        aux_data->port = device_add_child(idev, "iicbus", DEVICE_UNIT_ANY);
        if (aux_data->port == NULL)
                return (ENXIO);
        device_quiet(aux_data->port);
        bus_attach_children(idev);
        return (0);
}

int
iic_dp_aux_add_bus(device_t dev, const char *name,
    int (*ch)(device_t idev, int mode, uint8_t write_byte, uint8_t *read_byte),
    void *priv, device_t *bus, device_t *adapter)
{
        device_t ibus;
        struct iic_dp_aux_data *data;
        int idx, error;
        static int dp_bus_counter;

        bus_topo_lock();

        idx = atomic_fetchadd_int(&dp_bus_counter, 1);
        ibus = device_add_child(dev, "drm_iic_dp_aux", idx);
        if (ibus == NULL) {
                bus_topo_unlock();
                DRM_ERROR("drm_iic_dp_aux bus %d creation error\n", idx);
                return (-ENXIO);
        }
        device_quiet(ibus);
        error = device_probe_and_attach(ibus);
        if (error != 0) {
                device_delete_child(dev, ibus);
                bus_topo_unlock();
                DRM_ERROR("drm_iic_dp_aux bus %d attach failed, %d\n",
                    idx, error);
                return (-error);
        }
        data = device_get_softc(ibus);
        data->running = false;
        data->address = 0;
        data->aux_ch = ch;
        data->priv = priv;
        error = iic_dp_aux_prepare_bus(ibus);
        if (error == 0) {
                *bus = ibus;
                *adapter = data->port;
        }
        bus_topo_unlock();
        return (-error);
}

static device_method_t drm_iic_dp_aux_methods[] = {
        DEVMETHOD(device_probe,         iic_dp_aux_probe),
        DEVMETHOD(device_attach,        iic_dp_aux_attach),
        DEVMETHOD(device_detach,        bus_generic_detach),
        DEVMETHOD(iicbus_reset,         iic_dp_aux_reset),
        DEVMETHOD(iicbus_transfer,      iic_dp_aux_xfer),
        DEVMETHOD_END
};

static driver_t drm_iic_dp_aux_driver = {
        "drm_iic_dp_aux",
        drm_iic_dp_aux_methods,
        sizeof(struct iic_dp_aux_data)
};

DRIVER_MODULE_ORDERED(drm_iic_dp_aux, drmn, drm_iic_dp_aux_driver, 0, 0,
    SI_ORDER_SECOND);