From 329906f6b30b1e01d8c9698298c427d90e15c839 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Wed, 12 Nov 2025 20:51:08 +0100 Subject: [PATCH 01/10] A first draft --- src/devices/Ina236/Ina236.cs | 233 ++++++++++++++++++ src/devices/Ina236/Ina236.csproj | 11 + src/devices/Ina236/Ina236.sln | 53 ++++ src/devices/Ina236/Ina236OperatingMode.cs | 24 ++ src/devices/Ina236/Ina236Register.cs | 20 ++ src/devices/Ina236/README.md | 54 ++++ src/devices/Ina236/category.txt | 2 + .../Ina236/samples/Ina236.Samples.csproj | 9 + src/devices/Ina236/samples/Program.cs | 28 +++ 9 files changed, 434 insertions(+) create mode 100644 src/devices/Ina236/Ina236.cs create mode 100644 src/devices/Ina236/Ina236.csproj create mode 100644 src/devices/Ina236/Ina236.sln create mode 100644 src/devices/Ina236/Ina236OperatingMode.cs create mode 100644 src/devices/Ina236/Ina236Register.cs create mode 100644 src/devices/Ina236/README.md create mode 100644 src/devices/Ina236/category.txt create mode 100644 src/devices/Ina236/samples/Ina236.Samples.csproj create mode 100644 src/devices/Ina236/samples/Program.cs diff --git a/src/devices/Ina236/Ina236.cs b/src/devices/Ina236/Ina236.cs new file mode 100644 index 0000000000..c80cab1dd1 --- /dev/null +++ b/src/devices/Ina236/Ina236.cs @@ -0,0 +1,233 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Device; +using System.Device.I2c; +using System.Device.Model; +using UnitsNet; +using static System.Math; + +namespace Iot.Device.Adc +{ + /// + /// INA236 Bidirectional Current/Power monitor. + /// + /// The INA236 current shunt and power monitor with an I2C interface. + /// The INA236 monitors both shunt drop and supply voltage, with programmable conversion + /// times and filtering. A programmable calibration value, combined with an internal multiplier, + /// enables direct readouts in amperes. An additional multiplying register calculates power in watts. + /// + /// + [Interface("INA236 Bidirectional Current/Power monitor")] + public class Ina236 : IDisposable + { + private I2cDevice _i2cDevice; + private ElectricResistance _shuntResistance; + private ElectricCurrent _currentLsb; + private ElectricCurrent _maxCurrent; + private ElectricPotential _voltageLsb; + + /// + /// Construct an Ina236 device using an I2cDevice + /// + /// The I2cDevice initialized to communicate with the INA236. + /// Maximum expected current. Typical values are 8-10 Amps. + /// The resistance of the shunt between input and output. + /// Example breakout boards from manufacturers such as Adafruit have 0.008 Ohms, so try this + /// value if you are unsure. + /// The power dissipation at the resistor is P = I*I*R at the maximum current. + public Ina236(I2cDevice i2cDevice, ElectricResistance shuntResistance, ElectricCurrent maxCurrent) + { + _i2cDevice = i2cDevice ?? throw new ArgumentNullException(nameof(i2cDevice)); + _shuntResistance = shuntResistance; + if (_shuntResistance <= ElectricResistance.Zero) + { + throw new ArgumentOutOfRangeException(nameof(shuntResistance), "The shuntResistance parameter must be greater than zero"); + } + + _maxCurrent = maxCurrent; + + Reset(_shuntResistance, _maxCurrent); + } + + /// + /// Reset the INA236 to default values; + /// + [Command] + public void Reset(ElectricResistance shuntResistance, ElectricCurrent current) + { + // Reset the device by sending a value to the configuration register with the reset bit set. + WriteRegister(Ina236Register.Configuration, 0x8000); + + ushort deviceId = ReadRegisterUnsigned(Ina236Register.DeviceId); + + if ((deviceId & 0xFFF0) != 0xa080) + { + throw new InvalidOperationException($"The device on I2C address {_i2cDevice.ConnectionSettings.DeviceAddress} doesn't seem to be an INA236. Device ID was {deviceId:X4} instead of 0xA080"); + } + + // See datasheet. Use twice the value to allow later rounding + var currentLsbMinimum = current / Pow(2.0, 15) * 2; + double exactCalibrationValue = 0.00512 / (currentLsbMinimum.Amperes * shuntResistance.Ohms); + int valueToSet = (int)(exactCalibrationValue * 1E6); // the value must be set in uA + if (valueToSet > 0xFFFF) + { + throw new InvalidOperationException("Invalid combination of settings - the calibration value is out of spec"); + } + + // Reverse calculation, to get the exact lsb + double exactLsbMinimum = valueToSet / 0.00512 / shuntResistance.Ohms; + + _currentLsb = ElectricCurrent.FromAmperes(exactLsbMinimum); + + // The LSB of the shunt voltage register is 2.5uV if ADCRANGE==0, otherwise it's 625nV. We currently + // do not support ADCRANGE=1, to keep things simple. + _voltageLsb = ElectricPotential.FromMicrovolts(2.5); + WriteRegister(Ina236Register.Calibration, (ushort)valueToSet); + } + + /// + /// Property representing the Operating mode of the INA236 + /// + /// + /// This allows the user to selects continuous, triggered, or power-down mode of operation along with which of the shunt and bus voltage measurements are made. + /// + [Property] + public Ina236OperatingMode OperatingMode + { + get + { + return (Ina236OperatingMode)(ReadRegisterUnsigned(Ina236Register.Configuration) & (ushort)Ina236OperatingMode.ModeMask); + } + set + { + int regValue = ReadRegisterUnsigned(Ina236Register.Configuration); + + regValue &= ~0b111; + regValue |= (int)value; + + WriteRegister(Ina236Register.Configuration, (ushort)regValue); + } + } + + /// + /// Dispose instance + /// + public void Dispose() + { + _i2cDevice?.Dispose(); + _i2cDevice = null!; + } + + /// + /// Read the measured shunt voltage. + /// + /// The shunt potential difference + /// The LSB is 2.5uV when ADCRANGE=0 + [Telemetry("ShuntVoltage")] + public ElectricPotential ReadShuntVoltage() + { + return ReadRegisterUnsigned(Ina236Register.ShuntVoltage) * _voltageLsb; + } + + /// + /// Read the measured Bus voltage. + /// This is the voltage on the primary side of the shunt. + /// + /// The Bus potential (voltage) + /// The LSB is 1.6mV. + [Telemetry("BusVoltage")] + public ElectricPotential ReadBusVoltage() + { + return ReadRegisterUnsigned(Ina236Register.BusVoltage) * ElectricPotential.FromMillivolts(1.6); + } + + /// + /// Read the calculated current through the INA236. + /// + /// + /// This value is determined by an internal calculation using the calibration register and the read shunt voltage and then scaled. + /// The value can be negative, when power flows to the bus. + /// + /// The calculated current + [Telemetry("Current")] + public ElectricCurrent ReadCurrent() + { + return ReadRegisterSigned(Ina236Register.Current) * _currentLsb; + } + + /// + /// Reads the current power consumed by the attached device. + /// + /// The power being used + /// Clarify whether this register is signed or unsigned. Since it is the product of the current and the bus voltage + /// registers, it should be possible to get a negative value, but the documentation says it's always positive + [Telemetry("Power")] + public Power ReadPower() + { + return Power.FromWatts(ReadRegisterUnsigned(Ina236Register.Power) * _currentLsb.Amperes * 32); + } + + /// + /// Read a register from the INA236 device + /// + /// The register to read. + /// Am unsiged short integer representing the regsiter contents. + private ushort ReadRegisterUnsigned(Ina236Register register) + { + Span buffer = stackalloc byte[2]; + + byte registerNumber = (byte)register; + // set a value in the buffer representing the register that we want to read and send it to the INA219 + _i2cDevice.WriteRead(new ReadOnlySpan(ref registerNumber), buffer); + + // read the register back from the INA219. + _i2cDevice.Read(buffer); + + // massage the big endian value read from the INA219 unto a ushort. + return BinaryPrimitives.ReadUInt16BigEndian(buffer); + } + + /// + /// Read a register from the INA236 device + /// + /// The register to read. + /// A signed short integer representing the regsiter contents. + private short ReadRegisterSigned(Ina236Register register) + { + Span buffer = stackalloc byte[2]; + + byte registerNumber = (byte)register; + // set a value in the buffer representing the register that we want to read and send it to the INA219 + _i2cDevice.WriteRead(new ReadOnlySpan(ref registerNumber), buffer); + + // read the register back from the INA219. + _i2cDevice.Read(buffer); + + // massage the big endian value read from the INA219 unto a ushort. + return BinaryPrimitives.ReadInt16BigEndian(buffer); + } + + /// + /// Write a value to an INA236 register. + /// + /// The register to be written to. + /// The value to be written to the register. + private void WriteRegister(Ina236Register register, ushort value) + { + Span buffer = stackalloc byte[3]; + + // set the first byte of the buffer to the register to be written + buffer[0] = (byte)register; + + // write the value to be written to the second and third bytes in big-endian order. + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(1, 2), value); + + // write the value to the register via the I2c Bus. + _i2cDevice.Write(buffer); + } + } +} diff --git a/src/devices/Ina236/Ina236.csproj b/src/devices/Ina236/Ina236.csproj new file mode 100644 index 0000000000..b116cbab81 --- /dev/null +++ b/src/devices/Ina236/Ina236.csproj @@ -0,0 +1,11 @@ + + + $(DefaultBindingTfms) + + false + + + + + + \ No newline at end of file diff --git a/src/devices/Ina236/Ina236.sln b/src/devices/Ina236/Ina236.sln new file mode 100644 index 0000000000..7432d61468 --- /dev/null +++ b/src/devices/Ina236/Ina236.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36603.0 d17.14 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{0D478BAB-AFEA-4AF1-866C-E3AC32E11C5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236.Samples", "samples\Ina236.Samples.csproj", "{D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236", "Ina236.csproj", "{1398DC15-97F0-4048-965A-CCB3D44BFE06}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|x64.Build.0 = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Debug|x86.Build.0 = Debug|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|Any CPU.Build.0 = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|x64.ActiveCfg = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|x64.Build.0 = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|x86.ActiveCfg = Release|Any CPU + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6}.Release|x86.Build.0 = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|x64.ActiveCfg = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|x64.Build.0 = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|x86.ActiveCfg = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Debug|x86.Build.0 = Debug|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|Any CPU.Build.0 = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x64.ActiveCfg = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x64.Build.0 = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x86.ActiveCfg = Release|Any CPU + {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6} = {0D478BAB-AFEA-4AF1-866C-E3AC32E11C5A} + EndGlobalSection +EndGlobal diff --git a/src/devices/Ina236/Ina236OperatingMode.cs b/src/devices/Ina236/Ina236OperatingMode.cs new file mode 100644 index 0000000000..7e86ea6ea1 --- /dev/null +++ b/src/devices/Ina236/Ina236OperatingMode.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Iot.Device.Adc +{ + /// + /// Current device operating mode + /// + public enum Ina236OperatingMode + { + Shutdown = 0, + SingeShuntVoltage = 0b001, + SingleBusVoltage = 0b010, + SingleShuntAndBusVoltage = 0b011, + Shutdown2 = 0b100, + ContinuousShuntVoltage = 0b101, + ContinuousBusVoltage = 0b110, + ContinuousShuntAndBusVoltage = 0b111, // This is the default setting + ModeMask = 0b111 + } +} diff --git a/src/devices/Ina236/Ina236Register.cs b/src/devices/Ina236/Ina236Register.cs new file mode 100644 index 0000000000..1ad6760bb1 --- /dev/null +++ b/src/devices/Ina236/Ina236Register.cs @@ -0,0 +1,20 @@ +namespace Iot.Device.Adc; + +#pragma warning disable CS1591 + +/// +/// The Ina236 Register map. Note that all registers are 16 bit wide. +/// +public enum Ina236Register +{ + Configuration = 0, + ShuntVoltage = 1, + BusVoltage = 2, + Power = 3, + Current = 4, + Calibration = 5, + MaskEnable = 6, + AlertLimit = 7, + ManufacturerId = 0x3E, + DeviceId = 0x3F +} diff --git a/src/devices/Ina236/README.md b/src/devices/Ina236/README.md new file mode 100644 index 0000000000..938fd9847e --- /dev/null +++ b/src/devices/Ina236/README.md @@ -0,0 +1,54 @@ +# INA219 - Bidirectional Current/Power Monitor + +The INA236 is a current shunt and power monitor with an I2C-compatible interface. It is an improved version of the INA219 with a higher accuracy and an extra voltage sensor for the secondary side. The device monitors both shunt voltage drop and bus supply voltage, with programmable conversion times and filtering. A programmable calibration value, combined with an internal multiplier, enables direct readouts of current in amperes. An additional multiplying register calculates power in watts. + +* Senses Bus Voltages from 0 to 26 V +* Reports Current, Voltage, and Power +* 16 Programmable Addresses +* High Accuracy: 0.5% (Maximum) Over Temperature +* Filtering Options +* Calibration Registers + +## Documentation + +* [INA236 Datasheet](http://www.ti.com/lit/ds/symlink/ina236.pdf) + +## Usage + +```csharp +const byte Adafruit_Ina236_I2cAddress = 0x40; + +// create an INA236 device on I2C bus 1 addressing channel 64 +using (Ina219 device = new Ina219(new I2cConnectionSettings(Adafruit_Ina236_I2cBus, Adafruit_Ina219_I2cAddress))) +{ + // reset the device + device.Reset(); + + // set up the bus and shunt voltage ranges and the calibration. Other values left at default. + device.BusVoltageRange = Ina219BusVoltageRange.Range16v; + device.PgaSensitivity = Ina219PgaSensitivity.PlusOrMinus40mv; + device.SetCalibration(33574, (float)12.2e-6); + + while (true) + { + // write out the current values from the INA219 device. + System.Console.WriteLine($"Bus Voltage {device.ReadBusVoltage()}V Shunt Voltage {device.ReadShuntVoltage() * 1000}mV Current {device.ReadCurrent() * 1000}mA Power {device.ReadPower() * 1000}mW"); + System.Threading.Thread.Sleep(1000); + } +} +``` + +### Notes + +This sample uses an Adafruit INA219 breakout board and monitors a LED wired into the 3.3 volts supply with a 150 ohm current limiting resistor. It prints the bus voltage, shunt voltage, current and power every second. + +The configuration and calibration is determinined as follows. + +* The bus voltage range can be either 16v or 32v. As this example uses a 3.3v supply then the 16v bus voltage range is chosen +* The current through the LED is in the low tens of milliamps. If we take 50mA as a reasonable maximum current that we may want to see then the maximum voltage accross the shunt resistor is 0.1 Ohms x 50mA which works out +at 5mV. Given this we can use a shunt voltage range of +/- 40mV +* The maximum possible current would then be 40mV / 0.1 = 400mA +* With a 400mA maximum current and a range of the ADC of 15bits then the LSB of the current would be 400mA/32767 = 12.2207 microamps. We will chose 12.2uA as a round number. +* From the [INA219 Datasheet](http://www.ti.com/lit/ds/symlink/ina219.pdf) the calibration register should be set at 0.04096/(currentLSB * shunt resistance) = 33574 = 0x8326 + +![circuit](Ina219.Sample_bb.png) diff --git a/src/devices/Ina236/category.txt b/src/devices/Ina236/category.txt new file mode 100644 index 0000000000..3954b07309 --- /dev/null +++ b/src/devices/Ina236/category.txt @@ -0,0 +1,2 @@ +adc +power diff --git a/src/devices/Ina236/samples/Ina236.Samples.csproj b/src/devices/Ina236/samples/Ina236.Samples.csproj new file mode 100644 index 0000000000..ef7379a624 --- /dev/null +++ b/src/devices/Ina236/samples/Ina236.Samples.csproj @@ -0,0 +1,9 @@ + + + Exe + $(DefaultSampleTfms) + + + + + \ No newline at end of file diff --git a/src/devices/Ina236/samples/Program.cs b/src/devices/Ina236/samples/Program.cs new file mode 100644 index 0000000000..56f019005f --- /dev/null +++ b/src/devices/Ina236/samples/Program.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Device.I2c; +using Iot.Device; +using Iot.Device.Adc; + +const byte Adafruit_Ina219_I2cAddress = 0x40; +const byte Adafruit_Ina219_I2cBus = 0x1; + +// create an INA219 device on I2C bus 1 addressing channel 64 +using Ina219 device = new(new I2cConnectionSettings(Adafruit_Ina219_I2cBus, Adafruit_Ina219_I2cAddress)); +// reset the device +device.Reset(); + +// set up the bus and shunt voltage ranges and the calibration. Other values left at default. +device.BusVoltageRange = Ina219BusVoltageRange.Range16v; +device.PgaSensitivity = Ina219PgaSensitivity.PlusOrMinus40mv; +device.SetCalibration(33574, 12.2e-6f); + +while (true) +{ + // write out the current values from the INA219 device. + Console.WriteLine($"Bus Voltage {device.ReadBusVoltage()} Shunt Voltage {device.ReadShuntVoltage().Millivolts}mV Current {device.ReadCurrent()} Power {device.ReadPower()}"); + Thread.Sleep(1000); +} From 6e98a2df0880c08e3885da1650e255cde13e56ba Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Thu, 13 Nov 2025 21:47:06 +0100 Subject: [PATCH 02/10] Compiles successfully --- src/devices/Ina236/Ina236.cs | 7 +++ src/devices/Ina236/Ina236.sln | 17 +++++++ src/devices/Ina236/Ina236OperatingMode.cs | 45 ++++++++++++++++--- src/devices/Ina236/Ina236Register.cs | 7 +-- .../Ina236/samples/Ina236.Samples.csproj | 1 + src/devices/Ina236/samples/Program.cs | 19 +++----- 6 files changed, 74 insertions(+), 22 deletions(-) diff --git a/src/devices/Ina236/Ina236.cs b/src/devices/Ina236/Ina236.cs index c80cab1dd1..0f2d9eeeaf 100644 --- a/src/devices/Ina236/Ina236.cs +++ b/src/devices/Ina236/Ina236.cs @@ -24,6 +24,13 @@ namespace Iot.Device.Adc [Interface("INA236 Bidirectional Current/Power monitor")] public class Ina236 : IDisposable { + /// + /// The default I2C Address for this device. + /// According to the datasheet, the device comes in two variants, A and B. + /// Type A has addresses 0x80 to 0x83, depending on whether the ADDR pin is connected to GND, VS, SDA or SCL. + /// Type B has addresses 0x90 to 0x94, depending on the ADDR pin. + /// + public const int DefaultI2cAddress = 0x80; private I2cDevice _i2cDevice; private ElectricResistance _shuntResistance; private ElectricCurrent _currentLsb; diff --git a/src/devices/Ina236/Ina236.sln b/src/devices/Ina236/Ina236.sln index 7432d61468..dad8eca4c4 100644 --- a/src/devices/Ina236/Ina236.sln +++ b/src/devices/Ina236/Ina236.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236.Samples", "samples\I EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236", "Ina236.csproj", "{1398DC15-97F0-4048-965A-CCB3D44BFE06}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arduino", "..\Arduino\Arduino.csproj", "{5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,18 @@ Global {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x64.Build.0 = Release|Any CPU {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x86.ActiveCfg = Release|Any CPU {1398DC15-97F0-4048-965A-CCB3D44BFE06}.Release|x86.Build.0 = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|x64.ActiveCfg = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|x64.Build.0 = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|x86.ActiveCfg = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Debug|x86.Build.0 = Debug|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|Any CPU.Build.0 = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x64.ActiveCfg = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x64.Build.0 = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x86.ActiveCfg = Release|Any CPU + {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -50,4 +64,7 @@ Global GlobalSection(NestedProjects) = preSolution {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6} = {0D478BAB-AFEA-4AF1-866C-E3AC32E11C5A} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {34E688F0-222B-4C21-A3C2-B6C54D9D78BF} + EndGlobalSection EndGlobal diff --git a/src/devices/Ina236/Ina236OperatingMode.cs b/src/devices/Ina236/Ina236OperatingMode.cs index 7e86ea6ea1..42d6627598 100644 --- a/src/devices/Ina236/Ina236OperatingMode.cs +++ b/src/devices/Ina236/Ina236OperatingMode.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. namespace Iot.Device.Adc { @@ -11,14 +8,50 @@ namespace Iot.Device.Adc /// public enum Ina236OperatingMode { + /// + /// The device is off + /// Shutdown = 0, + + /// + /// Generate a single measurement of the shunt voltage value, then wait for a command again + /// SingeShuntVoltage = 0b001, + + /// + /// Generate s single measurement of the shunt voltage value, then wait for a command again + /// SingleBusVoltage = 0b010, + + /// + /// Generate a single measurement of both bus voltage and shunt voltage, then wait for a command again + /// SingleShuntAndBusVoltage = 0b011, + + /// + /// Enter shutdown mode + /// Shutdown2 = 0b100, + + /// + /// Continuously measure the shunt voltage + /// ContinuousShuntVoltage = 0b101, + + /// + /// Continuously measure the bus voltage + /// ContinuousBusVoltage = 0b110, - ContinuousShuntAndBusVoltage = 0b111, // This is the default setting + + /// + /// Continuously measure both bus and shut voltages. + /// This is the default setting. + /// + ContinuousShuntAndBusVoltage = 0b111, + + /// + /// A mask field + /// ModeMask = 0b111 } } diff --git a/src/devices/Ina236/Ina236Register.cs b/src/devices/Ina236/Ina236Register.cs index 1ad6760bb1..6f9622a720 100644 --- a/src/devices/Ina236/Ina236Register.cs +++ b/src/devices/Ina236/Ina236Register.cs @@ -1,11 +1,12 @@ -namespace Iot.Device.Adc; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. -#pragma warning disable CS1591 +namespace Iot.Device.Adc; /// /// The Ina236 Register map. Note that all registers are 16 bit wide. /// -public enum Ina236Register +internal enum Ina236Register { Configuration = 0, ShuntVoltage = 1, diff --git a/src/devices/Ina236/samples/Ina236.Samples.csproj b/src/devices/Ina236/samples/Ina236.Samples.csproj index ef7379a624..6ab982f9ef 100644 --- a/src/devices/Ina236/samples/Ina236.Samples.csproj +++ b/src/devices/Ina236/samples/Ina236.Samples.csproj @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/src/devices/Ina236/samples/Program.cs b/src/devices/Ina236/samples/Program.cs index 56f019005f..d9edfa7af6 100644 --- a/src/devices/Ina236/samples/Program.cs +++ b/src/devices/Ina236/samples/Program.cs @@ -6,21 +6,14 @@ using System.Device.I2c; using Iot.Device; using Iot.Device.Adc; +using Iot.Device.Arduino; +using UnitsNet; -const byte Adafruit_Ina219_I2cAddress = 0x40; -const byte Adafruit_Ina219_I2cBus = 0x1; +using ArduinoBoard board = new ArduinoBoard("COM4", 115200); +using Ina236 device = new(board.CreateI2cDevice(new I2cConnectionSettings(0, 0x80)), ElectricResistance.FromMilliohms(8), + ElectricCurrent.FromAmperes(10.0)); -// create an INA219 device on I2C bus 1 addressing channel 64 -using Ina219 device = new(new I2cConnectionSettings(Adafruit_Ina219_I2cBus, Adafruit_Ina219_I2cAddress)); -// reset the device -device.Reset(); - -// set up the bus and shunt voltage ranges and the calibration. Other values left at default. -device.BusVoltageRange = Ina219BusVoltageRange.Range16v; -device.PgaSensitivity = Ina219PgaSensitivity.PlusOrMinus40mv; -device.SetCalibration(33574, 12.2e-6f); - -while (true) +while (!Console.KeyAvailable) { // write out the current values from the INA219 device. Console.WriteLine($"Bus Voltage {device.ReadBusVoltage()} Shunt Voltage {device.ReadShuntVoltage().Millivolts}mV Current {device.ReadCurrent()} Power {device.ReadPower()}"); From 69b0b93c7926d48c60403a7167bdcdce1433da6a Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sun, 23 Nov 2025 13:48:03 +0100 Subject: [PATCH 03/10] Calibration calculation now seemingly correct --- src/devices/Ina236/Ina236.cs | 17 +++++++++++------ src/devices/Ina236/samples/Program.cs | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/devices/Ina236/Ina236.cs b/src/devices/Ina236/Ina236.cs index 0f2d9eeeaf..5ec397dd6c 100644 --- a/src/devices/Ina236/Ina236.cs +++ b/src/devices/Ina236/Ina236.cs @@ -27,10 +27,10 @@ public class Ina236 : IDisposable /// /// The default I2C Address for this device. /// According to the datasheet, the device comes in two variants, A and B. - /// Type A has addresses 0x80 to 0x83, depending on whether the ADDR pin is connected to GND, VS, SDA or SCL. - /// Type B has addresses 0x90 to 0x94, depending on the ADDR pin. + /// Type A has addresses 0x40 to 0x43, depending on whether the ADDR pin is connected to GND, VS, SDA or SCL. + /// Type B has addresses 0x44 to 0x47, depending on the ADDR pin. /// - public const int DefaultI2cAddress = 0x80; + public const int DefaultI2cAddress = 0x40; private I2cDevice _i2cDevice; private ElectricResistance _shuntResistance; private ElectricCurrent _currentLsb; @@ -77,16 +77,21 @@ public void Reset(ElectricResistance shuntResistance, ElectricCurrent current) } // See datasheet. Use twice the value to allow later rounding - var currentLsbMinimum = current / Pow(2.0, 15) * 2; + ElectricCurrent currentLsbMinimum = current / Pow(2.0, 15) * 2; double exactCalibrationValue = 0.00512 / (currentLsbMinimum.Amperes * shuntResistance.Ohms); - int valueToSet = (int)(exactCalibrationValue * 1E6); // the value must be set in uA + int valueToSet = (int)exactCalibrationValue; if (valueToSet > 0xFFFF) { throw new InvalidOperationException("Invalid combination of settings - the calibration value is out of spec"); } // Reverse calculation, to get the exact lsb - double exactLsbMinimum = valueToSet / 0.00512 / shuntResistance.Ohms; + // Solve this for x: + // calibrationValue = 0.00512 / (x * resistance) // * (x * resistance) + // calibrationValue * (x * resistance) = 0.00512 // / resistance + // calibrationValue * x = 0.00512 / resistance // / calibrationvalue + // x = 0.00512 / resistance / calibrationValue + double exactLsbMinimum = 0.00512 / shuntResistance.Ohms / valueToSet; _currentLsb = ElectricCurrent.FromAmperes(exactLsbMinimum); diff --git a/src/devices/Ina236/samples/Program.cs b/src/devices/Ina236/samples/Program.cs index d9edfa7af6..be12b9b9a6 100644 --- a/src/devices/Ina236/samples/Program.cs +++ b/src/devices/Ina236/samples/Program.cs @@ -9,8 +9,8 @@ using Iot.Device.Arduino; using UnitsNet; -using ArduinoBoard board = new ArduinoBoard("COM4", 115200); -using Ina236 device = new(board.CreateI2cDevice(new I2cConnectionSettings(0, 0x80)), ElectricResistance.FromMilliohms(8), +using ArduinoBoard board = new ArduinoBoard("COM5", 115200); +using Ina236 device = new(board.CreateI2cDevice(new I2cConnectionSettings(0, 0x40)), ElectricResistance.FromMilliohms(8), ElectricCurrent.FromAmperes(10.0)); while (!Console.KeyAvailable) From 2583e29cd8c1d950a8bf7a4c93f154837021a238 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sun, 30 Nov 2025 20:25:39 +0100 Subject: [PATCH 04/10] Add additional properties --- src/devices/Ina236/Ina236.cs | 130 ++++++++++++++++++++++++++ src/devices/Ina236/samples/Program.cs | 6 ++ 2 files changed, 136 insertions(+) diff --git a/src/devices/Ina236/Ina236.cs b/src/devices/Ina236/Ina236.cs index 5ec397dd6c..d38db54a0b 100644 --- a/src/devices/Ina236/Ina236.cs +++ b/src/devices/Ina236/Ina236.cs @@ -125,6 +125,136 @@ public Ina236OperatingMode OperatingMode } } + /// + /// How many samples should be combined into one result. + /// A high value returns less often a new reading, but is more stable. + /// + /// Valid values are: 1, 4, 16, 64, 128, 256, 512 and 1024. Other values will be rounded accordingly. + /// + /// + [Property] + public uint AverageOverNoSamples + { + get + { + int reg = (ReadRegisterUnsigned(Ina236Register.Configuration) >> 9) & 0x7; + return reg switch + { + 0b000 => 1, + 0b001 => 4, + 0b010 => 16, + 0b011 => 64, + 0b100 => 128, + 0b101 => 256, + 0b110 => 512, + 0b111 => 1024, + _ => throw new InvalidOperationException("This is not possible") + }; + } + set + { + int valueToSet = value switch + { + <= 1 => 0b000, + <= 4 => 0b001, + <= 16 => 0b010, + <= 64 => 0b011, + <= 128 => 0b100, + <= 256 => 0b101, + <= 512 => 0b110, + >= 513 => 0b111, + }; + + int reg = ReadRegisterUnsigned(Ina236Register.Configuration) & 0xF1FF; + reg = reg | (valueToSet << 9); + WriteRegister(Ina236Register.Configuration, (ushort)reg); + } + } + + /// + /// Conversion time for a single bus value, in microseconds + /// + /// Valid values are: 140, 204, 332, 588, 1100 (default), 2116, 4156 and 8244us. Other values will be rounded + public int BusConversionTime + { + get + { + int reg = (ReadRegisterUnsigned(Ina236Register.Configuration) >> 6) & 0x7; + return ConversionPeriodFromValue(reg); + } + + set + { + int valueToSet = ValueFromConversionPeriod(value); + int reg = ReadRegisterUnsigned(Ina236Register.Configuration) & 0xFE3F; + reg = reg | (valueToSet << 6); + WriteRegister(Ina236Register.Configuration, (ushort)reg); + } + } + + /// + /// Conversion time for a single shunt value, in microseconds + /// + /// Valid values are: 140, 204, 332, 588, 1100 (default), 2116, 4156 and 8244us. Other values will be rounded + public int ShuntConversionTime + { + get + { + int reg = (ReadRegisterUnsigned(Ina236Register.Configuration) >> 3) & 0x7; + return ConversionPeriodFromValue(reg); + } + + set + { + int valueToSet = ValueFromConversionPeriod(value); + int reg = ReadRegisterUnsigned(Ina236Register.Configuration) & 0xFFC7; + reg = reg | (valueToSet << 3); + WriteRegister(Ina236Register.Configuration, (ushort)reg); + } + } + + /// + /// Converts the given conversion period in us into the binary equivalent + /// + /// Period in microseconds + /// An integer value to be set to the register (with appropriate shift, used for VBUSCT and VSHCT + /// in the configuration register) + private int ValueFromConversionPeriod(int period) + { + return period switch + { + <= 140 => 0b000, + <= 204 => 0b001, + <= 332 => 0b010, + <= 588 => 0b011, + <= 1100 => 0b100, + <= 2116 => 0b101, + <= 4156 => 0b110, + >= 4157 => 0b111 + }; + } + + /// + /// Inverse of the above + /// + /// The time period + /// The bit value for the conversion register + private int ConversionPeriodFromValue(int period) + { + return period switch + { + 0b000 => 140, + 0b001 => 204, + 0b010 => 332, + 0b011 => 588, + 0b100 => 1100, + 0b101 => 2116, + 0b110 => 4156, + 0b111 => 8244, + _ => throw new InvalidOperationException("This cannot really happen") + }; + } + /// /// Dispose instance /// diff --git a/src/devices/Ina236/samples/Program.cs b/src/devices/Ina236/samples/Program.cs index be12b9b9a6..620e7ab6fc 100644 --- a/src/devices/Ina236/samples/Program.cs +++ b/src/devices/Ina236/samples/Program.cs @@ -13,6 +13,12 @@ using Ina236 device = new(board.CreateI2cDevice(new I2cConnectionSettings(0, 0x40)), ElectricResistance.FromMilliohms(8), ElectricCurrent.FromAmperes(10.0)); +Console.WriteLine("Device initialized. Default settings used:"); +Console.WriteLine($"Operating Mode: {device.OperatingMode}"); +Console.WriteLine($"Number of Samples to average: {device.AverageOverNoSamples}"); +Console.WriteLine($"Bus conversion time: {device.BusConversionTime}us"); +Console.WriteLine($"Shunt conversion time: {device.ShuntConversionTime}us"); + while (!Console.KeyAvailable) { // write out the current values from the INA219 device. From 69dc380cd935281db8f0edb6931d9bd9123c7211 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sat, 6 Dec 2025 16:06:54 +0100 Subject: [PATCH 05/10] Some first test cases --- src/devices/Common/Common.csproj | 1 + .../Device/I2c/I2cSimulatedDeviceBase.cs | 126 ++++++++++++++++++ src/devices/Ina236/Ina236.sln | 33 ++++- src/devices/Ina236/tests/Ina236.Tests.csproj | 14 ++ src/devices/Ina236/tests/Ina236Tests.cs | 39 ++++++ src/devices/Ina236/tests/SimulatedIna236.cs | 25 ++++ 6 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs create mode 100644 src/devices/Ina236/tests/Ina236.Tests.csproj create mode 100644 src/devices/Ina236/tests/Ina236Tests.cs create mode 100644 src/devices/Ina236/tests/SimulatedIna236.cs diff --git a/src/devices/Common/Common.csproj b/src/devices/Common/Common.csproj index 4dbd080868..b0297d729f 100644 --- a/src/devices/Common/Common.csproj +++ b/src/devices/Common/Common.csproj @@ -12,6 +12,7 @@ + diff --git a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs new file mode 100644 index 0000000000..61330a6e74 --- /dev/null +++ b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace System.Device.I2c; + +/// +/// This class can be used to create a simulated I2C device. +/// Derive from it and implement the and commands +/// to behave as expected. +/// Can also serve as base for a testing mock. +/// +public abstract class I2cSimulatedDeviceBase : I2cDevice +{ + private bool _disposed; + + /// + /// Default constructor + /// + /// The connection settings for this device. + public I2cSimulatedDeviceBase(I2cConnectionSettings settings) + { + ConnectionSettings = settings; + _disposed = false; + } + + /// + /// The active connection settings + /// + public override I2cConnectionSettings ConnectionSettings { get; } + + /// + /// Reads a byte from the bus + /// + /// + /// The instance is disposed already + /// + public override byte ReadByte() + { + if (_disposed) + { + throw new ObjectDisposedException("This instance is disposed"); + } + + byte[] buffer = new byte[1]; + if (WriteRead([], buffer) == 1) + { + return buffer[0]; + } + + throw new IOException("Unable to read a byte from the device"); + } + + /// + /// This method should implement the read operation from the device. + /// + /// Buffer with input data to the device, buffer[0] is usually the command byte + /// The return data from the device + /// How many bytes where read. Should usually match the length of the output buffer + /// This doesn't use as argument type to be mockable + protected abstract int WriteRead(byte[] inputBuffer, byte[] outputBuffer); + + /// + public override void Read(Span buffer) + { + byte[] buffer2 = buffer.ToArray(); + if (WriteRead([], buffer2) == buffer.Length) + { + buffer2.CopyTo(buffer); + } + + throw new IOException($"Unable to read {buffer.Length} bytes from the device"); + } + + /// + public override void WriteByte(byte value) + { + if (WriteRead([value], []) == 1) + { + return; + } + + throw new IOException("Unable to write a byte to the device"); + } + + /// + public override void Write(ReadOnlySpan buffer) + { + WriteRead(buffer.ToArray(), []); + } + + /// + public override void WriteRead(ReadOnlySpan writeBuffer, Span readBuffer) + { + byte[] outBuffer = new byte[readBuffer.Length]; + if (WriteRead(writeBuffer.ToArray(), outBuffer) != readBuffer.Length) + { + throw new IOException($"Unable to read {readBuffer.Length} bytes from the device"); + } + + outBuffer.CopyTo(readBuffer); + } + + /// + protected override void Dispose(bool disposing) + { + _disposed = true; + base.Dispose(disposing); + } + + /// + public override ComponentInformation QueryComponentInformation() + { + var self = new ComponentInformation(this, "Simulated I2C Device"); + self.Properties["BusNo"] = ConnectionSettings.BusId.ToString(CultureInfo.InvariantCulture); + self.Properties["DeviceAddress"] = $"0x{ConnectionSettings.DeviceAddress:x2}"; + return self; + } +} diff --git a/src/devices/Ina236/Ina236.sln b/src/devices/Ina236/Ina236.sln index dad8eca4c4..57c4f40ceb 100644 --- a/src/devices/Ina236/Ina236.sln +++ b/src/devices/Ina236/Ina236.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36603.0 d17.14 +VisualStudioVersion = 17.14.36603.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{0D478BAB-AFEA-4AF1-866C-E3AC32E11C5A}" EndProject @@ -11,6 +11,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236", "Ina236.csproj", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arduino", "..\Arduino\Arduino.csproj", "{5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ina236.Tests", "tests\Ina236.Tests.csproj", "{7328E5E9-483A-00C5-2E02-D2796B496CD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "..\Common\Common.csproj", "{9F0601AB-EA31-A20F-1B21-30C901BDC579}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,12 +63,37 @@ Global {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x64.Build.0 = Release|Any CPU {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x86.ActiveCfg = Release|Any CPU {5D91E9F7-12AC-CAB9-87BD-975F0E21D50A}.Release|x86.Build.0 = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|x64.Build.0 = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Debug|x86.Build.0 = Debug|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|Any CPU.Build.0 = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|x64.ActiveCfg = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|x64.Build.0 = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|x86.ActiveCfg = Release|Any CPU + {7328E5E9-483A-00C5-2E02-D2796B496CD2}.Release|x86.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x64.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x86.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x86.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|Any CPU.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x64.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x64.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x86.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {D9FEFE09-18E7-458A-8ECC-73A8D1E078F6} = {0D478BAB-AFEA-4AF1-866C-E3AC32E11C5A} + {7328E5E9-483A-00C5-2E02-D2796B496CD2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {34E688F0-222B-4C21-A3C2-B6C54D9D78BF} diff --git a/src/devices/Ina236/tests/Ina236.Tests.csproj b/src/devices/Ina236/tests/Ina236.Tests.csproj new file mode 100644 index 0000000000..7332effc31 --- /dev/null +++ b/src/devices/Ina236/tests/Ina236.Tests.csproj @@ -0,0 +1,14 @@ + + + + $(DefaultSampleTfms) + 10 + false + false + + + + + + + diff --git a/src/devices/Ina236/tests/Ina236Tests.cs b/src/devices/Ina236/tests/Ina236Tests.cs new file mode 100644 index 0000000000..530476c66b --- /dev/null +++ b/src/devices/Ina236/tests/Ina236Tests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Device.Gpio; +using System.Device.I2c; +using System.Device.Spi; +using System.Drawing; +using Ina236.Tests; +using Iot.Device.Graphics; +using Moq; +using UnitsNet; +using Xunit; + +namespace Iot.Device.Adc.Tests +{ + public sealed class Ina236Tests : IDisposable + { + private Ina236 _ina236; + + public Ina236Tests() + { + _ina236 = new Ina236(new SimulatedIna236(new I2cConnectionSettings(1, 0x40)), ElectricResistance.FromMilliohms(8), ElectricCurrent.FromAmperes(10)); + } + + [Fact] + public void InitialValues() + { + Assert.Equal(Ina236OperatingMode.ContinuousShuntAndBusVoltage, _ina236.OperatingMode); + Assert.Equal(1u, _ina236.AverageOverNoSamples); + Assert.Equal(1100, _ina236.BusConversionTime); + Assert.Equal(1100, _ina236.ShuntConversionTime); + } + + public void Dispose() + { + } + } +} diff --git a/src/devices/Ina236/tests/SimulatedIna236.cs b/src/devices/Ina236/tests/SimulatedIna236.cs new file mode 100644 index 0000000000..9a66026491 --- /dev/null +++ b/src/devices/Ina236/tests/SimulatedIna236.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Device.I2c; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ina236.Tests +{ + internal class SimulatedIna236 : I2cSimulatedDeviceBase + { + public SimulatedIna236(I2cConnectionSettings settings) + : base(settings) + { + } + + protected override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) + { + throw new NotImplementedException(); + } + } +} From 6a0b617e034b399ae9561394cad6271871b12184 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sat, 6 Dec 2025 21:03:44 +0100 Subject: [PATCH 06/10] Add generic helper classes to simulate I2C devices --- .../Device/I2c/I2cSimulatedDeviceBase.cs | 106 ++++++++++++++++++ src/devices/Ina236/tests/SimulatedIna236.cs | 22 +++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs index 61330a6e74..3a6d0edf8c 100644 --- a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs +++ b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs @@ -6,8 +6,10 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Numerics; using System.Text; using System.Threading.Tasks; +using UnitsNet; namespace System.Device.I2c; @@ -20,6 +22,7 @@ namespace System.Device.I2c; public abstract class I2cSimulatedDeviceBase : I2cDevice { private bool _disposed; + private Dictionary _registerMap; /// /// Default constructor @@ -28,9 +31,15 @@ public abstract class I2cSimulatedDeviceBase : I2cDevice public I2cSimulatedDeviceBase(I2cConnectionSettings settings) { ConnectionSettings = settings; + _registerMap = new Dictionary(); _disposed = false; } + /// + /// The registermap of this device. + /// + protected Dictionary RegisterMap => _registerMap; + /// /// The active connection settings /// @@ -123,4 +132,101 @@ public override ComponentInformation QueryComponentInformation() self.Properties["DeviceAddress"] = $"0x{ConnectionSettings.DeviceAddress:x2}"; return self; } + + /// + /// Base class for generic register access + /// + public abstract record class RegisterBase : IComparable + { + /// + /// Writes the register, regardless of its actual type + /// + /// The value to write + public abstract void WriteRegister(int value); + + /// + /// Reads the register value regardless of its actual type + /// + /// The register value, sign-extended to int + public abstract int ReadRegister(); + + /// + public abstract int CompareTo(object? obj); + } + + /// + /// Represents a register value + /// + /// Size of the register, usually byte or int + public record class Register : RegisterBase + where T : struct, IEquatable, INumber, IComparable + { + private T _value; + + /// + /// Event that is raised when the register is written + /// + public event Action? ValueChanged; + + /// + /// Create a new register + /// + public Register() + : this(default(T)) + { + } + + /// + /// Creates a new register + /// + /// The initial (power-on-reset) value of the register + public Register(T initialValue) + { + _value = initialValue; + } + + /// + /// The current value of the register + /// + public T Value + { + get + { + return _value; + } + set + { + _value = value; + ValueChanged?.Invoke(_value); + } + } + + /// + public override void WriteRegister(int value) + { + Value = T.CreateChecked(value); + } + + /// + public override int ReadRegister() + { + return int.CreateChecked(Value); + } + + /// + public override int CompareTo(object? obj) + { + if (obj == null) + { + return 1; + } + + if (obj is Register t1) + { + return _value.CompareTo(t1._value); + } + + throw new ArgumentException("These types can't be compared"); + } + } } diff --git a/src/devices/Ina236/tests/SimulatedIna236.cs b/src/devices/Ina236/tests/SimulatedIna236.cs index 9a66026491..c9b8c964d2 100644 --- a/src/devices/Ina236/tests/SimulatedIna236.cs +++ b/src/devices/Ina236/tests/SimulatedIna236.cs @@ -12,14 +12,34 @@ namespace Ina236.Tests { internal class SimulatedIna236 : I2cSimulatedDeviceBase { + private byte _currentRegister; + public SimulatedIna236(I2cConnectionSettings settings) : base(settings) { + _currentRegister = 0; + RegisterMap.Add(0, new Register(0x4127)); } protected override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) { - throw new NotImplementedException(); + if (inputBuffer.Length > 0) + { + _currentRegister = inputBuffer[0]; + if (inputBuffer.Length >= 3 && RegisterMap.TryGetValue(_currentRegister, out var register)) + { + ushort reg = BitConverter.ToUInt16(inputBuffer, 1); + register.WriteRegister(reg); + } + } + + // All registers of this device are 16 bit, so we need to read that or nothing + if (outputBuffer.Length >= 2 && RegisterMap.TryGetValue(_currentRegister, out var register2)) + { + ushort ret = (ushort)register2.ReadRegister(); + } + + return outputBuffer.Length; } } } From 72c3786b975be291d74d485b02758cef0ec49080 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sun, 7 Dec 2025 16:35:28 +0100 Subject: [PATCH 07/10] Complex simulation infrastructure - but it has issues Generic implementation is difficult, one problem is the different argument sizes and the fact that the endianess on the bus may change. --- .../Device/I2c/I2cSimulatedDeviceBase.cs | 101 ++++++++++++++++-- src/devices/Ina236/Ina236.cs | 6 -- src/devices/Ina236/tests/Ina236Tests.cs | 12 ++- src/devices/Ina236/tests/SimulatedIna236.cs | 40 +++++-- 4 files changed, 135 insertions(+), 24 deletions(-) diff --git a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs index 3a6d0edf8c..36003bf08c 100644 --- a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs +++ b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using UnitsNet; @@ -23,6 +24,7 @@ public abstract class I2cSimulatedDeviceBase : I2cDevice { private bool _disposed; private Dictionary _registerMap; + private byte _currentRegister; /// /// Default constructor @@ -33,18 +35,37 @@ public I2cSimulatedDeviceBase(I2cConnectionSettings settings) ConnectionSettings = settings; _registerMap = new Dictionary(); _disposed = false; + _currentRegister = 0; } /// /// The registermap of this device. + /// This should only be accessed from a derived class, except for test purposes. /// - protected Dictionary RegisterMap => _registerMap; + public Dictionary RegisterMap => _registerMap; /// /// The active connection settings /// public override I2cConnectionSettings ConnectionSettings { get; } + /// + /// The active register. + /// Can be set to mimic some non-standard behavior of setting a register (or if reading increases + /// the register pointer, which is the case on some chips) + /// + protected byte CurrentRegister + { + get + { + return _currentRegister; + } + set + { + _currentRegister = value; + } + } + /// /// Reads a byte from the bus /// @@ -68,7 +89,7 @@ public override byte ReadByte() } /// - /// This method should implement the read operation from the device. + /// This method implements the read operation from the device. /// /// Buffer with input data to the device, buffer[0] is usually the command byte /// The return data from the device @@ -83,6 +104,7 @@ public override void Read(Span buffer) if (WriteRead([], buffer2) == buffer.Length) { buffer2.CopyTo(buffer); + return; } throw new IOException($"Unable to read {buffer.Length} bytes from the device"); @@ -142,16 +164,22 @@ public abstract record class RegisterBase : IComparable /// Writes the register, regardless of its actual type /// /// The value to write - public abstract void WriteRegister(int value); + protected abstract void WriteRegister(int value); /// /// Reads the register value regardless of its actual type /// /// The register value, sign-extended to int - public abstract int ReadRegister(); + protected abstract int ReadRegister(); /// public abstract int CompareTo(object? obj); + + /// + /// Gets the value as stored in the register + /// + /// + public abstract int GetValue(); } /// @@ -161,12 +189,18 @@ public abstract record class RegisterBase : IComparable public record class Register : RegisterBase where T : struct, IEquatable, INumber, IComparable { - private T _value; - /// /// Event that is raised when the register is written /// - public event Action? ValueChanged; + private readonly Func? _registerUpdateHandler; + + /// + /// Event that is raised to read the register. Gets the internal value of the register + /// and returns the value the client should see (e.g a random measurement value) + /// + private readonly Func? _registerReadHandler; + + private T _value; /// /// Create a new register @@ -185,6 +219,19 @@ public Register(T initialValue) _value = initialValue; } + /// + /// Creates a new register with handlers + /// + /// The initial value of the register at power-up + /// A handler for a register write. Can be null. + /// A handler for a register read. Can be null. + public Register(T initialValue, Func? updateHandler, Func? readHandler) + { + _value = initialValue; + _registerUpdateHandler = updateHandler; + _registerReadHandler = readHandler; + } + /// /// The current value of the register /// @@ -192,23 +239,57 @@ public T Value { get { + if (_registerReadHandler != null) + { + return _registerReadHandler(_value); + } + return _value; } set { + if (_registerUpdateHandler != null) + { + _value = _registerUpdateHandler(value); + return; + } + _value = value; - ValueChanged?.Invoke(_value); } } /// - public override void WriteRegister(int value) + protected override void WriteRegister(int value) { + if (Marshal.SizeOf() == 2 && BitConverter.IsLittleEndian) + { + // The bus runs in big-endian mode + int r = (value & 0xFF) << 8 | value >> 8; + value = r; + } + Value = T.CreateChecked(value); } /// - public override int ReadRegister() + protected override int ReadRegister() + { + int ret = int.CreateChecked(Value); + if (Marshal.SizeOf() == 2 && BitConverter.IsLittleEndian) + { + // The bus runs in big-endian mode + int r = (ret & 0xFF) << 8 | ret >> 8; + ret = r; + } + + return ret; + } + + /// + /// The value, for external access + /// + /// The value, sign-extended to int + public override int GetValue() { return int.CreateChecked(Value); } diff --git a/src/devices/Ina236/Ina236.cs b/src/devices/Ina236/Ina236.cs index d38db54a0b..3f20bee6cf 100644 --- a/src/devices/Ina236/Ina236.cs +++ b/src/devices/Ina236/Ina236.cs @@ -326,9 +326,6 @@ private ushort ReadRegisterUnsigned(Ina236Register register) // set a value in the buffer representing the register that we want to read and send it to the INA219 _i2cDevice.WriteRead(new ReadOnlySpan(ref registerNumber), buffer); - // read the register back from the INA219. - _i2cDevice.Read(buffer); - // massage the big endian value read from the INA219 unto a ushort. return BinaryPrimitives.ReadUInt16BigEndian(buffer); } @@ -346,9 +343,6 @@ private short ReadRegisterSigned(Ina236Register register) // set a value in the buffer representing the register that we want to read and send it to the INA219 _i2cDevice.WriteRead(new ReadOnlySpan(ref registerNumber), buffer); - // read the register back from the INA219. - _i2cDevice.Read(buffer); - // massage the big endian value read from the INA219 unto a ushort. return BinaryPrimitives.ReadInt16BigEndian(buffer); } diff --git a/src/devices/Ina236/tests/Ina236Tests.cs b/src/devices/Ina236/tests/Ina236Tests.cs index 530476c66b..c74485a93f 100644 --- a/src/devices/Ina236/tests/Ina236Tests.cs +++ b/src/devices/Ina236/tests/Ina236Tests.cs @@ -17,10 +17,13 @@ namespace Iot.Device.Adc.Tests public sealed class Ina236Tests : IDisposable { private Ina236 _ina236; + private SimulatedIna236 _simulatedIna236; public Ina236Tests() { - _ina236 = new Ina236(new SimulatedIna236(new I2cConnectionSettings(1, 0x40)), ElectricResistance.FromMilliohms(8), ElectricCurrent.FromAmperes(10)); + _simulatedIna236 = new SimulatedIna236(new I2cConnectionSettings(1, 0x40)); + // Use the setting that corresponds to the example values in the data sheet + _ina236 = new Ina236(_simulatedIna236, ElectricResistance.FromMilliohms(8), ElectricCurrent.FromAmperes(16.384 / 2.0)); } [Fact] @@ -32,6 +35,13 @@ public void InitialValues() Assert.Equal(1100, _ina236.ShuntConversionTime); } + [Fact] + public void CheckSetupComplete() + { + int calibrationValue = _simulatedIna236.RegisterMap[5].ReadRegister(); + Assert.Equal(1280, calibrationValue); + } + public void Dispose() { } diff --git a/src/devices/Ina236/tests/SimulatedIna236.cs b/src/devices/Ina236/tests/SimulatedIna236.cs index c9b8c964d2..f4bc53447c 100644 --- a/src/devices/Ina236/tests/SimulatedIna236.cs +++ b/src/devices/Ina236/tests/SimulatedIna236.cs @@ -12,21 +12,45 @@ namespace Ina236.Tests { internal class SimulatedIna236 : I2cSimulatedDeviceBase { - private byte _currentRegister; - public SimulatedIna236(I2cConnectionSettings settings) : base(settings) { - _currentRegister = 0; - RegisterMap.Add(0, new Register(0x4127)); + RegisterMap.Add(0, new Register(0x4127, ConfigurationRegisterHandler, null)); // 0x4127 is the power-on default of the configuration register + RegisterMap.Add(1, new Register()); + RegisterMap.Add(2, new Register()); + RegisterMap.Add(3, new Register()); + RegisterMap.Add(4, new Register()); + RegisterMap.Add(5, new Register()); + RegisterMap.Add(6, new Register()); + RegisterMap.Add(7, new Register()); + // the value is big-endian, but that is taken care of later + RegisterMap.Add(0x3F, new Register(0xA080, DeviceIdentificationRegisterHandler, null)); + } + + private ushort DeviceIdentificationRegisterHandler(ushort arg) + { + // This register is read-only + return 0xA080; + } + + private ushort ConfigurationRegisterHandler(ushort newValue) + { + // When the reset bit is set, set everything to default + if ((newValue & 0x8000) != 0) + { + RegisterMap[5].WriteRegister(0); // Reset the calibration register + return 0x4127; + } + + return newValue; } protected override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) { if (inputBuffer.Length > 0) { - _currentRegister = inputBuffer[0]; - if (inputBuffer.Length >= 3 && RegisterMap.TryGetValue(_currentRegister, out var register)) + CurrentRegister = inputBuffer[0]; + if (inputBuffer.Length >= 3 && RegisterMap.TryGetValue(CurrentRegister, out var register)) { ushort reg = BitConverter.ToUInt16(inputBuffer, 1); register.WriteRegister(reg); @@ -34,9 +58,11 @@ protected override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) } // All registers of this device are 16 bit, so we need to read that or nothing - if (outputBuffer.Length >= 2 && RegisterMap.TryGetValue(_currentRegister, out var register2)) + if (outputBuffer.Length >= 2 && RegisterMap.TryGetValue(CurrentRegister, out var register2)) { ushort ret = (ushort)register2.ReadRegister(); + byte[] buf = BitConverter.GetBytes(ret); + buf.CopyTo(outputBuffer, 0); } return outputBuffer.Length; From c654da0e3d0aadb21d5d1800fa28d3f7a1b076f9 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sun, 7 Dec 2025 17:03:16 +0100 Subject: [PATCH 08/10] Tests are now passing --- .../Device/I2c/I2cSimulatedDeviceBase.cs | 38 ++----------------- src/devices/Ina236/tests/Ina236Tests.cs | 15 ++++++++ src/devices/Ina236/tests/SimulatedIna236.cs | 14 +++---- 3 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs index 36003bf08c..81518654ff 100644 --- a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs +++ b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs @@ -164,22 +164,16 @@ public abstract record class RegisterBase : IComparable /// Writes the register, regardless of its actual type /// /// The value to write - protected abstract void WriteRegister(int value); + public abstract void WriteRegister(int value); /// /// Reads the register value regardless of its actual type /// /// The register value, sign-extended to int - protected abstract int ReadRegister(); + public abstract int ReadRegister(); /// public abstract int CompareTo(object? obj); - - /// - /// Gets the value as stored in the register - /// - /// - public abstract int GetValue(); } /// @@ -259,37 +253,13 @@ public T Value } /// - protected override void WriteRegister(int value) + public override void WriteRegister(int value) { - if (Marshal.SizeOf() == 2 && BitConverter.IsLittleEndian) - { - // The bus runs in big-endian mode - int r = (value & 0xFF) << 8 | value >> 8; - value = r; - } - Value = T.CreateChecked(value); } /// - protected override int ReadRegister() - { - int ret = int.CreateChecked(Value); - if (Marshal.SizeOf() == 2 && BitConverter.IsLittleEndian) - { - // The bus runs in big-endian mode - int r = (ret & 0xFF) << 8 | ret >> 8; - ret = r; - } - - return ret; - } - - /// - /// The value, for external access - /// - /// The value, sign-extended to int - public override int GetValue() + public override int ReadRegister() { return int.CreateChecked(Value); } diff --git a/src/devices/Ina236/tests/Ina236Tests.cs b/src/devices/Ina236/tests/Ina236Tests.cs index c74485a93f..fdf0bb1607 100644 --- a/src/devices/Ina236/tests/Ina236Tests.cs +++ b/src/devices/Ina236/tests/Ina236Tests.cs @@ -42,8 +42,23 @@ public void CheckSetupComplete() Assert.Equal(1280, calibrationValue); } + [Fact] + public void ReadValues() + { + // Calibration has been set up, so we should get the values mentioned in the data sheet + ElectricPotential voltage = _ina236.ReadBusVoltage(); + Assert.Equal(12.0, voltage.Volts); + + ElectricCurrent current = _ina236.ReadCurrent(); + Assert.Equal(6.0, current.Amperes); + + Power p = _ina236.ReadPower(); + Assert.Equal(72.0m, p.Watts); + } + public void Dispose() { + _ina236.Dispose(); } } } diff --git a/src/devices/Ina236/tests/SimulatedIna236.cs b/src/devices/Ina236/tests/SimulatedIna236.cs index f4bc53447c..208c6ebf3d 100644 --- a/src/devices/Ina236/tests/SimulatedIna236.cs +++ b/src/devices/Ina236/tests/SimulatedIna236.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Device.I2c; using System.Linq; @@ -16,10 +17,10 @@ public SimulatedIna236(I2cConnectionSettings settings) : base(settings) { RegisterMap.Add(0, new Register(0x4127, ConfigurationRegisterHandler, null)); // 0x4127 is the power-on default of the configuration register - RegisterMap.Add(1, new Register()); - RegisterMap.Add(2, new Register()); - RegisterMap.Add(3, new Register()); - RegisterMap.Add(4, new Register()); + RegisterMap.Add(1, new Register(19200)); // Default values for example from data sheet + RegisterMap.Add(2, new Register(7500)); + RegisterMap.Add(3, new Register(4500)); + RegisterMap.Add(4, new Register(12000)); RegisterMap.Add(5, new Register()); RegisterMap.Add(6, new Register()); RegisterMap.Add(7, new Register()); @@ -52,7 +53,7 @@ protected override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) CurrentRegister = inputBuffer[0]; if (inputBuffer.Length >= 3 && RegisterMap.TryGetValue(CurrentRegister, out var register)) { - ushort reg = BitConverter.ToUInt16(inputBuffer, 1); + ushort reg = BinaryPrimitives.ReadUInt16BigEndian(inputBuffer.AsSpan().Slice(1)); register.WriteRegister(reg); } } @@ -61,8 +62,7 @@ protected override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) if (outputBuffer.Length >= 2 && RegisterMap.TryGetValue(CurrentRegister, out var register2)) { ushort ret = (ushort)register2.ReadRegister(); - byte[] buf = BitConverter.GetBytes(ret); - buf.CopyTo(outputBuffer, 0); + BinaryPrimitives.WriteUInt16BigEndian(outputBuffer, ret); } return outputBuffer.Length; From 2398d40042dc231caa64bb2855f8898173c65cde Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sat, 13 Dec 2025 13:16:29 +0100 Subject: [PATCH 09/10] Use new interface for this binding, too Will make it easier in future. --- .../Device/I2c/I2cSimulatedDeviceBase.cs | 6 +- src/devices/Ina236/tests/SimulatedIna236.cs | 2 +- src/devices/Tca955x/README.md | 2 +- src/devices/Tca955x/Tca955x.sln | 14 +++ .../Tca955x/tests/MockableI2cDevice.cs | 45 ---------- src/devices/Tca955x/tests/Tca9554Tests.cs | 50 ++++------- src/devices/Tca955x/tests/Tca9555Tests.cs | 34 +++----- .../Tca955x/tests/Tca955xSimulatedDevice.cs | 85 +++++++++++++++++++ 8 files changed, 133 insertions(+), 105 deletions(-) delete mode 100644 src/devices/Tca955x/tests/MockableI2cDevice.cs create mode 100644 src/devices/Tca955x/tests/Tca955xSimulatedDevice.cs diff --git a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs index 81518654ff..928bf5be22 100644 --- a/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs +++ b/src/devices/Common/System/Device/I2c/I2cSimulatedDeviceBase.cs @@ -94,8 +94,10 @@ public override byte ReadByte() /// Buffer with input data to the device, buffer[0] is usually the command byte /// The return data from the device /// How many bytes where read. Should usually match the length of the output buffer - /// This doesn't use as argument type to be mockable - protected abstract int WriteRead(byte[] inputBuffer, byte[] outputBuffer); + /// This doesn't use as argument type to be mockable. Be sure + /// to use this method in mocks, not any that take or , as that + /// will cause runtime exceptions + public abstract int WriteRead(byte[] inputBuffer, byte[] outputBuffer); /// public override void Read(Span buffer) diff --git a/src/devices/Ina236/tests/SimulatedIna236.cs b/src/devices/Ina236/tests/SimulatedIna236.cs index 208c6ebf3d..9e77538074 100644 --- a/src/devices/Ina236/tests/SimulatedIna236.cs +++ b/src/devices/Ina236/tests/SimulatedIna236.cs @@ -46,7 +46,7 @@ private ushort ConfigurationRegisterHandler(ushort newValue) return newValue; } - protected override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) + public override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) { if (inputBuffer.Length > 0) { diff --git a/src/devices/Tca955x/README.md b/src/devices/Tca955x/README.md index 52a4036d27..a6efcdb5c0 100644 --- a/src/devices/Tca955x/README.md +++ b/src/devices/Tca955x/README.md @@ -2,7 +2,7 @@ ## Summary -The TCA955X device family provides 8/16-bit, general purpose I/O expansion for I2C. The devices can be configured with polariy invertion and interrupts. +The TCA955X device family provides 8/16-bit, general purpose I/O expansion for I2C. The devices can be configured with polarity inversion and interrupts. ## Device Family diff --git a/src/devices/Tca955x/Tca955x.sln b/src/devices/Tca955x/Tca955x.sln index fd3136b2ac..e227882739 100644 --- a/src/devices/Tca955x/Tca955x.sln +++ b/src/devices/Tca955x/Tca955x.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AC41B656 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tca955x.Tests", "tests\Tca955x.Tests.csproj", "{F3BCAFCA-A6B8-4530-B435-36FA238D947A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "..\Common\Common.csproj", "{9F0601AB-EA31-A20F-1B21-30C901BDC579}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,18 @@ Global {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x64.Build.0 = Release|Any CPU {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x86.ActiveCfg = Release|Any CPU {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x86.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x64.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x86.ActiveCfg = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Debug|x86.Build.0 = Debug|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|Any CPU.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x64.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x64.Build.0 = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x86.ActiveCfg = Release|Any CPU + {9F0601AB-EA31-A20F-1B21-30C901BDC579}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/devices/Tca955x/tests/MockableI2cDevice.cs b/src/devices/Tca955x/tests/MockableI2cDevice.cs deleted file mode 100644 index 3f15863024..0000000000 --- a/src/devices/Tca955x/tests/MockableI2cDevice.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Device.I2c; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Iot.Device.Tca955x.Tests -{ - /// - /// This class allows mocking of the Read/Write functions of I2cDevice taking Spans - /// - public abstract class MockableI2cDevice : I2cDevice - { - /// - /// These are mockable, operations taking Span<T> are not - /// - /// - public abstract void Read(byte[] data); - public sealed override void Read(Span buffer) - { - byte[] b = new byte[buffer.Length]; - Read(b); - b.CopyTo(buffer); - } - - public abstract void Write(byte[] data); - - public sealed override void Write(ReadOnlySpan buffer) - { - byte[] data = new byte[buffer.Length]; - buffer.CopyTo(data); - Write(data); - } - - public sealed override void WriteRead(ReadOnlySpan writeBuffer, Span readBuffer) - { - Write(writeBuffer); - Read(readBuffer); - } - } -} diff --git a/src/devices/Tca955x/tests/Tca9554Tests.cs b/src/devices/Tca955x/tests/Tca9554Tests.cs index 8f8bcf2fb0..fb61a2ec91 100644 --- a/src/devices/Tca955x/tests/Tca9554Tests.cs +++ b/src/devices/Tca955x/tests/Tca9554Tests.cs @@ -8,33 +8,32 @@ using System.Threading; using Moq; +using Tca955x.Tests; using Xunit; namespace Iot.Device.Tca955x.Tests { public class Tca9554Tests { - private readonly Mock _device; - private readonly Mock _deviceWithBadAddress; + private readonly Tca955xSimulatedDevice _device; + private readonly Mock _deviceWithBadAddress; private readonly GpioController _controller; private readonly Mock _driver; public Tca9554Tests() { - _device = new Mock(MockBehavior.Loose); - _deviceWithBadAddress = new Mock(MockBehavior.Loose); - _device.CallBase = true; + _device = new Tca955xSimulatedDevice(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress)); + _deviceWithBadAddress = new Mock(MockBehavior.Loose, new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress + Tca9554.AddressRange + 1)); + _deviceWithBadAddress.Setup(x => x.ConnectionSettings).CallBase(); _driver = new Mock(); _driver.CallBase = true; _controller = new GpioController(_driver.Object); - _device.Setup(x => x.ConnectionSettings).Returns(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress)); - _deviceWithBadAddress.Setup(x => x.ConnectionSettings).Returns(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress + Tca9554.AddressRange + 1)); } [Fact] public void CreateWithInterrupt() { - var testee = new Tca9554(_device.Object, 10, _controller); + var testee = new Tca9554(_device, 10, _controller); } [Fact] @@ -46,22 +45,13 @@ public void CreateWithBadAddress() [Fact] public void CreateWithoutInterrupt() { - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); } [Fact] public void TestRead() { - _device.Setup(x => x.Write(new byte[1] - { - 0 - })); - _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) => - { - b[0] = 1; - }); - - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); var tcaController = new GpioController(testee); Assert.Equal(8, tcaController.PinCount); GpioPin pin0 = tcaController.OpenPin(0); @@ -78,7 +68,7 @@ public void InterruptCallbackIsInvokedOnPinChange() { // Arrange var interruptPin = 10; - var testee = new Tca9554(_device.Object, interruptPin, _controller); + var testee = new Tca9554(_device, interruptPin, _controller); var tcaController = new GpioController(testee); tcaController.OpenPin(1, PinMode.Input); bool callbackInvoked = false; @@ -92,23 +82,15 @@ void Callback(object sender, PinValueChangedEventArgs args) mre.Set(); } - // Change the device setup to simulate pin1 as high - _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) => - { - b[0] = 0x02; - }); + _device.SetPinState(1, PinValue.High); // Register callback for rising edge tcaController.RegisterCallbackForPinValueChangedEvent(1, PinEventTypes.Falling, Callback); // Change the device setup to simulate pin1 as low. - _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) => - { - b[0] = 0x00; - }); - + _device.SetPinState(1, PinValue.Low); // Act - // Simulate the hardware int pin pin change using the _controller mock + // Simulate the hardware int pin change using the _controller mock _driver.Object.FireEventHandler(interruptPin, PinEventTypes.Rising); mre.Wait(2000); // Wait for the callback to be invoked @@ -122,7 +104,7 @@ void Callback(object sender, PinValueChangedEventArgs args) [Fact] public void TestReadOfIllegalPinThrows() { - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); var tcaController = new GpioController(testee); Assert.Equal(8, tcaController.PinCount); GpioPin pin0 = tcaController.OpenPin(0); @@ -136,12 +118,12 @@ public void CanNotConstructIfInterruptConfiguredIncorrectly() { Assert.Throws(() => { - var testee = new Tca9554(_device.Object, -1, _controller); + var testee = new Tca9554(_device, -1, _controller); }); Assert.Throws(() => { - var testee = new Tca9554(_device.Object, 2); + var testee = new Tca9554(_device, 2); }); } diff --git a/src/devices/Tca955x/tests/Tca9555Tests.cs b/src/devices/Tca955x/tests/Tca9555Tests.cs index f04943dd1d..dfc80d94e8 100644 --- a/src/devices/Tca955x/tests/Tca9555Tests.cs +++ b/src/devices/Tca955x/tests/Tca9555Tests.cs @@ -6,67 +6,57 @@ using System.Device.Gpio.Tests; using System.Device.I2c; using Moq; +using Tca955x.Tests; using Xunit; namespace Iot.Device.Tca955x.Tests { public class Tca9555Tests { - private readonly Mock _device; + private readonly Tca955xSimulatedDevice _device; private readonly GpioController _controller; private readonly Mock _driver; public Tca9555Tests() { - _device = new Mock(MockBehavior.Loose); - _device.CallBase = true; + _device = new Tca955xSimulatedDevice(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress)); _driver = new Mock(); _controller = new GpioController(_driver.Object); - _device.Setup(x => x.ConnectionSettings).Returns(new I2cConnectionSettings(0, Tca9554.DefaultI2cAddress)); } [Fact] public void CreateWithInterrupt() { - var testee = new Tca9555(_device.Object, 10, _controller); + var testee = new Tca9555(_device, 10, _controller); Assert.NotNull(testee); } [Fact] public void CreateWithoutInterrupt() { - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); Assert.NotNull(testee); } [Fact] public void TestRead() { - _device.Setup(x => x.Write(new byte[1] - { - 0 - })); - _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) => - { - b[0] = 1; - }); - - var testee = new Tca9555(_device.Object, -1); + var testee = new Tca9555(_device, -1); var tcaController = new GpioController(testee); Assert.Equal(16, tcaController.PinCount); - GpioPin pin8 = tcaController.OpenPin(8); - Assert.NotNull(pin8); - Assert.True(tcaController.IsPinOpen(8)); - var value = pin8.Read(); + GpioPin pin0 = tcaController.OpenPin(0); + Assert.NotNull(pin0); + Assert.True(tcaController.IsPinOpen(0)); + var value = pin0.Read(); Assert.Equal(PinValue.High, value); - pin8.Dispose(); + pin0.Dispose(); Assert.False(tcaController.IsPinOpen(8)); } [Fact] public void TestReadOfIllegalPinThrows() { - var testee = new Tca9554(_device.Object, -1); + var testee = new Tca9554(_device, -1); var tcaController = new GpioController(testee); Assert.Equal(8, tcaController.PinCount); GpioPin pin0 = tcaController.OpenPin(0); diff --git a/src/devices/Tca955x/tests/Tca955xSimulatedDevice.cs b/src/devices/Tca955x/tests/Tca955xSimulatedDevice.cs new file mode 100644 index 0000000000..fc62e33c83 --- /dev/null +++ b/src/devices/Tca955x/tests/Tca955xSimulatedDevice.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Device.Gpio; +using System.Device.I2c; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tca955x.Tests +{ + internal class Tca955xSimulatedDevice : I2cSimulatedDeviceBase + { + private Register _register0; // pin register 0 + private Register _register2; // polarity inversion register + private Register _register3; // Configuration Register + + public Tca955xSimulatedDevice(I2cConnectionSettings settings) + : base(settings) + { + _register0 = new Register(1); // Pin 0 is high + _register2 = new Register(0); + _register3 = new Register(0); + } + + public void SetPinState(int pin, PinValue value) + { + int bit = 1 << pin; + if (value == PinValue.High) + { + _register0.WriteRegister(_register0.ReadRegister() | bit); + } + else + { + _register0.WriteRegister(_register0.ReadRegister() & ~bit); + } + } + + public override int WriteRead(byte[] inputBuffer, byte[] outputBuffer) + { + if (inputBuffer.Length >= 1) + { + CurrentRegister = inputBuffer[0]; + } + + if (CurrentRegister == 0) + { + outputBuffer[0] = _register0.Value; + return 1; + } + + if (CurrentRegister == 2) + { + if (inputBuffer.Length > 1) + { + _register2.Value = inputBuffer[1]; + } + + if (outputBuffer.Length > 0) + { + outputBuffer[0] = _register2.Value; + return 1; + } + } + + if (CurrentRegister == 3) + { + if (inputBuffer.Length > 1) + { + _register3.Value = inputBuffer[1]; + } + + if (outputBuffer.Length > 0) + { + outputBuffer[0] = _register3.Value; + return 1; + } + } + + return 0; + } + } +} From 80eb4882ba1dd1a1598aa95f8df55d7a5654439c Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Sat, 20 Dec 2025 12:34:17 +0100 Subject: [PATCH 10/10] Update README.md --- src/devices/Ina236/README.md | 45 +++++++++++++++--------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/src/devices/Ina236/README.md b/src/devices/Ina236/README.md index 938fd9847e..183c8da84a 100644 --- a/src/devices/Ina236/README.md +++ b/src/devices/Ina236/README.md @@ -8,6 +8,7 @@ The INA236 is a current shunt and power monitor with an I2C-compatible interface * High Accuracy: 0.5% (Maximum) Over Temperature * Filtering Options * Calibration Registers +* Two variants available: Address range 0x40-0x43 or 0x60-0x63 ## Documentation @@ -19,36 +20,28 @@ The INA236 is a current shunt and power monitor with an I2C-compatible interface const byte Adafruit_Ina236_I2cAddress = 0x40; // create an INA236 device on I2C bus 1 addressing channel 64 -using (Ina219 device = new Ina219(new I2cConnectionSettings(Adafruit_Ina236_I2cBus, Adafruit_Ina219_I2cAddress))) +// Known breakouts often have a shunt resistor of 0.008 Ohms and are designed to measure up to 10 Amperes. +using Ina219 device = new Ina236(new I2cConnectionSettings(Adafruit_Ina236_I2cBus, Adafruit_Ina219_I2cAddress), + ElectricResistance.FromMilliohms(8), ElectricCurrent.FromAmperes(10.0)); + +Console.WriteLine("Device initialized. Default settings used:"); +Console.WriteLine($"Operating Mode: {device.OperatingMode}"); +Console.WriteLine($"Number of Samples to average: {device.AverageOverNoSamples}"); +Console.WriteLine($"Bus conversion time: {device.BusConversionTime}us"); +Console.WriteLine($"Shunt conversion time: {device.ShuntConversionTime}us"); + +while (!Console.KeyAvailable) { - // reset the device - device.Reset(); - - // set up the bus and shunt voltage ranges and the calibration. Other values left at default. - device.BusVoltageRange = Ina219BusVoltageRange.Range16v; - device.PgaSensitivity = Ina219PgaSensitivity.PlusOrMinus40mv; - device.SetCalibration(33574, (float)12.2e-6); - - while (true) - { - // write out the current values from the INA219 device. - System.Console.WriteLine($"Bus Voltage {device.ReadBusVoltage()}V Shunt Voltage {device.ReadShuntVoltage() * 1000}mV Current {device.ReadCurrent() * 1000}mA Power {device.ReadPower() * 1000}mW"); - System.Threading.Thread.Sleep(1000); - } + // write out the current values from the INA219 device. + Console.WriteLine($"Bus Voltage {device.ReadBusVoltage()} Shunt Voltage {device.ReadShuntVoltage().Millivolts}mV Current {device.ReadCurrent()} Power {device.ReadPower()}"); + Thread.Sleep(1000); } + ``` ### Notes -This sample uses an Adafruit INA219 breakout board and monitors a LED wired into the 3.3 volts supply with a 150 ohm current limiting resistor. It prints the bus voltage, shunt voltage, current and power every second. - -The configuration and calibration is determinined as follows. - -* The bus voltage range can be either 16v or 32v. As this example uses a 3.3v supply then the 16v bus voltage range is chosen -* The current through the LED is in the low tens of milliamps. If we take 50mA as a reasonable maximum current that we may want to see then the maximum voltage accross the shunt resistor is 0.1 Ohms x 50mA which works out -at 5mV. Given this we can use a shunt voltage range of +/- 40mV -* The maximum possible current would then be 40mV / 0.1 = 400mA -* With a 400mA maximum current and a range of the ADC of 15bits then the LSB of the current would be 400mA/32767 = 12.2207 microamps. We will chose 12.2uA as a round number. -* From the [INA219 Datasheet](http://www.ti.com/lit/ds/symlink/ina219.pdf) the calibration register should be set at 0.04096/(currentLSB * shunt resistance) = 33574 = 0x8326 +To set up the binding, the shunt resistor value and the maximum expected current need to be provided. Known breakout boards +(e.g. from Adafruit or Joy-It) have a shunt resistor of 0.008 Ohms. With a 10 A load, the voltage drop at the resistor is thus +0.08 V, resulting in a power dissipation of 0.8 Watts. -![circuit](Ina219.Sample_bb.png)