
io.dvlopt.linux.i2c.I2CBus Maven / Gradle / Ivy
/*
* Copyright 2018 Adam Helinski
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.dvlopt.linux.i2c ;
import com.sun.jna.Memory ;
import com.sun.jna.NativeLong ;
import com.sun.jna.Pointer ;
import com.sun.jna.ptr.LongByReference ;
import io.dvlopt.linux.LinuxException ;
import io.dvlopt.linux.NativeMemory ;
import io.dvlopt.linux.SizeT ;
import io.dvlopt.linux.i2c.I2CBlock ;
import io.dvlopt.linux.i2c.I2CFunctionalities ;
import io.dvlopt.linux.i2c.I2CTransaction ;
import io.dvlopt.linux.i2c.internal.NativeI2CSmbusData ;
import io.dvlopt.linux.i2c.internal.NativeI2CSmbusIoctlData ;
import io.dvlopt.linux.io.LinuxIO ;
/**
* Class representing an I2C bus.
*
* It provides 3 sets of operations for doing IO :
*
* - Directly reading and writing by using {@link #read( I2CBuffer ) read} and
* {@link #write( I2CBuffer ) write}.
* - Doing {@link #doTransaction( I2CTransaction ) transactions} (ie. uninterrupted reads and writes)
* - Using SMBUS operations as defined in the standard (all the other IO functions).
*
*
* SMBUS operations are a subset of what I2C can achieve but propose common interactions. However,
* {@link #getFunctionalities() getFunctionnalities} should be used in order to understand what operations the underlying
* driver support.
*
* I2C is a standard but your master device and the slaves devices it talks do probably do not support everything properly.
* For instance, as is, the Raspberry Pi only supports a few SMBUS operations and do not support transactions involving more than
* one message at a time (which defeats the purpose of having them in the first place). Furthermore, talking to an Arduino will result
* in timing issues if it is too slow to answer. Communication between those two is very much possible but requires more preparation and
* testing. This is an example of how things are not always that simple.
*
* In this API, `command` is just another word for what is sometimes called `register`. Essentially, it means that before reading or
* writing bytes, the master writes a byte specifying a command, something to do with the following byte(s). If an SMBUS functionality
* is not available, this can often be emulated by explicitly sending the command byte and then reading or writing as needed.
*
* @see SMBUS operations
*/
public class I2CBus implements AutoCloseable {
private final static NativeLong I2C_RETRIES = new NativeLong( 0x0701L ,
true ) ;
private final static NativeLong I2C_TIMEOUT = new NativeLong( 0x0702L ,
true ) ;
private final static NativeLong I2C_SLAVE = new NativeLong( 0x0703L ,
true ) ;
private final static NativeLong I2C_SLAVE_FORCE = new NativeLong( 0x0706L ,
true ) ;
private final static NativeLong I2C_TENBIT = new NativeLong( 0x0704L ,
true ) ;
private final static NativeLong I2C_FUNCS = new NativeLong( 0x0705L ,
true ) ;
private final static NativeLong I2C_RDWR = new NativeLong( 0x0707L ,
true ) ;
private final static NativeLong I2C_PEC = new NativeLong( 0x0708L ,
true ) ;
private final static NativeLong I2C_SMBUS = new NativeLong( 0x0720L ,
true ) ;
private final static byte I2C_SMBUS_READ = 1 ;
private final static byte I2C_SMBUS_WRITE = 0 ;
private final static byte I2C_SMBUS_QUICK = 0 ;
private final static byte I2C_SMBUS_BYTE = 1 ;
private final static byte I2C_SMBUS_BYTE_DATA = 2 ;
private final static byte I2C_SMBUS_WORD_DATA = 3 ;
private final static byte I2C_SMBUS_PROC_CALL = 4 ;
private final static byte I2C_SMBUS_BLOCK_DATA = 5 ;
private final static byte I2C_SMBUS_I2C_BLOCK_BROKEN = 6 ;
private final static byte I2C_SMBUS_BLOCK_PROC_CALL = 7 ;
private final static byte I2C_SMBUS_I2C_BLOCK_DATA = 8 ;
private final int fd ;
private final Memory i2cSmbusIoctlData ;
private final Memory i2cSmbusData ;
private boolean isTenBit = false ;
/**
* Opens an I2C bus by number, `/dev/i2c-$busNumber`.
*
* @param busNumber The number of the bus.
*
* @throws LinuxException When something fails on the native side.
*/
public I2CBus( int busNumber ) throws LinuxException {
this( "/dev/i2c-" + busNumber ) ;
}
/**
* Opens an I2C bus on the given path.
*
* @param path A path to the bus such as `/dev/i2c-1`.
*
* @throws LinuxException When something fails on the native side.
*/
public I2CBus( String path ) throws LinuxException {
this.fd = LinuxIO.open64( path ,
LinuxIO.O_RDWR ) ;
if ( this.fd < 0 ) {
throw new LinuxException( "Unable to open an I2C bus at the given path" ) ;
}
this.i2cSmbusIoctlData = new Memory( NativeI2CSmbusIoctlData.SIZE ) ;
this.i2cSmbusData = new Memory( NativeI2CSmbusData.SIZE ) ;
this.i2cSmbusIoctlData.clear() ;
this.i2cSmbusData.clear() ;
}
/**
* Closes this bus.
*
* @throws LinuxException When something fails on the native side.
*/
public void close() throws LinuxException {
if ( LinuxIO.close( this.fd ) != 0 ) {
throw new LinuxException( "Unable to close this I2C bus" ) ;
}
}
/**
* Do an uninterrupted transaction of several messages.
*
* Not all devices support this feature, or support only one message at a time which
* is not very interesting.
*
* @param transaction The transaction that will be carried out.
*
* @throws LinuxException When something fails on the native side.
*/
public void doTransaction( I2CTransaction transaction ) throws LinuxException {
if ( LinuxIO.ioctl( this.fd ,
I2C_RDWR ,
transaction.memory ) < 0 ) {
throw new LinuxException( "Unable to fully perform requested I2C transaction" ) ;
}
}
/**
* Finds out what this I2C bus can do.
*
* @return Functionalities.
*
* @throws LinuxException When something fails on the native side.
*/
public I2CFunctionalities getFunctionalities() throws LinuxException {
LongByReference longRef = new LongByReference() ;
if ( LinuxIO.ioctl( this.fd ,
I2C_FUNCS ,
longRef.getPointer() ) < 0 ) {
throw new LinuxException( "Unable to get the I2C functionalities of this bus" ) ;
}
return new I2CFunctionalities( (int)( longRef.getValue() ) ) ;
}
/**
* Selects an available slave device using a regular 7-bit address.
*
* @param address The address of the needed slave.
*
* @throws LinuxException When something fails on the native side.
*
* @see #selectSlave( int, boolean, boolean )
*/
public void selectSlave( int address ) throws LinuxException {
this.selectSlave( address ,
false ,
false ) ;
}
/**
* Selects an available slave device.
*
* This needs to be called only once if several IO operations are performed on the same
* slave and has no effect on {@link #doTransaction(I2CTransaction) transactions}.
*
* @param address The address of the needed slave.
*
* @param force Should the slave be selected even if it is already used somewhere else ?
*
* @param isTenBit Is the given address in 10-bit resolution ?
*
* @throws LinuxException When something fails on the native side.
*/
public void selectSlave( int address ,
boolean force ,
boolean isTenBit ) throws LinuxException {
if ( isTenBit ) {
if ( this.isTenBit == false ) {
if ( LinuxIO.ioctl( this.fd ,
I2C_TENBIT ,
1 ) < 0 ) {
throw new LinuxException( "Unable to set 10 bit addressing" ) ;
}
this.isTenBit = true ;
}
}
else if ( this.isTenBit ) {
if ( LinuxIO.ioctl( this.fd ,
I2C_TENBIT ,
0 ) < 0 ) {
throw new LinuxException( "Unable to set 7 bit addressing" ) ;
}
this.isTenBit = false ;
}
if ( LinuxIO.ioctl( this.fd ,
force ? I2C_SLAVE_FORCE
: I2C_SLAVE ,
address
& 0xffffffffL ) < 0 ) {
throw new LinuxException( "Unable to use the given slave address" ) ;
}
}
/**
* Sets the number of time a slave should be polled when not acknowledging.
*
* Does not always have an effect depending on the underlying driver.
*
* @param nRetries the number of retries.
*
* @throws LinuxException When something fails on the native side.
*/
public void setRetries( int nRetries ) throws LinuxException {
if ( LinuxIO.ioctl( this.fd ,
I2C_RETRIES ,
nRetries
& 0xffffffffL ) < 0 ) {
throw new LinuxException( "Unable to set the number of retries" ) ;
}
}
/**
* Sets the timeout in milliseconds.
*
* Does not always have an effect depending on the underlying driver.
*
* @param milliseconds The timeout in milliseconds.
*
* @throws LinuxException When something fails on the native side.
*/
public void setTimeout( int milliseconds ) throws LinuxException {
if ( LinuxIO.ioctl( this.fd ,
I2C_TIMEOUT ,
( milliseconds / 10 )
& 0xffffffL ) < 0 ) {
throw new LinuxException( "Unable to set timeout" ) ;
}
}
/**
* Enables or disables packet error checking for SMBUS commands.
*
* Is ignored unless the underlying driver provides this {@link #getFunctionalities() functionality}.
*
* Slave devices do not necessarely support this feature either.
*
* @param usePEC Should PEC be enabled ?
*
* @throws LinuxException When something fails on the native side.
*/
public void usePEC( boolean usePEC ) throws LinuxException {
if ( LinuxIO.ioctl( this.fd ,
I2C_PEC ,
usePEC ? 1L
: 0L ) < 0 ) {
throw new LinuxException( "Unable to set or unset PEC for SMBUS operations" ) ;
}
}
// Performs an SMBUS command.
//
private int i2cSmbusAccess( byte readWrite ,
int command ,
int size ,
Pointer data ) throws LinuxException {
this.i2cSmbusIoctlData.setByte( NativeI2CSmbusIoctlData.OFFSET_READ_WRITE ,
readWrite ) ;
this.i2cSmbusIoctlData.setByte( NativeI2CSmbusIoctlData.OFFSET_COMMAND ,
(byte)command ) ;
this.i2cSmbusIoctlData.setInt( NativeI2CSmbusIoctlData.OFFSET_SIZE ,
size ) ;
this.i2cSmbusIoctlData.setPointer( NativeI2CSmbusIoctlData.OFFSET_DATA ,
data ) ;
int result = LinuxIO.ioctl( this.fd ,
I2C_SMBUS ,
this.i2cSmbusIoctlData ) ;
if ( result < 0 ) {
throw new LinuxException( "Unable to perform I2C/SMBUS operation" ) ;
}
return result ;
}
/**
* SMBUS operation sending only the READ or WRITE bit, no data is carried.
*
* @param isWrite True if the WRITE bit must be set.
*
* @throws LinuxException When something fails on the native side.
*/
public void quick( boolean isWrite ) throws LinuxException {
this.i2cSmbusAccess( isWrite ? I2C_SMBUS_WRITE
: I2C_SMBUS_READ ,
0 ,
I2C_SMBUS_QUICK ,
null ) ;
}
/**
* SMBUS operation reading a byte without a command.
*
* @return The unsigned byte received from the slave.
*
* @throws LinuxException When something fails on the native side.
*/
public int readByteDirectly() throws LinuxException {
this.i2cSmbusAccess( I2C_SMBUS_READ ,
0 ,
I2C_SMBUS_BYTE ,
this.i2cSmbusData ) ;
return NativeMemory.getUnsignedByte( this.i2cSmbusData ,
0 ) ;
}
/**
* SMBUS operation reading a byte after specifying a command.
*
* @param command The command.
*
* @return The unsigned byte received from the slave.
*
* @throws LinuxException When something fails on the native side.
*/
public int readByte( int command ) throws LinuxException {
this.i2cSmbusAccess( I2C_SMBUS_READ ,
command ,
I2C_SMBUS_BYTE_DATA ,
this.i2cSmbusData ) ;
return NativeMemory.getUnsignedByte( this.i2cSmbusData ,
0 ) ;
}
/**
* SMBUS operation reading a short after specifying a command.
*
* @param command The command.
*
* @return The unsigned short received from the slave.
*
* @throws LinuxException When something fails on the native side.
*/
public int readWord( int command ) throws LinuxException {
this.i2cSmbusAccess( I2C_SMBUS_READ ,
command ,
I2C_SMBUS_WORD_DATA ,
this.i2cSmbusData ) ;
return NativeMemory.getUnsignedShort( this.i2cSmbusData ,
0 ) ;
}
/**
* SMBUS operation reading several bytes after specifying a command.
*
* @param command The command.
*
* @param block Block of bytes the answer will be written to.
*
* @return The number of bytes read.
*
* @throws LinuxException When something fails on the native side.
*/
public int readBlock( int command ,
I2CBlock block ) throws LinuxException {
this.i2cSmbusAccess( I2C_SMBUS_READ ,
command ,
I2C_SMBUS_BLOCK_DATA ,
block.memory ) ;
return block.readLength() ;
}
/**
* SMBUS-like operation reading several bytes after specifying a command where the length
* is part of the message.
*
* This operation is not in the SMBUS standard but is often supported nonetheless.
*
* @param command The command.
*
* @param block Block of bytes the answer will be written to.
*
* @param length How many bytes should be read.
*
* @throws LinuxException When something fails on the native side.
*/
public void readI2CBlock( int command ,
I2CBlock block ,
int length ) throws LinuxException {
if ( length > I2CBlock.SIZE ) {
throw new IllegalArgumentException( "Too many bytes requested." ) ;
}
block.writeLength( length ) ;
this.i2cSmbusAccess( I2C_SMBUS_READ ,
command ,
length == 32 ? I2C_SMBUS_I2C_BLOCK_BROKEN
: I2C_SMBUS_I2C_BLOCK_DATA ,
block.memory ) ;
block.readLength() ;
}
/**
* Directly reads bytes from the slave device (length is specified by the buffer).
*
* @param buffer Buffer the answer will be written to.
*
* @throws LinuxException When something fails on the native side.
*
* @see #read( I2CBuffer, int )
*/
public void read( I2CBuffer buffer ) throws LinuxException {
this.read( buffer ,
buffer.length ) ;
}
/**
* Directly reads length bytes from the slave device.
*
* @param buffer Buffer the answer will be written to.
*
* @param length The number of bytes requested.
*
* @throws LinuxException When something fails on the native side.
*/
public void read( I2CBuffer buffer ,
int length ) throws LinuxException {
if ( LinuxIO.read( this.fd ,
buffer.memory ,
new SizeT( length ) ).intValue() < 0 ) {
throw new LinuxException( "Unable to read to I2C buffer" ) ;
}
}
/**
* SMBUS operation writing a byte without a command.
*
* @param b The unsigned byte that needs to be sent.
*
* @throws LinuxException When something fails on the native side.
*/
public void writeByteDirectly( int b ) throws LinuxException {
this.i2cSmbusAccess( I2C_SMBUS_WRITE ,
b ,
I2C_SMBUS_BYTE ,
null ) ;
}
/**
* SMBUS operation writing a byte after specifying a command.
*
* @param command The command.
*
* @param b The unsigned byte that need to be sent.
*
* @throws LinuxException When something fails on the native side.
*/
public void writeByte( int command ,
int b ) throws LinuxException {
NativeMemory.setUnsignedByte( this.i2cSmbusData ,
0 ,
b ) ;
this.i2cSmbusAccess( I2C_SMBUS_WRITE ,
command ,
I2C_SMBUS_BYTE_DATA ,
this.i2cSmbusData ) ;
}
/**
* SMBUS operation writing a short after specifying a command.
*
* @param command The command.
*
* @param word The unsigned short that needs to be sent.
*
* @throws LinuxException When something fails on the native side.
*/
public void writeWord( int command ,
int word ) throws LinuxException {
NativeMemory.setUnsignedShort( this.i2cSmbusData ,
0 ,
word ) ;
this.i2cSmbusAccess( I2C_SMBUS_WRITE ,
command ,
I2C_SMBUS_WORD_DATA ,
this.i2cSmbusData ) ;
}
/**
* SMBUS operation writing several bytes after specifying a command.
*
* After the command byte, the master also sends a byte count, how many bytes
* will be written.
*
* @param command The command.
*
* @param block Block of bytes to write.
*
* @throws LinuxException When something fails on the native side.
*/
public void writeBlock( int command ,
I2CBlock block ) throws LinuxException {
block.writeLength() ;
this.i2cSmbusAccess( I2C_SMBUS_WRITE ,
command ,
I2C_SMBUS_I2C_BLOCK_DATA ,
block.memory ) ;
}
/**
* SMBUS-like operation writing several bytes after specifying a command where the length
* is part of the message.
*
* This operation is not in the SMBUS standard but is often supported nonetheless.
*
* Unlike {@link #writeBlock( int, I2CBlock ) writeBlock}, this operation
* does not send a byte count after the command.
*
* @param command The command.
*
* @param block Block of bytes to write.
*
* @throws LinuxException When something fails on the native side.
*/
public void writeI2CBlock( int command ,
I2CBlock block ) throws LinuxException {
block.writeLength() ;
this.i2cSmbusAccess( I2C_SMBUS_WRITE ,
command ,
I2C_SMBUS_I2C_BLOCK_BROKEN ,
block.memory ) ;
}
/**
* Directly writes bytes to the slave device (length is specified by the buffer).
*
* @param buffer Buffer to write.
*
* @throws LinuxException When something fails on the native side.
*
* @see #write( I2CBuffer, int )
*/
public void write( I2CBuffer buffer ) throws LinuxException {
this.write( buffer ,
buffer.length ) ;
}
/**
* Directly writes length bytes to the slave device.
*
* @param buffer Buffer to write.
*
* @param length The number of bytes.
*
* @throws LinuxException When something fails on the native side.
*/
public void write( I2CBuffer buffer ,
int length ) throws LinuxException {
if ( LinuxIO.write( this.fd ,
buffer.memory ,
new SizeT( length ) ).intValue() < 0 ) {
throw new LinuxException( "Unable to write I2C buffer" ) ;
}
}
/**
* SMBUS RPC-like operation, writing a short after specifying a command and then
* reading the answer.
*
* @param command The command.
*
* @param word The unsigned short to be sent.
*
* @return The unsigned short given back by the slave.
*
* @throws LinuxException When something fails on the native side.
*/
public int processCall( int command ,
int word ) throws LinuxException {
NativeMemory.setUnsignedShort( this.i2cSmbusData ,
0 ,
word ) ;
this.i2cSmbusAccess( I2C_SMBUS_WRITE ,
command ,
I2C_SMBUS_PROC_CALL ,
this.i2cSmbusData ) ;
return NativeMemory.getUnsignedShort( this.i2cSmbusData ,
0 ) ;
}
/**
* SMBUS RPC-like operation, writing several bytes after specifying a command and then
* reading several bytes as an answer.
*
* @param command The command.
*
* @param block Block of bytes to write, also where the answer will be written to.
*
* @return The number of bytes read.
*
* @throws LinuxException When something fails on the native side.
*/
public int blockProcessCall( int command ,
I2CBlock block ) throws LinuxException {
return this.blockProcessCall( command ,
block ,
block ) ;
}
/**
* SMBUS RPC-like operation, writing several bytes after specifying a command and then
* reading several bytes as an answer.
*
* @param command The command.
*
* @param blockWrite Block of bytes to write.
*
* @param blockRead Block of bytes the answer will be written to (can be the same as blockWrite
* is it can be overwritten
*
* @return The number of bytes read.
*
* @throws LinuxException When something fails on the native side.
*/
public int blockProcessCall( int command ,
I2CBlock blockWrite ,
I2CBlock blockRead ) throws LinuxException {
I2CBlock block ;
blockWrite.writeLength() ;
if ( blockWrite == blockRead ) {
block = blockWrite ;
}
else {
NativeMemory.copy( blockWrite.memory ,
blockRead.memory ) ;
block = blockRead ;
}
this.i2cSmbusAccess( I2C_SMBUS_WRITE ,
command ,
I2C_SMBUS_BLOCK_PROC_CALL ,
block.memory ) ;
return block.readLength() ;
}
}