**Adding a non-VEX I2C device
**
I’m going to talk a little about how to add a non VEX I2C device to the cortex and control it with ConVEX. For this example I’m assuming that there are no IMEs, to have the code co-exist with IMEs would mean either modifying the IME task or creating a duplicate version that both polls the IMEs as well as the new I2C device.
The only thing I had laying around for this example was a small switch board with a MAX7311 I2C port expander on it (actually two but only one was used in the code). This particular part adds 16 digital inputs or outputs for controlling switches, leds and anything else needing a single bit control port (it cannot be used for quad encoders or sonars). The datasheet for this part can be seen here.
Picture of my test board.
[ATTACH]7656[/ATTACH]
The part of the datasheet most relevant to this post is table 1.
[ATTACH]7655[/ATTACH]
This shows the internal registers, the general case for most I2C commands will be to send a register address followed by a read or write transaction. You can read the datasheet for details on the protocol and may also wish to refer to my previous explanation of the I2C bus.
https://vexforum.com/showpost.php?p=327819&postcount=1
In normal operation, ConVEX initializes the I2C bus and then starts a task polling the IMEs. This is done from the main initialization function which is called vexCortexInit and is in the vexcortex.c file, the init code looks like this.
// Init I2C bus
i2cInit();
// Init IMEs
vexImeInit( &I2CD1, (vexStream *)SD_CONSOLE );
The variable called I2CD1 is a structure with details about the I2C port on the cortex, this is passed to the IME code which stores it in a variable for later use but we can use it directly. The IME initialization function sets the I2C bus speed at 100KHz and then starts the poll task. For this example, I decided to let the normal initialization occur and then terminate the IME task in the user code. This is not really part of the I2C control but I show it here for completeness.
/*-----------------------------------------------------------------------------*/
/** @brief User setup */
/*-----------------------------------------------------------------------------*/
/** @details
* The digital and motor ports can (should) be configured here.
*/
void
vexUserSetup()
{
Thread *tp;
vexDigitalConfigure( dConfig, DIG_CONFIG_SIZE( dConfig ) );
vexMotorConfigure( mConfig, MOT_CONFIG_SIZE( mConfig ) );
// find IME task
tp = chRegFirstThread();
do
{
if( !strcmp( tp->p_name, "ime" ) )
break;
tp = chRegNextThread(tp);
} while (tp != NULL);
// If we found the ime thread then terminate it
if( tp != NULL )
{
// terminate the IME task
chThdTerminate(tp);
chThdWait(tp);
vex_printf("IME thread terminated\r\n");
}
// We have some input and some output ports on the port expander
// so call a setup function
I2CPortExpanderSetup();
}
What I’m doing here is searching through all the current threads looking for a thread with the name of “ime”, when I find that thread I’m sending a terminate message to it and then waiting for termination. The IME task is able to receive this message (well, it’s not really a message but you get the idea) and terminates itself.
Once the IME task is terminated we can have exclusive access to the I2C bus without any other interference (I need to check on whether I2C control is thread safe, will deal with that in another post). The general form of an I2C transaction will look like the function below, this one reads from the port expander.
msg_t
I2CPortExpanderPoll( uint8_t device, uint8_t *buf )
{
msg_t status = RDY_OK;
i2cflags_t errors = 0;
uint8_t txbuf = 0x00; // read from register 0
// acquire bus
i2cAcquireBus(&I2CD1);
// send 1 byte and then read 2 bytes from the port expander
status = i2cMasterTransmitTimeout(&I2CD1, device>>1, &txbuf, 1, buf, 2, MS2ST(4));
// release bus
i2cReleaseBus(&I2CD1);
// Ignore status for now in this demo
if (status != RDY_OK){
errors = i2cGetErrors(&I2CD1);
}
return(status);
}
i2cMasterTransmitTimeout is the function that does most of the work, this sends bytes to the I2C device and then reads a response if necessary. For devices that do not need to be sent any bytes before reading the i2cMasterReceiveTimeout function can be used.
The above function is used to read two bytes from the port expander registers 0 and 1. A single byte containing the starting register is sent, two bytes are then received.
Similar functions are used for other purposes. Here is the setup function that configures the port expander so that bits 12 through 15 are outputs (see the datasheet on the port expander for more details)
#define PORT_EXP_ADDR 0x4C
msg_t
I2CPortExpanderSetup()
{
msg_t status = RDY_OK;
i2cflags_t errors = 0;
// two bytes sent to port expander starting at register 6
uint8_t txbuf[3] = { 0x06, 0x0F, 0xFF };
// acquire bus
i2cAcquireBus(&I2CD1);
// send 3 bytes
status = i2cMasterTransmitTimeout(&I2CD1, PORT_EXP_ADDR>>1, txbuf, 3, NULL, 0, MS2ST(4));
// release bus
i2cReleaseBus(&I2CD1);
// Ignore status for now in this demo
if (status != RDY_OK){
errors = i2cGetErrors(&I2CD1);
}
}
This function sends two bytes to the port expander registers 6 and 7, it does not read anything.
The final function I used was for writing to the port expander output bits to turn on and off some leds. I used this function to write two bytes to port expander registers 2 and 3 (the data was also inverted as 0 turns the led on).
msg_t
I2CPortExpanderLeds( uint8_t device, uint8_t data )
{
msg_t status = RDY_OK;
i2cflags_t errors = 0;
// two bytes sent to port expander starting at register 2
uint8_t txbuf[3] = { 0x02, 0xFF, 0xFF };
txbuf[1] = ~data;
// acquire bus
i2cAcquireBus(&I2CD1);
// send 3 bytes
status = i2cMasterTransmitTimeout(&I2CD1, PORT_EXP_ADDR>>1, txbuf, 3, NULL, 0, MS2ST(4));
// release bus
i2cReleaseBus(&I2CD1);
// Ignore status for now in this demo
if (status != RDY_OK){
errors = i2cGetErrors(&I2CD1);
}
}
Here is the user code that was polling for input switches and flashing the leds.
msg_t
vexOperator( void *arg )
{
uint8_t port_data[8];
uint8_t led_data = 0x10;
(void)arg;
// Must call this
vexTaskRegister("operator");
// Run until asked to terminate
while(!chThdShouldTerminate())
{
// status on LCD of input ports
vexLcdPrintf( VEX_LCD_DISPLAY_2, VEX_LCD_LINE_1, "%4.2fV %8.1f", vexSpiGetMainBattery() / 1000.0, chTimeNow() / 1000.0 );
vexLcdPrintf( VEX_LCD_DISPLAY_2, VEX_LCD_LINE_2, "%2X %2X ", port_data[0], port_data[1] );
// get status from the I2C port expander
I2CPortExpanderPoll( PORT_EXP_ADDR, port_data );
// Flash leds for fun
I2CPortExpanderLeds( PORT_EXP_ADDR, led_data );
if((led_data <<= 1)==0)
led_data = 0x10;
// Don't hog cpu
vexSleep( 25 );
}
return (msg_t)0;
}
So there’s not much point in posting the complete example as the code is dependent on my switch board which you don’t have. However, I hope it gives you a sense of how easy it is to communicate with an I2C device. Read up on the I2C functions on the chibiOS documentation page here;
http://chibios.sourceforge.net/docs/kernel_hal_rm/group___i2_c.html#ga57039ea5e6f70691b667a14248618967
Also look at vexime.c to see how I was communicating with the IMEs.