diff --git a/README.md b/README.md index efc2828..8dca2a2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Introduction A powerful library for a simple and easy to use API when communicating with One Wire devices via I2C on Raspberry Pi. using (var ds2482_800 = await _dS2482DeviceFactory.CreateDS2482_800(false, false, false)) - using (var ds2482_100 = await _dS2482DeviceFactory.CreateDS2482_100(true, true)) + using (var ds2482_100 = await _dS2482DeviceFactory.CreateDS2482_100(1, true, true)) { while (true) { @@ -49,16 +49,29 @@ A powerful library for a simple and easy to use API when communicating with One // Insert code to log result in some way } + foreach (Rinsen.IoT.OneWire.MAX31850 device in ds2482_100.GetDevices()) + { + MAX31850.TemperatureConversionResult tcr = device.GetTemperature(); + Console.WriteLine("DS2482-100, MAX31850 :"); + Console.WriteLine(tcr.TemperatureConversionResultNarrative); + + // Insert code to log result in some way + } + await Task.Delay(5000); } } -And thats all you need to get started with measuring temperatures with a DS18B20 from .NET and C# on Raspberry Pi. +And thats all you need to get started with measuring temperatures with a DS18B20 or MAX31850 from .NET and C# on Raspberry Pi. Headed apps ----------- Headed apps do not currently support disposing the DS2482 devices. The instance MUST be reused between measurements. +I2C Bus +------- +CreateDS2482_100(int busId, bool ad0, bool ad1) allows the I2C bus to be selected in the case you are either not using the default I2C bus 1, or your project has multiple I2C busses. + I2C Address ----------- @@ -72,6 +85,7 @@ Built in One Wire Device Support ## Today: 1. DS18B20 2. DS18S20 +3. MAX31850 ## Extend with your own device diff --git a/src/Rinsen.IoT.OneWire/DS2482.cs b/src/Rinsen.IoT.OneWire/DS2482.cs index fdd319d..657655a 100644 --- a/src/Rinsen.IoT.OneWire/DS2482.cs +++ b/src/Rinsen.IoT.OneWire/DS2482.cs @@ -23,6 +23,7 @@ public DS2482(I2cDevice i2cDevice, bool disposeI2cDevice) AddDeviceType(0x10); AddDeviceType(0x28); + AddDeviceType(0x3B); } public abstract bool IsCorrectChannelSelected(OneWireChannel channel); diff --git a/src/Rinsen.IoT.OneWire/DS2482DeviceFactory.cs b/src/Rinsen.IoT.OneWire/DS2482DeviceFactory.cs index def52f2..a00636f 100644 --- a/src/Rinsen.IoT.OneWire/DS2482DeviceFactory.cs +++ b/src/Rinsen.IoT.OneWire/DS2482DeviceFactory.cs @@ -5,6 +5,32 @@ namespace Rinsen.IoT.OneWire { public class DS2482DeviceFactory : IDS2482DeviceFactory { + /// + /// Instantiate a DS2482 on the specified port + /// To use I2C port 0 : (1) Enable I2C using raspi-config, (2) modify /boot/config.txt to include : + /// dtparam=i2c_arm=on + /// dtoverlay=i2c-gpio,bus=0,i2c_gpio_sda=0,i2c_gpio_scl=1 #this is a software i2c on pins 27 and 28 + /// #dtoverlay=vc4-kms-v3d (comment this line out) + /// + /// busID = 0 : SDA on pin 27, SCL on pin 28 ; busID = 1 : SDA on pin 3, SCL on pin 5 + /// AD0 on the DS2482-100 + /// AD1 on the DS2482-100 + /// + public DS2482_100 CreateDS2482_100(int busId, bool ad0, bool ad1) + { + byte address = 0x18; + if (ad0) + { + address |= 1 << 0; + } + if (ad1) + { + address |= 1 << 1; + } + + return CreateDS2482_100(busId, address); + } + public DS2482_100 CreateDS2482_100(bool ad0, bool ad1) { byte address = 0x18; diff --git a/src/Rinsen.IoT.OneWire/ExtensionMethods.cs b/src/Rinsen.IoT.OneWire/ExtensionMethods.cs index 3c50a02..5370131 100644 --- a/src/Rinsen.IoT.OneWire/ExtensionMethods.cs +++ b/src/Rinsen.IoT.OneWire/ExtensionMethods.cs @@ -6,5 +6,10 @@ public static bool GetBit(this byte b, int bitNumber) { return (b & (1 << bitNumber)) != 0; } + + public static bool GetBit(this ushort b, int bitNumber) + { + return (b & (1 << bitNumber)) != 0; + } } } diff --git a/src/Rinsen.IoT.OneWire/IDS2482DeviceFactory.cs b/src/Rinsen.IoT.OneWire/IDS2482DeviceFactory.cs index c3d63f4..d044442 100644 --- a/src/Rinsen.IoT.OneWire/IDS2482DeviceFactory.cs +++ b/src/Rinsen.IoT.OneWire/IDS2482DeviceFactory.cs @@ -5,6 +5,7 @@ namespace Rinsen.IoT.OneWire { public interface IDS2482DeviceFactory { + DS2482_100 CreateDS2482_100(int busId, bool ad0, bool ad1); DS2482_100 CreateDS2482_100(int busId, int address); DS2482_100 CreateDS2482_100(bool ad0, bool ad1); DS2482_100 CreateDS2482_100(I2cDevice i2cDevice); diff --git a/src/Rinsen.IoT.OneWire/MAX31850.cs b/src/Rinsen.IoT.OneWire/MAX31850.cs new file mode 100644 index 0000000..32a6fde --- /dev/null +++ b/src/Rinsen.IoT.OneWire/MAX31850.cs @@ -0,0 +1,356 @@ +using Rinsen.IoT.OneWire.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Rinsen.IoT.OneWire +{ + public class MAX31850 : IOneWireDevice + { + public class TemperatureConversionResult + { + public bool CRC_OK = false; + public bool Fault; + public bool ShortToVdd; + public bool ShortToGnd; + public bool OpenCircuit; + public double ColdJunctionCompensatedThermocoupleTemperature; + public double InternalColdJunctionTemperature; + + public string TemperatureConversionResultNarrative + { + get + { + StringBuilder sb = new StringBuilder(); + + if (!CRC_OK) + { + sb.Append("CRC Error"); + return sb.ToString(); + } + + if (Fault) + { + if (ShortToVdd) + { + sb.Append("Short to Vdd"); + } + + if (ShortToGnd) + { + sb.Append("Short to Gnd"); + } + + if (OpenCircuit) + { + sb.Append("Open circuit"); + } + + return sb.ToString(); + } + + sb.Append("Internal cold junction temperature : "); + sb.Append(InternalColdJunctionTemperature); + sb.Append(Environment.NewLine); + + sb.Append("Cold junction compensated thermocouple temperature : "); + sb.Append(ColdJunctionCompensatedThermocoupleTemperature); + sb.Append(Environment.NewLine); + + return sb.ToString(); + } + } + } + + public class AddressPinsResult + { + public bool CRC_OK = false; + public bool[] AddressPins = new bool[4]; + } + + private OneWireMaster _oneWireMaster; + + public byte[] OneWireAddress { get; private set; } + + public string OneWireAddressString { get { return BitConverter.ToString(OneWireAddress); } } + + private byte[] scratchpad; + + private bool scrachpadCRC_OK = false; + public void Initialize(OneWireMaster oneWireMaster, byte[] oneWireAddress) + { + _oneWireMaster = oneWireMaster; + OneWireAddress = oneWireAddress; + } + + /// + /// The address pins that are used to locate the physical device (they aren't address pins for addressing the device) + /// + /// AddressPinsResult + public AddressPinsResult GetAddressPins() + { + RetrieveScratchpad(); + + AddressPinsResult result = new AddressPinsResult(); + + result.CRC_OK = scrachpadCRC_OK; + result.AddressPins[0] = (scratchpad[Scratchpad.ConfigurationRegister] & 1) != 0; + result.AddressPins[1] = (scratchpad[Scratchpad.ConfigurationRegister] & 2) != 0; + result.AddressPins[2] = (scratchpad[Scratchpad.ConfigurationRegister] & 4) != 0; + result.AddressPins[3] = (scratchpad[Scratchpad.ConfigurationRegister] & 8) != 0; + return result; + } + + /// + /// CopyBit creates a new ushort based on the bit specified from a scratchpad byte + /// + /// Temperature ushort to work on + /// The origin scratchpad byte location number + /// The bit position to extract from the scratchpad byte + /// The bit position to place the extracted bit in + /// + private ushort CopyBit(ushort tmp, byte scratchpadByteNumber, byte originalBitPosition, byte newBitPosition) + { + ushort temp = tmp; + if ((scratchpad[scratchpadByteNumber] & (1 << originalBitPosition)) != 0) + { + temp = (ushort)(temp | (1 << newBitPosition)); + + } + return temp; + } + + private double GetColdJunctionCompensatedThermocoupleTemperature() + { + ushort msblsb = 0; // Combined and reordered msb and lsb + + msblsb = CopyBit(msblsb, 0, 2, 0); + msblsb = CopyBit(msblsb, 0, 3, 1); + msblsb = CopyBit(msblsb, 0, 4, 2); + msblsb = CopyBit(msblsb, 0, 5, 3); + msblsb = CopyBit(msblsb, 0, 6, 4); + msblsb = CopyBit(msblsb, 0, 7, 5); + msblsb = CopyBit(msblsb, 1, 0, 6); + msblsb = CopyBit(msblsb, 1, 1, 7); + msblsb = CopyBit(msblsb, 1, 2, 8); + msblsb = CopyBit(msblsb, 1, 3, 9); + msblsb = CopyBit(msblsb, 1, 4, 10); + msblsb = CopyBit(msblsb, 1, 5, 11); + msblsb = CopyBit(msblsb, 1, 6, 12); + + bool sign = scratchpad[1].GetBit(7); + bool negative = sign; + + if (negative) // Temperature is negative + { + msblsb = (ushort)~msblsb; // Flip the bits + msblsb = (ushort)(msblsb & 0b0001111111111111); // Discard the 3 most significant bits + msblsb++; // Add one + } + + double temperature = 0; + + if (msblsb.GetBit(0)) temperature += PowerValues[-2]; // Math,Pow(2,-2) + if (msblsb.GetBit(1)) temperature += PowerValues[-1]; // Math,Pow(2,-1) + if (msblsb.GetBit(2)) temperature += PowerValues[0]; // Math,Pow(2,0) + if (msblsb.GetBit(3)) temperature += PowerValues[1]; // Math,Pow(2,1) + if (msblsb.GetBit(4)) temperature += PowerValues[2]; // Math,Pow(2,2) + if (msblsb.GetBit(5)) temperature += PowerValues[3]; // Math,Pow(2,3) + if (msblsb.GetBit(6)) temperature += PowerValues[4]; // Math,Pow(2,4) + if (msblsb.GetBit(7)) temperature += PowerValues[5]; // Math,Pow(2,5) + if (msblsb.GetBit(8)) temperature += PowerValues[6]; // Math,Pow(2,6) + if (msblsb.GetBit(9)) temperature += PowerValues[7]; // Math,Pow(2,7) + if (msblsb.GetBit(10)) temperature += PowerValues[8]; // Math,Pow(2,8) + if (msblsb.GetBit(11)) temperature += PowerValues[9]; // Math,Pow(2,9) + if (msblsb.GetBit(12)) temperature += PowerValues[10]; // Math,Pow(2,10) + + if (negative) // Temperature is negative + { + temperature = temperature * -1; + } + + return temperature; + } + + private double GetInternalColdJunctionTemperature() + { + ushort msblsb = 0; // Combined and reordered msb and lsb + + msblsb = CopyBit(msblsb, 2, 4, 0); + msblsb = CopyBit(msblsb, 2, 5, 1); + msblsb = CopyBit(msblsb, 2, 6, 2); + msblsb = CopyBit(msblsb, 2, 7, 3); + msblsb = CopyBit(msblsb, 3, 0, 4); + msblsb = CopyBit(msblsb, 3, 1, 5); + msblsb = CopyBit(msblsb, 3, 2, 6); + msblsb = CopyBit(msblsb, 3, 3, 7); + msblsb = CopyBit(msblsb, 3, 4, 8); + msblsb = CopyBit(msblsb, 3, 5, 9); + msblsb = CopyBit(msblsb, 3, 6, 10); + + bool sign = scratchpad[3].GetBit(7); + bool negative = sign; + + if (negative) // Temperature is negative + { + msblsb = (ushort)~msblsb; // Flip the bits + msblsb = (ushort)(msblsb & 0b0000011111111111); // Discard the 5 most significant bits + msblsb++; // Add one + } + + double temperature = 0; + + if (msblsb.GetBit(0)) temperature += PowerValues[-4]; // Math,Pow(2,-4) + if (msblsb.GetBit(1)) temperature += PowerValues[-3]; // Math,Pow(2,-3) + if (msblsb.GetBit(2)) temperature += PowerValues[-2]; // Math,Pow(2,-2) + if (msblsb.GetBit(3)) temperature += PowerValues[-1]; // Math,Pow(2,-1) + if (msblsb.GetBit(4)) temperature += PowerValues[0]; // Math,Pow(2,0) + if (msblsb.GetBit(5)) temperature += PowerValues[1]; // Math,Pow(2,1) + if (msblsb.GetBit(6)) temperature += PowerValues[2]; // Math,Pow(2,2) + if (msblsb.GetBit(7)) temperature += PowerValues[3]; // Math,Pow(2,3) + if (msblsb.GetBit(8)) temperature += PowerValues[4]; // Math,Pow(2,4) + if (msblsb.GetBit(9)) temperature += PowerValues[5]; // Math,Pow(2,5) + if (msblsb.GetBit(10)) temperature += PowerValues[6]; // Math,Pow(2,6) + + if (negative) // Temperature is negative + { + temperature = temperature * -1; + } + + return temperature; + } + + public TemperatureConversionResult GetTemperature() + { + RetrieveTemperatureScratchpad(); + + TemperatureConversionResult result = new TemperatureConversionResult(); + result.CRC_OK = scrachpadCRC_OK; + result.Fault = scratchpad[0].GetBit(0); + result.OpenCircuit = scratchpad[2].GetBit(0); + result.ShortToGnd = scratchpad[2].GetBit(1); + result.ShortToVdd = scratchpad[2].GetBit(2); + + result.ColdJunctionCompensatedThermocoupleTemperature = GetColdJunctionCompensatedThermocoupleTemperature(); + result.InternalColdJunctionTemperature = GetInternalColdJunctionTemperature(); + + return result; + } + + protected void RetrieveScratchpad() + { + scratchpad = GetScratchpad(); + scrachpadCRC_OK = CalculateCRC(); + } + + protected byte[] GetScratchpad() + { + ResetOneWireAndMatchDeviceRomAddress(); + return ReadScratchpad(); + } + + protected void RetrieveTemperatureScratchpad() + { + scratchpad = GetTemperatureScratchpad(); + scrachpadCRC_OK = CalculateCRC(); + } + + protected byte[] GetTemperatureScratchpad() + { + ResetOneWireAndMatchDeviceRomAddress(); + StartTemperatureConversion(); + + ResetOneWireAndMatchDeviceRomAddress(); + + return ReadScratchpad(); + } + + void StartTemperatureConversion() + { + _oneWireMaster.WriteByte(FunctionCommand.CONVERT_T); + + Task.Delay(TimeSpan.FromSeconds(1)).Wait(); + } + + byte[] ReadScratchpad() + { + _oneWireMaster.WriteByte(FunctionCommand.READ_SCRATCHPAD); + + var scratchpadData = new byte[9]; + + for (int i = 0; i < scratchpadData.Length; i++) + { + scratchpadData[i] = _oneWireMaster.ReadByte(); + } + + return scratchpadData; + } + + void ResetOneWireAndMatchDeviceRomAddress() + { + _oneWireMaster.Reset(); + + _oneWireMaster.WriteByte(RomCommand.MATCH); + + foreach (var item in OneWireAddress) + { + _oneWireMaster.WriteByte(item); + } + } + + /// + /// Compares crc calculated from the first 8 bytes of the scratchpad to the crc in the 9th byte of the scratchpad + /// + /// True if the calculated crc matches the received crc, false otherwise + private bool CalculateCRC() + { + byte crc = 0; + for (int i = 0; i < 8; i++) + { + crc = DS2482Channel.CalculateGlobalCrc8(scratchpad[i], crc); + } + return (crc == scratchpad[8]) ? true : false; + } + + Dictionary PowerValues = new Dictionary() { { 11, 2048 }, { 10, 1024 }, { 9, 512 }, { 8, 256 }, { 7, 128 }, { 6, 64 }, { 5, 32 }, { 4, 16 }, { 3, 8 }, { 2, 4 }, { 1, 2 }, { 0, 1 }, { -1, 0.5 }, { -2, 0.25 }, { -3, 0.125 }, { -4, 0.0625 } }; + + class Scratchpad + { + public const int ColdJunctionCompensatedThermocoupleTemperatureLSBAndFaultStatus = 0; + + public const int ColdJunctionCompensatedThermocoupleTemperatureMSB = 1; + + public const int InteralColdJunctionTemperatureAndFaultStatusLSB = 2; + + public const int InteralColdJunctionTemperatureAndFaultStatusMSB = 3; + + public const int ConfigurationRegister = 4; + + public const int Reserved1 = 5; + + public const int Reserved2 = 6; + + public const int Reserved3 = 7; + + public const int CRC = 8; + + } + + public class RomCommand + { + public const byte SEARCH = 0xF0; + public const byte READ = 0x33; + public const byte MATCH = 0x55; + public const byte SKIP = 0xCC; + } + + public class FunctionCommand + { + public const byte CONVERT_T = 0x44; + public const byte READ_SCRATCHPAD = 0xBE; + public const byte READ_POWER_SUPPLY = 0xB4; + } + } +} diff --git a/tests/Rinsen.IoT.OneWire.Tests/MAX31850Tests.cs b/tests/Rinsen.IoT.OneWire.Tests/MAX31850Tests.cs new file mode 100644 index 0000000..03d0743 --- /dev/null +++ b/tests/Rinsen.IoT.OneWire.Tests/MAX31850Tests.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Rinsen.IoT.OneWire.Tests +{ + // See page 13 of the MAX31850/MAX31851 datasheet for the following test data + + public class MAX31850Tests + { + [Theory] + [InlineData(new byte[] { 0b00000000, 0b01100100, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 1600)] + [InlineData(new byte[] { 0b10000000, 0b00111110, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 1000)] + [InlineData(new byte[] { 0b01001100, 0b00000110, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 100.75)] + [InlineData(new byte[] { 0b10010000, 0b00000001, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 25)] + [InlineData(new byte[] { 0b00000000, 0b00000000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 0)] + [InlineData(new byte[] { 0b11111100, 0b11111111, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, -0.25)] + [InlineData(new byte[] { 0b11110000, 0b11111111, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, -1.00)] + [InlineData(new byte[] { 0b01100000, 0b11110000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, -250.00)] + public void ColdJunctionCompensatedThermocoupleTemperatureCalculations(byte[] scratchpad, double expectedTemp) + { + // Arrange + var max31850 = new MAX31850(); + + // Use reflection to set the scratchpad (assuming SetScratchpad is public or has internal access) + var fieldInfo = typeof(MAX31850).GetField("scratchpad", BindingFlags.NonPublic | BindingFlags.Instance); + fieldInfo.SetValue(max31850, scratchpad); + + // Use reflection to invoke the private method + var methodInfo = typeof(MAX31850).GetMethod("GetColdJunctionCompensatedThermocoupleTemperature", BindingFlags.NonPublic | BindingFlags.Instance); + var result = (double)methodInfo.Invoke(max31850, null); + + // Assert + Assert.Equal(expectedTemp, result); + } + + [Theory] + [InlineData(new byte[] { 0x00, 0x00, 0b00000000, 0b01111111, 0x00, 0x00, 0x00, 0x00 }, 127)] + [InlineData(new byte[] { 0x00, 0x00, 0b10010000, 0b01100100, 0x00, 0x00, 0x00, 0x00 }, 100.5625)] + [InlineData(new byte[] { 0x00, 0x00, 0b00000000, 0b00011001, 0x00, 0x00, 0x00, 0x00 }, 25)] + [InlineData(new byte[] { 0x00, 0x00, 0b00000000, 0b00000000, 0x00, 0x00, 0x00, 0x00 }, 0)] + [InlineData(new byte[] { 0x00, 0x00, 0b11110000, 0b11111111, 0x00, 0x00, 0x00, 0x00 }, -0.0625)] + [InlineData(new byte[] { 0x00, 0x00, 0b00000000, 0b11111111, 0x00, 0x00, 0x00, 0x00 }, -1)] + [InlineData(new byte[] { 0x00, 0x00, 0b00000000, 0b11101100, 0x00, 0x00, 0x00, 0x00 }, -20)] + [InlineData(new byte[] { 0x00, 0x00, 0b00000000, 0b11001001, 0x00, 0x00, 0x00, 0x00 }, -55)] + public void InternalColdJunctionTemperatureCalculations(byte[] scratchpad, double expectedTemp) + { + // Arrange + var max31850 = new MAX31850(); + + // Use reflection to set the scratchpad (assuming SetScratchpad is public or has internal access) + var fieldInfo = typeof(MAX31850).GetField("scratchpad", BindingFlags.NonPublic | BindingFlags.Instance); + fieldInfo.SetValue(max31850, scratchpad); + + // Use reflection to invoke the private method + var methodInfo = typeof(MAX31850).GetMethod("GetInternalColdJunctionTemperature", BindingFlags.NonPublic | BindingFlags.Instance); + var result = (double)methodInfo.Invoke(max31850, null); + + // Assert + Assert.Equal(expectedTemp, result); + } + } +}