550fdfb443
Change-Id: I478704fbf30dbf6d3382bcbfb11e75b512c032a1
555 lines
13 KiB
C
555 lines
13 KiB
C
/*
|
|
* i2c - generic driver for Inter-Integrated Circuit bus (I2C).
|
|
*/
|
|
|
|
/* kernel headers */
|
|
#include <minix/chardriver.h>
|
|
#include <minix/drivers.h>
|
|
#include <minix/ds.h>
|
|
#include <minix/i2c.h>
|
|
#include <minix/log.h>
|
|
#include <minix/type.h>
|
|
|
|
/* system headers */
|
|
#include <sys/mman.h>
|
|
|
|
/* usr headers */
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
/* SoC specific headers - 1 for each SoC */
|
|
#include "omap_i2c.h"
|
|
|
|
/* local definitions */
|
|
|
|
/* i2c slave addresses can be up to 10 bits */
|
|
#define NR_I2CDEV (0x3ff)
|
|
|
|
/* local function prototypes */
|
|
static int do_reserve(endpoint_t endpt, int slave_addr);
|
|
static int check_reservation(endpoint_t endpt, int slave_addr);
|
|
static void update_reservation(endpoint_t endpt, char *key);
|
|
static void ds_event(void);
|
|
|
|
static int validate_ioctl_exec(minix_i2c_ioctl_exec_t * ioctl_exec);
|
|
static int do_i2c_ioctl_exec(message * m);
|
|
|
|
static int env_parse_instance(void);
|
|
|
|
/* libchardriver callbacks */
|
|
int i2c_ioctl(message * m);
|
|
struct device *i2c_prepare(dev_t dev);
|
|
int i2c_transfer(endpoint_t endpt, int opcode, u64_t position,
|
|
iovec_t * iov, unsigned nr_req, endpoint_t user_endpt, unsigned int flags);
|
|
int i2c_other(message * m);
|
|
|
|
/* SEF callbacks and driver state management */
|
|
static int sef_cb_lu_state_save(int);
|
|
static int lu_state_restore(void);
|
|
static int sef_cb_init(int type, sef_init_info_t * info);
|
|
static void sef_local_startup(void);
|
|
|
|
/* Globals */
|
|
|
|
/* the bus that this instance of the driver is responsible for */
|
|
uint32_t i2c_bus_id;
|
|
|
|
/* Table of i2c device reservations. */
|
|
static struct i2cdev
|
|
{
|
|
uint8_t inuse;
|
|
endpoint_t endpt;
|
|
char key[DS_MAX_KEYLEN];
|
|
} i2cdev[NR_I2CDEV];
|
|
|
|
/* Process a request for an i2c operation.
|
|
* This is the interface that all hardware specific code must implement.
|
|
*/
|
|
int (*process) (minix_i2c_ioctl_exec_t * ioctl_exec);
|
|
|
|
struct device i2c_device;
|
|
|
|
/* logging - use with log_warn(), log_info(), log_debug(), log_trace() */
|
|
static struct log log = {
|
|
.name = "i2c",
|
|
.log_level = LEVEL_INFO,
|
|
.log_func = default_log
|
|
};
|
|
|
|
/* Entry points to the i2c driver from libchardriver.
|
|
* Only i2c_ioctl() and i2c_other() are implemented. The rest are no-op.
|
|
*/
|
|
static struct chardriver i2c_tab = {
|
|
.cdr_open = do_nop,
|
|
.cdr_close = do_nop,
|
|
.cdr_ioctl = i2c_ioctl,
|
|
.cdr_prepare = i2c_prepare,
|
|
.cdr_transfer = i2c_transfer,
|
|
.cdr_cleanup = nop_cleanup,
|
|
.cdr_alarm = nop_alarm,
|
|
.cdr_cancel = nop_cancel,
|
|
.cdr_select = nop_select,
|
|
.cdr_other = i2c_other
|
|
};
|
|
|
|
/*
|
|
* Claim an unclaimed device for exclusive use by endpt. This function can
|
|
* also be used to update the endpt if the endpt's label matches the label
|
|
* already associated with the slave address. This is useful if a driver
|
|
* shuts down unexpectedly and starts up with a new endpt and wants to reserve
|
|
* the same device it reserved before.
|
|
*/
|
|
static int
|
|
do_reserve(endpoint_t endpt, int slave_addr)
|
|
{
|
|
int r;
|
|
char key[DS_MAX_KEYLEN];
|
|
char label[DS_MAX_KEYLEN];
|
|
|
|
/* find the label for the endpoint */
|
|
r = ds_retrieve_label_name(label, endpt);
|
|
if (r != OK) {
|
|
log_warn(&log, "Couldn't find label for endpt='0x%x'\n",
|
|
endpt);
|
|
return r;
|
|
}
|
|
|
|
/* construct the key i2cdriver_announce published (saves an IPC call) */
|
|
snprintf(key, DS_MAX_KEYLEN, "drv.i2c.%d.%s", i2c_bus_id + 1, label);
|
|
|
|
if (slave_addr < 0 || slave_addr >= NR_I2CDEV) {
|
|
log_debug(&log,
|
|
"slave address must be positive & no more than 10 bits\n");
|
|
return EINVAL;
|
|
}
|
|
|
|
/* check if device is in use by another driver */
|
|
if (i2cdev[slave_addr].inuse != 0
|
|
&& strncmp(i2cdev[slave_addr].key, key, DS_MAX_KEYLEN) != 0) {
|
|
log_debug(&log, "address in use by '%s'/0x%x\n",
|
|
i2cdev[slave_addr].key, i2cdev[slave_addr].endpt);
|
|
return EBUSY;
|
|
}
|
|
|
|
/* device is free or already owned by us, claim it */
|
|
i2cdev[slave_addr].inuse = 1;
|
|
i2cdev[slave_addr].endpt = endpt;
|
|
memcpy(i2cdev[slave_addr].key, key, DS_MAX_KEYLEN);
|
|
|
|
sef_cb_lu_state_save(0); /* save reservations */
|
|
|
|
log_debug(&log, "Device 0x%x claimed by 0x%x key='%s'\n",
|
|
slave_addr, endpt, key);
|
|
|
|
return OK;
|
|
}
|
|
|
|
/*
|
|
* All drivers must reserve their device(s) before doing operations on them
|
|
* (read/write, etc). ioctl()'s from VFS (i.e. user programs) can only use
|
|
* devices that haven't been reserved. A driver isn't allowed to access a
|
|
* device that another driver has reserved (not even other instances of the
|
|
* same driver).
|
|
*/
|
|
static int
|
|
check_reservation(endpoint_t endpt, int slave_addr)
|
|
{
|
|
if (slave_addr < 0 || slave_addr >= NR_I2CDEV) {
|
|
log_debug(&log,
|
|
"slave address must be positive & no more than 10 bits\n");
|
|
return EINVAL;
|
|
}
|
|
|
|
if (endpt == VFS_PROC_NR && i2cdev[slave_addr].inuse == 0) {
|
|
log_debug(&log,
|
|
"allowing ioctl() from VFS to access unclaimed device\n");
|
|
return OK;
|
|
}
|
|
|
|
if (i2cdev[slave_addr].inuse && i2cdev[slave_addr].endpt != endpt) {
|
|
log_debug(&log, "device reserved by another endpoint\n");
|
|
return EBUSY;
|
|
} else if (i2cdev[slave_addr].inuse == 0) {
|
|
log_debug(&log,
|
|
"all drivers sending messages directly to this driver must reserve\n");
|
|
return EPERM;
|
|
} else {
|
|
log_debug(&log, "allowing access to registered device\n");
|
|
return OK;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* i2c listens to updates from ds about i2c device drivers starting up.
|
|
* When a driver comes back up with the same label, the endpt associated
|
|
* with the reservation needs to be updated. This function does the updating.
|
|
*/
|
|
static void
|
|
update_reservation(endpoint_t endpt, char *key)
|
|
{
|
|
int i;
|
|
|
|
log_debug(&log, "Updating reservation for '%s' endpt=0x%x\n", key,
|
|
endpt);
|
|
|
|
for (i = 0; i < NR_I2CDEV; i++) {
|
|
|
|
/* find devices in use that the driver owns */
|
|
if (i2cdev[i].inuse != 0
|
|
&& strncmp(i2cdev[i].key, key, DS_MAX_KEYLEN) == 0) {
|
|
/* update reservation with new endpoint */
|
|
do_reserve(endpt, i);
|
|
log_debug(&log, "Found device to update 0x%x\n", i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Checks a minix_i2c_ioctl_exec_t to see if the fields make sense.
|
|
*/
|
|
static int
|
|
validate_ioctl_exec(minix_i2c_ioctl_exec_t * ioctl_exec)
|
|
{
|
|
i2c_op_t op;
|
|
i2c_addr_t addr;
|
|
size_t len;
|
|
|
|
op = ioctl_exec->iie_op;
|
|
if (op != I2C_OP_READ &&
|
|
op != I2C_OP_READ_WITH_STOP &&
|
|
op != I2C_OP_WRITE &&
|
|
op != I2C_OP_WRITE_WITH_STOP &&
|
|
op != I2C_OP_READ_BLOCK && op != I2C_OP_WRITE_BLOCK) {
|
|
log_warn(&log, "iie_op value not valid\n");
|
|
return EINVAL;
|
|
}
|
|
|
|
addr = ioctl_exec->iie_addr;
|
|
if (addr < 0 || addr >= NR_I2CDEV) {
|
|
log_warn(&log, "iie_addr out of range 0x0-0x%x\n", NR_I2CDEV);
|
|
return EINVAL;
|
|
}
|
|
|
|
len = ioctl_exec->iie_cmdlen;
|
|
if (len < 0 || len > I2C_EXEC_MAX_CMDLEN) {
|
|
log_warn(&log,
|
|
"iie_cmdlen out of range 0-I2C_EXEC_MAX_CMDLEN\n");
|
|
return EINVAL;
|
|
}
|
|
|
|
len = ioctl_exec->iie_buflen;
|
|
if (len < 0 || len > I2C_EXEC_MAX_BUFLEN) {
|
|
log_warn(&log,
|
|
"iie_buflen out of range 0-I2C_EXEC_MAX_BUFLEN\n");
|
|
return EINVAL;
|
|
}
|
|
|
|
return OK;
|
|
}
|
|
|
|
/*
|
|
* Performs the action in minix_i2c_ioctl_exec_t.
|
|
*/
|
|
static int
|
|
do_i2c_ioctl_exec(message * m)
|
|
{
|
|
int r;
|
|
endpoint_t caller;
|
|
cp_grant_id_t grant_nr;
|
|
minix_i2c_ioctl_exec_t ioctl_exec;
|
|
|
|
caller = (endpoint_t) m->m_source;
|
|
grant_nr = (cp_grant_id_t) m->IO_GRANT;
|
|
|
|
/* Copy the requested exection into the driver */
|
|
r = sys_safecopyfrom(caller, grant_nr, (vir_bytes) 0,
|
|
(vir_bytes) & ioctl_exec, sizeof(ioctl_exec));
|
|
if (r != OK) {
|
|
log_warn(&log, "sys_safecopyfrom() failed\n");
|
|
return r;
|
|
}
|
|
|
|
/* input validation */
|
|
r = validate_ioctl_exec(&ioctl_exec);
|
|
if (r != OK) {
|
|
log_debug(&log, "Message validation failed\n");
|
|
return r;
|
|
}
|
|
|
|
/* permission check */
|
|
r = check_reservation(caller, ioctl_exec.iie_addr);
|
|
if (r != OK) {
|
|
log_debug(&log, "check_reservation() denied the request\n");
|
|
return r;
|
|
}
|
|
|
|
/* Call the device specific code to execute the action */
|
|
r = process(&ioctl_exec);
|
|
if (r != OK) {
|
|
log_debug(&log, "process() failed\n");
|
|
return r;
|
|
}
|
|
|
|
/* Copy the results of the execution back to the calling process */
|
|
r = sys_safecopyto(caller, grant_nr, (vir_bytes) 0,
|
|
(vir_bytes) & ioctl_exec, sizeof(ioctl_exec));
|
|
if (r != OK) {
|
|
log_warn(&log, "sys_safecopyto() failed\n");
|
|
return r;
|
|
}
|
|
|
|
return OK;
|
|
}
|
|
|
|
int
|
|
i2c_ioctl(message * m)
|
|
{
|
|
int r;
|
|
|
|
switch (m->COUNT) {
|
|
case MINIX_I2C_IOCTL_EXEC:
|
|
r = do_i2c_ioctl_exec(m);
|
|
break;
|
|
default:
|
|
log_warn(&log, "Invalid ioctl() 0x%x\n", m->COUNT);
|
|
r = EINVAL;
|
|
break;
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
int
|
|
i2c_other(message * m)
|
|
{
|
|
int r;
|
|
|
|
switch (m->m_type) {
|
|
case BUSC_I2C_RESERVE:
|
|
/* reserve a device on the bus for exclusive access */
|
|
r = do_reserve((endpoint_t) m->m_source, m->DEVICE);
|
|
break;
|
|
case BUSC_I2C_EXEC:
|
|
/* handle request from another driver */
|
|
m->COUNT = MINIX_I2C_IOCTL_EXEC;
|
|
r = do_i2c_ioctl_exec(m);
|
|
break;
|
|
case NOTIFY_MESSAGE:
|
|
/* handle notifications about drivers changing state */
|
|
if (m->m_source == DS_PROC_NR) {
|
|
ds_event();
|
|
}
|
|
r = OK;
|
|
break;
|
|
default:
|
|
log_warn(&log, "Invalid message type (0x%x)\n", m->m_type);
|
|
r = EINVAL;
|
|
break;
|
|
}
|
|
|
|
log_trace(&log, "i2c_other() returning r=%d\n", r);
|
|
|
|
return r;
|
|
}
|
|
|
|
struct device *
|
|
i2c_prepare(dev_t dev)
|
|
{
|
|
/* NOP */
|
|
i2c_device.dv_base = make64(0, 0);
|
|
i2c_device.dv_size = make64(0, 0);
|
|
|
|
return &i2c_device;
|
|
}
|
|
|
|
int
|
|
i2c_transfer(endpoint_t endpt, int opcode, u64_t position,
|
|
iovec_t * iov, unsigned nr_req, endpoint_t user_endpt, unsigned int flags)
|
|
{
|
|
/* NOP */
|
|
return OK;
|
|
}
|
|
|
|
/*
|
|
* The bus drivers are subscribed to DS events about device drivers on their
|
|
* bus. When the device drivers restart, DS sends a notification and this
|
|
* function updates the reservation table with the device driver's new
|
|
* endpoint.
|
|
*/
|
|
static void
|
|
ds_event(void)
|
|
{
|
|
char key[DS_MAX_KEYLEN];
|
|
u32_t value;
|
|
int type;
|
|
endpoint_t owner_endpoint;
|
|
int r;
|
|
|
|
/* check for pending events */
|
|
while ((r = ds_check(key, &type, &owner_endpoint)) == OK) {
|
|
|
|
r = ds_retrieve_u32(key, &value);
|
|
if (r != OK) {
|
|
log_warn(&log, "ds_retrieve_u32() failed r=%d\n", r);
|
|
return;
|
|
}
|
|
|
|
log_debug(&log, "key='%s' owner_endpoint=0x%x\n", key,
|
|
owner_endpoint);
|
|
|
|
if (value == DS_DRIVER_UP) {
|
|
/* clean up any old reservations the driver had */
|
|
log_debug(&log, "DS_DRIVER_UP\n");
|
|
update_reservation(owner_endpoint, key);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int
|
|
sef_cb_lu_state_save(int UNUSED(state))
|
|
{
|
|
int r;
|
|
char key[DS_MAX_KEYLEN];
|
|
|
|
memset(key, '\0', DS_MAX_KEYLEN);
|
|
snprintf(key, DS_MAX_KEYLEN, "i2c.%d.i2cdev", i2c_bus_id + 1);
|
|
r = ds_publish_mem(key, i2cdev, sizeof(i2cdev), DSF_OVERWRITE);
|
|
if (r != OK) {
|
|
log_warn(&log, "ds_publish_mem(%s) failed (r=%d)\n", key, r);
|
|
return r;
|
|
}
|
|
|
|
log_debug(&log, "State Saved\n");
|
|
|
|
return OK;
|
|
}
|
|
|
|
static int
|
|
lu_state_restore(void)
|
|
{
|
|
int r;
|
|
char key[DS_MAX_KEYLEN];
|
|
size_t size;
|
|
|
|
env_parse_instance();
|
|
|
|
size = sizeof(i2cdev);
|
|
|
|
memset(key, '\0', DS_MAX_KEYLEN);
|
|
snprintf(key, DS_MAX_KEYLEN, "i2c.%d.i2cdev", i2c_bus_id + 1);
|
|
|
|
r = ds_retrieve_mem(key, (char *) i2cdev, &size);
|
|
if (r != OK) {
|
|
log_warn(&log, "ds_retrieve_mem(%s) failed (r=%d)\n", key, r);
|
|
return r;
|
|
}
|
|
|
|
log_debug(&log, "State Restored\n");
|
|
|
|
return OK;
|
|
}
|
|
|
|
static int
|
|
sef_cb_init(int type, sef_init_info_t * UNUSED(info))
|
|
{
|
|
int r;
|
|
char regex[DS_MAX_KEYLEN];
|
|
|
|
if (type != SEF_INIT_FRESH) {
|
|
/* Restore a prior state. */
|
|
lu_state_restore();
|
|
}
|
|
#if defined(AM335X) || defined(DM37XX)
|
|
/* Set callback and initialize the bus */
|
|
r = omap_interface_setup(&process, i2c_bus_id);
|
|
if (r != OK) {
|
|
return r;
|
|
}
|
|
#else
|
|
#error /* Unknown SoC or bad configuration */
|
|
#endif
|
|
|
|
/* Announce we are up when necessary. */
|
|
if (type != SEF_INIT_LU) {
|
|
|
|
/* only capture events for this particular bus */
|
|
snprintf(regex, DS_MAX_KEYLEN, "drv\\.i2c\\.%d\\..*",
|
|
i2c_bus_id + 1);
|
|
|
|
/* Subscribe to driver events for i2c drivers */
|
|
r = ds_subscribe(regex, DSF_INITIAL | DSF_OVERWRITE);
|
|
if (r != OK) {
|
|
log_warn(&log, "ds_subscribe() failed\n");
|
|
return r;
|
|
}
|
|
|
|
chardriver_announce();
|
|
}
|
|
|
|
/* Save state */
|
|
sef_cb_lu_state_save(0);
|
|
|
|
/* Initialization completed successfully. */
|
|
return OK;
|
|
}
|
|
|
|
static void
|
|
sef_local_startup()
|
|
{
|
|
/* Register init callbacks. */
|
|
sef_setcb_init_fresh(sef_cb_init);
|
|
sef_setcb_init_lu(sef_cb_init);
|
|
sef_setcb_init_restart(sef_cb_init);
|
|
|
|
/* Register live update callbacks */
|
|
/* Agree to update immediately when LU is requested in a valid state */
|
|
sef_setcb_lu_prepare(sef_cb_lu_prepare_always_ready);
|
|
/* - Support live update starting from any standard state */
|
|
sef_setcb_lu_state_isvalid(sef_cb_lu_state_isvalid_standard);
|
|
/* - Register a custom routine to save the state. */
|
|
sef_setcb_lu_state_save(sef_cb_lu_state_save);
|
|
|
|
/* Let SEF perform startup. */
|
|
sef_startup();
|
|
}
|
|
|
|
static int
|
|
env_parse_instance(void)
|
|
{
|
|
int r;
|
|
long instance;
|
|
|
|
/* Parse the instance number passed to service */
|
|
instance = 0;
|
|
r = env_parse("instance", "d", 0, &instance, 1, 3);
|
|
if (r == -1) {
|
|
log_warn(&log,
|
|
"Expecting '-arg instance=N' argument (N=1..3)\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
/* Device files count from 1, hardware starts counting from 0 */
|
|
i2c_bus_id = instance - 1;
|
|
|
|
return OK;
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
int r;
|
|
|
|
env_setargs(argc, argv);
|
|
|
|
r = env_parse_instance();
|
|
if (r != OK) {
|
|
return r;
|
|
}
|
|
|
|
memset(i2cdev, '\0', sizeof(i2cdev));
|
|
sef_local_startup();
|
|
chardriver_task(&i2c_tab, CHARDRIVER_SYNC);
|
|
|
|
return OK;
|
|
}
|