Similar to the GpioController
, there needs to be a mechanism to determine the best I2cDevice
to use.
Updated 6/18/2019
I2cDevice
allows for an easy way to create an I2C connection between a master controller and one device binding. However, there are scenarios where a new type of controller can manage related devices more efficiently.
Rationale and Usage
System.Device.Gpio Consistency - Other APIs provide controllers that work with low-level drivers to perform the functional operations. For example, GpioController
informs related driver to write logic-levels to pins and PwmController
informs related driver to write variable frequency to pins. The I2cController
will inform which I2C device to read/write data.
Multiple Device Management - Other controllers also can open and close multiple pins that will be controlled. Sometimes hardware configurations require a master to communicate with multiple devices on the same I2C bus. This is a current limitation with I2cDevice
as it requires the bus ID & device address when instantiating and the read/write methods communicate with only the one device.
Default OS Implementation - Most current device binding samples specifically define the I2cDevice
related to executing OS (e.g. UnixI2cDevice
or Windows10I2cDevice
). This forces users to update sample code when running on different hardware and OS. This proposal will offer a method to generate the default I2cDevice
for OS similar to how a GpioController
provides a default GpioDriver
.
Controller Interface - In addition to providing a default I2cController
, this proposal also includes an II2cController
as there are use-cases where bindings could be a master I2C controller that can control other I2C devices.
Proposed Changes
1. Add New II2cController and I2cController
Add a new II2cController
interface that includes the same read/write methods as an I2cDevice
with the addition of specified bus ID and address parameters. This allows the controller to know which device to use for communications. There are also methods to open/close devices.
public interface II2cController : IDisposable
{
void OpenDevice(I2cConnectionSettings settings, bool shouldDispose = true);
void OpenDevice(I2cDevice device, bool shouldDispose = true);
void CloseDevice(I2cConnectionSettings settings);
void CloseDevice(I2cDevice device);
void ClearDevices();
bool TryGetDevice(int busId, int address, out I2cDevice device);
void Read(int busId, int address, Span<byte> buffer);
byte ReadByte(int busId, int address);
void Write(int busId, int address, ReadOnlySpan<byte> buffer);
void WriteByte(int busId, int address, byte value);
void WriteRead(int busId, int address, ReadOnlySpan<byte> writeBuffer, Span<byte> readBuffer);
}
The I2cController
class will implement the new II2cController
interface and be the default for most apps and device bindings. The controller is instantiated with zero I2C devices to control. More devices can be opened/closed similar to pins on other controllers.
// Class
public class I2cController : II2cController
// Constructor
public I2cController()
The creation of devices with a new static helper method that attempts to pick the best I2C device for OS app is executing on. This static method will be added to a new sealed partial class
respective to OS.
// I2cController.Linux.cs
public static I2cDevice Create(I2cConnectionSettings settings) => new UnixI2cDevice(settings);
// I2cController.Windows.cs
public static I2cDevice Create(I2cConnectionSettings settings) => new Windows10I2cDevice(settings);
Example 1.1
Instantiate a controller and open a device.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
var i2cController = new I2cController();
i2cController.OpenDevice(settings);
Example 1.2
Open a new device to controller that already contains device with specified address.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
var i2cController = new I2cController();
i2cController.OpenDevice(settings);
i2cController.OpenDevice(settings); // Throws exception because device with address already exists.
Example 1.3
Open two devices with same address but on different buses.
int busId0 = 0;
int busId1 = 1;
int address = 0x21;
var settings0 = new I2cConnectionSettings(busId0, address);
var settings1 = new I2cConnectionSettings(busId1, address);
var i2cController = new I2cController();
i2cController.OpenDevice(settings0);
i2cController.OpenDevice(settings1);
Example 1.4
Open a new device that implements I2cDevice
to controller.
NOTE: This I2C software implementation doesn't currently exist but is the same concept as the SPI software implementation. See [Proposal] Provide a Software implementation of the SPI protocol for related details.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
int sdaPin = 17;
int sclPin = 18;
var gpioController = new GpioController();
var i2cDevice = new GpioI2cDevice(
settings,
sdaPin,
sclPin,
gpioController);
var i2cController = new I2cController();
i2cController.OpenDevice(i2cDevice);
Example 1.5
Close a device from controller that doesn't contain device with specified address.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
var i2cController = new I2cController();
i2cController.OpenDevice(settings);
i2cController.CloseDevice(new I2cConnectionSettings(busId, 0x45)); // Throws exception because no device with specified address exists.
Example 1.6
Reading from a specific device in controller.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
var i2cController = new I2cController();
i2cController.OpenDevice(settings);
byte value = i2cController.ReadByte(busId, address);
// Or...
Span<byte> buffer = stackalloc byte[3];
i2cController.Read(busId, address, buffer);
Example 1.7
Write to a specific device in controller.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
var i2cController = new I2cController();
i2cController.OpenDevice(settings);
byte value = 0x23;
i2cController.WriteByte(busId, address, value);
// Or...
ReadOnlySpan<byte> buffer = new byte[] { 0x12, 0x23, 0x45 };
i2cController.Write(busId, address, buffer);
2. Disposing of I2cControllers and I2cDevices
NOTE: This dispose pattern needs to fit other controllers to not confuse others, as well.
A new public bool ShouldDispose
property is added to I2cDevice
class for different disposing scenarios. This property will allow controllers/bindings to know if it should dispose device(s) if/when the controller/binding is being disposed. By default, this property should be set to true
as this is the intention in most cases.
public abstract class I2cDevice : IDisposable
{
public bool ShouldDispose { get; set; } => true;
// other current code...
}
Example 2.1
Use controller with default device (ShouldDispose = true).
Example 2.2
Use controller with provided device (ShouldDispose = false).
3. Device Binding Guidance
This section points out a few areas bindings should use that use I2cDevice
.
Binding Constructors
Bindings that use I2C should be the owners of opening the I2cDevice
on the controller. Therefore, the bindings should provide at least 2 constructor overloads that offer the option of using I2cConnectionSettings
or I2cDevice
. This will make it easier when instantiating the bindings with other apps and APIs. It is assumed there will be no other I2C device with the same bus ID and address as it will throw an exception when the binding adds the device to the controller.
Example 3.1
Instantiate a device binding with specified settings without a defined controller.
NOTE: The default controller would be instantiated similar to a master IGpioController
when not defined. Since the controller is not provided and only settings, there is no way to add a specified I2cDevice
. Therefore, the intent is to also create a default I2cDevice
when it is added to controller.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
var mcp23018 = new Mcp23018(settings); // Default controller and default device is opened within.
Example 3.2
Instantiate a device binding with specified device without a defined controller.
NOTE: The default controller would be instantiated similar to a master IGpioController
when not defined. The specified device will then be opened.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
MyI2cDevice device = new MyI2cDevice(settings); // New device that implements I2cDevice.
var mcp23018 = new Mcp23018(myI2cDevice); // Default controller and specified device is opened within.
Example 3.3
Instantiate a device binding with specified settings (or device) and specified controller.
int busId = 1;
int address = 0x20;
var i2cController = new WhizBangI2cController();
// By settings.
var settings = new I2cConnectionSettings(busId, address);
var mcp23018 = new Mcp23018(settings, i2cController);
// Or...
// By device.
var device = I2cController.Create(new I2cConnectionSettings(busId, address));
i2cController.OpenDevice(device, i2cController);
Example 3.4
Instantiate a controller for a binding that has multiple devices with fixed addresses.
See Adding GHI Fez Cream driver for related details.
NOTE: This hardware is a RPi HAT with hard-wired pins on I2C bus 1.
var ghiFezCream = new GhiFezCream(); // Adds all 4 on-board devices to binding.
// Or...
// Adds all 4 on-board devices to binding using default devices on specified controller.
var i2cController = new I2cController();
var ghiFezCream = new GhiFezCream(i2cController);
// Or...
// Adds all 4 on-board devices to binding using specified devices and specified controller.
int busId = 1;
var ghiFezCream = new GhiFezCream(
new GpioI2cDevice(new I2cConnectionSettings(busId, GhiFezCream.Pca9535AddressIc1)),
new GpioI2cDevice(new I2cConnectionSettings(busId, GhiFezCream.Pca9535AddressIc2)),
new GpioI2cDevice(new I2cConnectionSettings(busId, GhiFezCream.Ads7830IpwtAddress)),
new GpioI2cDevice(new I2cConnectionSettings(busId, GhiFezCream.Pca9685Address)),
i2cController);
public class GhiFezCream
{
// Constructor shows it adding fixed devices.
public GhiFezCream(II2cController? i2cController = null)
{
int busId = 1;
i2cController = i2cController ?? new I2cController();
i2cController.OpenDevice(new I2cConnectionSettings(busId, GhiFezCream.Pca9685Address)),
i2cController.OpenDevice(new I2cConnectionSettings(busId, GhiFezCream.Pca9535AddressIc2)),
i2cController.OpenDevice(new I2cConnectionSettings(busId, GhiFezCream.Ads7830IpwtAddress)),
i2cController.OpenDevice(new I2cConnectionSettings(busId, GhiFezCream.Pca9685Address)),
}
// Or you could overload with specified devices.
public GhiFezCream(
I2cDevice pca9535Ic1,
I2cDevice pca9535Ic2 ,
I2cDevice ads7830Ipwt ,
I2cDevice pca9685 ,
II2cController? i2cController = null)
{
i2cController = i2cController ?? new I2cController();
i2cController.OpenDevice(pca9535Ic1);
i2cController.OpenDevice(pca9535Ic2);
i2cController.OpenDevice(ads7830Ipwt);
i2cController.OpenDevice(pca9685);
}
public static int Pca9535AddressIc1 => 0x23;
public static int Pca9535AddressIc2 => 0x25;
public static int Ads7830IpwtAddress => 0x48;
public static int Pca9685Address => 0x70;
}
Example 3.5
Instantiate a controller, opens another I2C device on bus, and then opens a binding.
int busId = 1;
int address = 0x21;
var settings = new I2cConnectionSettings(busId, address);
var i2cController = new I2cController();
i2cController.OpenDevice(settings); // This opens a default device using specified settings.
var bindingSettings = new I2cConnectionSettings(busId, 0x20);
var mcp23018 = new Mcp23018(bindingSettings, i2cController); // Specified controller opens default device within on same bus as other device.
Disposing of Bindings
Bindings should implement IDisposable
where it disposes the related devices.
public class MyBinding : IDisposable
{
public void Dispose()
{
// This will remove the device from controller and only dispose if ShouldDispose is true.
_i2cController.CloseDevice(_i2cSettings);
}
}
Update I2C Device Namespace
โ ๏ธ This topic is related to a future PR that will be a breaking change.
There have been discussions how we should structure/name some of the folders and classes throughout API. I originally thought we should rename I2C devices to use 'Driver' instead to be consistent with other types. For example, I2cDevice
would be renamed I2cDriver
.
My opinions have changed as the new I2C controller fits quite nicely with the current device naming. Although, there is a breaking change needed with the folder structure and related namespace. This could be implemented before the release of v1 in a future PR. So this proposal is in-line to the current device class naming. It only states we need to change the namespace later (hopefully soon after).
// From...
namespace System.Device.I2c.Drivers
// To...
namespace System.Device.I2c.Devices
See [Proposal] Move classes and rename to be consistent throughout API for related details.
Related Issues