1 //
2 // Copyright (c) 2010-2024 Antmicro
3 //
4 // This file is licensed under the MIT License.
5 // Full license text is available in 'licenses/MIT.txt'.
6 //
7 using System;
8 using System.Collections.Generic;
9 using System.Linq;
10 using Antmicro.Renode.Core;
11 using Antmicro.Renode.Exceptions;
12 using Antmicro.Renode.Logging;
13 using Antmicro.Renode.Peripherals.I2C;
14 using Antmicro.Renode.Peripherals.Sensor;
15 using Antmicro.Renode.Utilities;
16 using Antmicro.Renode.Utilities.RESD;
17 
18 namespace Antmicro.Renode.Peripherals.Sensors
19 {
20     public class ICP_101xx : II2CPeripheral, ISensor, ITemperatureSensor, IUnderstandRESD
21     {
ICP_101xx(IMachine machine)22         public ICP_101xx(IMachine machine)
23         {
24             crcEngine = new CRCEngine(0x31, 8, false, false, 0xFF, 0x00);
25             writeHandlers = new Dictionary<Command, Action<byte[], int>>();
26             readHandlers = new Dictionary<Command, Func<int, IEnumerable<byte>>>();
27             DefaultPressure = MinPressure;
28             this.machine = machine;
29             DefineCommandHandlers();
30         }
31 
SoftwareReset()32         public void SoftwareReset()
33         {
34             command = null;
35         }
36 
Reset()37         public void Reset()
38         {
39             SoftwareReset();
40 
41             temperatureResdStream?.Dispose();
42             temperatureResdStream = null;
43 
44             pressureResdStream?.Dispose();
45             pressureResdStream = null;
46         }
47 
Write(byte[] data)48         public void Write(byte[] data)
49         {
50             if(data.Length == 0)
51             {
52                 this.Log(LogLevel.Error, "Unexpected write with no data.");
53                 return;
54             }
55             if(data.Length < 2)
56             {
57                 this.Log(LogLevel.Warning, "Malformed command.");
58                 return;
59             }
60             this.Log(LogLevel.Debug, "Received command: {0}", Misc.PrettyPrintCollectionHex(data));
61             command = (Command)BitHelper.ToUInt16(data, 0, reverse: false);
62             HandleWrite(data, 2);
63         }
64 
Read(int count = 1)65         public byte[] Read(int count = 1)
66         {
67             if(!command.HasValue)
68             {
69                 this.Log(LogLevel.Error, "Attempted read, but Command not specified.");
70                 return Enumerable.Empty<byte>().ToArray();
71             }
72             // Concat to handle cases, where HandleRead could return 0 bytes
73             var rets = InsertCrc(HandleRead(count)).Concat(new byte[]{ 0, 0 }).Take(count).ToArray();
74             this.Log(LogLevel.Debug, "Reading data with CRC: {0}, current command: {1}, requested: {2} bytes", Misc.PrettyPrintCollectionHex(rets), GetCommandString(), count);
75             return rets;
76         }
77 
FeedTemperatureSamplesFromRESD(ReadFilePath path, uint channelId = 0, RESDStreamSampleOffset sampleOffsetType = RESDStreamSampleOffset.Specified, long sampleOffsetTime = 0)78         public void FeedTemperatureSamplesFromRESD(ReadFilePath path, uint channelId = 0,
79             RESDStreamSampleOffset sampleOffsetType = RESDStreamSampleOffset.Specified, long sampleOffsetTime = 0)
80         {
81             temperatureResdStream?.Dispose();
82             temperatureResdStream = this.CreateRESDStream<TemperatureSample>(path, channelId, sampleOffsetType, sampleOffsetTime);
83         }
84 
FeedPressureSamplesFromRESD(ReadFilePath path, uint channelId = 0, RESDStreamSampleOffset sampleOffsetType = RESDStreamSampleOffset.Specified, long sampleOffsetTime = 0)85         public void FeedPressureSamplesFromRESD(ReadFilePath path, uint channelId = 0,
86             RESDStreamSampleOffset sampleOffsetType = RESDStreamSampleOffset.Specified, long sampleOffsetTime = 0)
87         {
88             pressureResdStream?.Dispose();
89             pressureResdStream = this.CreateRESDStream<PressureSample>(path, channelId, sampleOffsetType, sampleOffsetTime);
90         }
91 
FinishTransmission()92         public void FinishTransmission()
93         {
94             // Don't reset state machine (current command) here - instead the software driver should use Soft Reset command
95         }
96 
SetCalibrationValue(uint index, ushort value)97         public void SetCalibrationValue(uint index, ushort value)
98         {
99             if(index >= CalibrationValues.Length)
100             {
101                 throw new RecoverableException($"Index has to be in range 0 - {CalibrationValues.Length - 1}");
102             }
103             CalibrationValues[index] = value;
104         }
105 
106         // Sensor calibration values, used when reading the pressure
107         // This calibration values have been determined using statistical solution search with the following goals in mind:
108         // * decrease probability that the raw pressure value returned from the sensor is negative
109         // * make sure the raw pressure value fits in 3 bytes of return value
110         // * minimize error margin between set pressure and reported raw pressure (post-conversion) as much as possible, taking into account temperature changes
111         // in real HW these are definitely different, but for our use case they are sufficient
112         public ushort[] CalibrationValues { get; private set; } = new ushort[4] { 620, 17540, 9475, 2945 };
113         // Another good vectors:
114         // { 15320, 15780, 32302, 3497 };
115         // { 30261, -20852 /*44684*/, -24209 /*41327*/, 3107 };
116 
117         // Pressure is specified in Pascals
118         public long DefaultPressure
119         {
120             get => defaultPressure;
121             set
122             {
123                 defaultPressure = value;
124                 ClampPressureAndLogWarning(ref defaultPressure);
125             }
126         }
127 
128         public long Pressure
129         {
130             get
131             {
132                 UpdateCurrentPressureSample();
133                 ClampPressureAndLogWarning(ref pressure);
134                 return pressure;
135             }
136             set
137             {
138                 throw new RecoverableException("Explicitly setting pressure is not supported by this model. " +
139                 $"Pressure should be provided from a RESD file or set via the '{nameof(DefaultPressure)}' property");
140             }
141         }
142 
143         // Temperature is specified in degrees Celsius
144         public decimal DefaultTemperature
145         {
146             get => defaultTemperature;
147             set
148             {
149                 defaultTemperature = value;
150                 ClampTemperatureAndLogWarning(ref defaultTemperature);
151             }
152         }
153 
154         public decimal Temperature
155         {
156             get
157             {
158                 UpdateCurrentTemperatureSample();
159                 ClampTemperatureAndLogWarning(ref temperature);
160                 return temperature;
161             }
162             set
163             {
164                 throw new RecoverableException("Explicitly setting temperature is not supported by this model. " +
165                 $"Temperature should be provided from a RESD file or set via the '{nameof(DefaultTemperature)}' property");
166             }
167         }
168 
169         public const decimal MinTemperature = -45;
170         public const decimal MaxTemperature = 85;
171 
172         public const uint MinPressure = 30000;
173         public const uint MaxPressure = 110000;
174 
ClampPressureAndLogWarning(ref long pressure)175         private void ClampPressureAndLogWarning(ref long pressure)
176         {
177             if(pressure < MinPressure || pressure > MaxPressure)
178             {
179                 this.Log(LogLevel.Warning, "Pressure value: {0} is out of range and it will be clamped. Supported range: {1} - {2}", pressure, MinPressure, MaxPressure);
180                 pressure = pressure.Clamp(MinPressure, MaxPressure);
181             }
182         }
183 
ClampTemperatureAndLogWarning(ref decimal temperature)184         private void ClampTemperatureAndLogWarning(ref decimal temperature)
185         {
186             if(temperature < MinTemperature || temperature > MaxTemperature)
187             {
188                 this.Log(LogLevel.Warning, "Temperature value: {0} is out of range and it will be clamped. Supported range: {1} - {2}", temperature, MinTemperature, MaxTemperature);
189                 temperature = temperature.Clamp(MinTemperature, MaxTemperature);
190             }
191         }
192 
DefineCommandHandlers()193         private void DefineCommandHandlers()
194         {
195             RegisterCommand(Command.LowPowerTemperaturePressure,
196                 readHandler: _ => Enumerable.Concat(
197                     GetTemperatureBytes(OperationMode.LowPower),
198                     GetPressureBytes(OperationMode.LowPower)
199                 )
200             );
201 
202             RegisterCommand(Command.LowPowerPressureTemperature,
203                 readHandler: _ => Enumerable.Concat(
204                     GetPressureBytes(OperationMode.LowPower),
205                     GetTemperatureBytes(OperationMode.LowPower)
206                 )
207             );
208 
209             RegisterCommand(Command.NormalTemperaturePressure,
210                 readHandler: _ => Enumerable.Concat(
211                     GetTemperatureBytes(OperationMode.Normal),
212                     GetPressureBytes(OperationMode.Normal)
213                 )
214             );
215 
216             RegisterCommand(Command.NormalPressureTemperature,
217                 readHandler: _ => Enumerable.Concat(
218                     GetPressureBytes(OperationMode.Normal),
219                     GetTemperatureBytes(OperationMode.Normal)
220                 )
221             );
222 
223             RegisterCommand(Command.LowNoiseTemperaturePressure,
224                 readHandler: _ => Enumerable.Concat(
225                     GetTemperatureBytes(OperationMode.LowNoise),
226                     GetPressureBytes(OperationMode.LowNoise)
227                 )
228             );
229 
230             RegisterCommand(Command.LowNoisePressureTemperature,
231                 readHandler: _ => Enumerable.Concat(
232                     GetPressureBytes(OperationMode.LowNoise),
233                     GetTemperatureBytes(OperationMode.LowNoise)
234                 )
235             );
236 
237             RegisterCommand(Command.UltraLowNoiseTemperaturePressure,
238                 readHandler: _ => Enumerable.Concat(
239                     GetTemperatureBytes(OperationMode.UltraLowNoise),
240                     GetPressureBytes(OperationMode.UltraLowNoise)
241                 )
242             );
243 
244             RegisterCommand(Command.UltraLowNoisePressureTemperature,
245                 readHandler: _ => Enumerable.Concat(
246                     GetPressureBytes(OperationMode.UltraLowNoise),
247                     GetTemperatureBytes(OperationMode.UltraLowNoise)
248                 )
249             );
250 
251             RegisterCommand(Command.SoftReset,
252                 writeHandler: (_, __) =>
253                 {
254                     SoftwareReset();
255                 }
256             );
257 
258             RegisterCommand(Command.Id,
259                 readHandler: _ => BitHelper.GetBytesFromValue(0b001000, 2, reverse: false) // It's already in the correct byte order
260             );
261 
262             RegisterCommand(Command.CalibrationParametersPointer,
263                 writeHandler: (data, offset) =>
264                 {
265                     if(data.Length <= offset + 2)
266                     {
267                         this.Log(LogLevel.Error, "Read from OTP received not enough bytes, got: {0}, total of: {0} are expected", data.Length, offset + 3);
268                         return;
269                     }
270                     var addr = data[offset] << 16 | data[offset + 1] << 8 | data[offset + 2];
271                     if(addr != CalibrationAddress)
272                     {
273                         this.Log(LogLevel.Error, "Read from OTP from offset 0x{0:X}, different than 0x{1:X} is unsupported!", addr, CalibrationAddress);
274                         return;
275                     }
276                     // Just reset the current calibration value number, as there is no data
277                     // other than calibration values to be read from the sensor memory
278                     // and the docs don't mention another usage for this command at this moment (rev 1.2)
279                     calibrationValueIndex = 0;
280                 }
281             );
282 
283             RegisterCommand(Command.CalibrationParameters,
284                 readHandler: _ =>
285                 {
286                     ushort rets = CalibrationValues[calibrationValueIndex];
287                     calibrationValueIndex = (calibrationValueIndex + 1) % CalibrationValues.Length;
288                     return BitHelper.GetBytesFromValue(rets, 2, reverse: false);
289                 }
290             );
291         }
292 
RegisterCommand(Command command, Action<byte[], int> writeHandler = null, Func<int, IEnumerable<byte>> readHandler = null)293         private void RegisterCommand(Command command, Action<byte[], int> writeHandler = null, Func<int, IEnumerable<byte>> readHandler = null)
294         {
295             if(writeHandlers.ContainsKey(command) || readHandlers.ContainsKey(command))
296             {
297                 throw new RecoverableException("Attempted to register command twice");
298             }
299 
300             this.Log(LogLevel.Noisy, "Registered command 0x{0:X}.", command);
301 
302             writeHandlers.Add(command, writeHandler ?? ((data, offset) =>
303             {
304                 if(offset != data.Length)
305                 {
306                     this.Log(LogLevel.Warning, "Attempted write while handling read-only command '{0}'.", command);
307                 }
308             }));
309             readHandlers.Add(command, readHandler ?? (count =>
310             {
311                 if(count != 0)
312                 {
313                     this.Log(LogLevel.Warning, "Attempted read while handling write-only command '{0}'.", command);
314                 }
315                 return Enumerable.Empty<byte>();
316             }));
317         }
318 
UpdateCurrentTemperatureSample()319         private void UpdateCurrentTemperatureSample()
320         {
321             if(TryGetSampleFromRESDStream<TemperatureSample>(temperatureResdStream, out var sample))
322             {
323                 temperature = (decimal)sample.Temperature / 1e3m;
324             }
325             else
326             {
327                 temperature = DefaultTemperature;
328             }
329         }
330 
UpdateCurrentPressureSample()331         private void UpdateCurrentPressureSample()
332         {
333             if(TryGetSampleFromRESDStream<PressureSample>(pressureResdStream, out var sample))
334             {
335                 pressure = (long)(sample.Pressure / 1000);
336             }
337             else
338             {
339                 pressure = DefaultPressure;
340             }
341         }
342 
343         private bool TryGetSampleFromRESDStream<T>(RESDStream<T> stream, out T sample) where T : RESDSample, new()
344         {
345             sample = null;
346             if(stream == null)
347             {
348                 return false;
349             }
350 
351             if(machine.SystemBus.TryGetCurrentCPU(out var cpu))
352             {
353                 cpu.SyncTime();
354             }
355 
356             var currentTimestamp = machine.ClockSource.CurrentValue.TotalNanoseconds;
357             if(stream.TryGetSample(currentTimestamp, out sample) == RESDStreamStatus.OK)
358             {
359                 return true;
360             }
361             return false;
362         }
363 
GetTemperatureBytes(OperationMode mode)364         private IEnumerable<byte> GetTemperatureBytes(OperationMode mode)
365         {
366             return BitHelper.GetBytesFromValue(GetTemperature(mode), 2, reverse: false);
367         }
368 
GetTemperature(OperationMode _)369         private ushort GetTemperature(OperationMode _)
370         {
371             return (ushort)((Temperature + 45) * (1 << 16) / 175);
372         }
373 
GetPressure(OperationMode mode)374         private uint GetPressure(OperationMode mode)
375         {
376             GetCoefficients(mode, out long A, out long B, out long C);
377 
378             var decimalPressure = (B / (Pressure - A)) - C;
379             // Detect and report that the sensor might be miscalibrated
380             if(decimalPressure < 0 || decimalPressure > 0xFFFFFF)
381             {
382                 this.Log(LogLevel.Warning, "Raw pressure value is invalid: {0}. The sensor might be miscalibrated.", decimalPressure);
383             }
384             uint rawPressure = (uint)decimalPressure;
385 
386             this.Log(LogLevel.Noisy, "p_raw={0}, p_long={1}", rawPressure, decimalPressure);
387             return rawPressure;
388         }
389 
GetPressureBytes(OperationMode mode)390         private IEnumerable<byte> GetPressureBytes(OperationMode mode)
391         {
392             // Shift value left, since LLSB is discarded (has no meaning in pressure calculation)
393             return BitHelper.GetBytesFromValue((uint)(GetPressure(mode) << 8), 4, reverse: false);
394         }
395 
GetCoefficients(OperationMode mode, out long A, out long B, out long C)396         private void GetCoefficients(OperationMode mode, out long A, out long B, out long C)
397         {
398             long t = GetTemperature(mode) - 32768;
399             long p_LUT0 = LUT_lower + (CalibrationValues[0] * t * t) / InverseQuadraticFactor;
400             long p_LUT1 = OffsetFactor * CalibrationValues[3] + (CalibrationValues[1] * t * t) / InverseQuadraticFactor;
401             long p_LUT2 = LUT_upper + (CalibrationValues[2] * t * t) / InverseQuadraticFactor;
402 
403             C = (p_LUT0 * p_LUT1 * (p_Pa[0] - p_Pa[1]) +
404                  p_LUT1 * p_LUT2 * (p_Pa[1] - p_Pa[2]) +
405                  p_LUT2 * p_LUT0 * (p_Pa[2] - p_Pa[0])) /
406                 (p_LUT2 * (p_Pa[0] - p_Pa[1]) +
407                  p_LUT0 * (p_Pa[1] - p_Pa[2]) +
408                  p_LUT1 * (p_Pa[2] - p_Pa[0]));
409 
410             A = (p_Pa[0] * p_LUT0 - p_Pa[1] * p_LUT1 - (p_Pa[1] - p_Pa[0]) * C) / (p_LUT0 - p_LUT1);
411 
412             B = (p_Pa[0] - A) * (p_LUT0 + C);
413 
414             this.Log(LogLevel.Noisy, "A={0} B={1} C={2}, p_LUT0={3}, p_LUT1={4}, p_LUT2={5}", A, B, C, p_LUT0, p_LUT1, p_LUT2);
415         }
416 
InsertCrc(IEnumerable<byte> data)417         private IEnumerable<byte> InsertCrc(IEnumerable<byte> data)
418         {
419             var wordsNumber = 0;
420             var word = new byte[2];
421             foreach(var b in data)
422             {
423                 yield return b;
424                 word[wordsNumber % 2] = b;
425                 if(wordsNumber % 2 == 1)
426                 {
427                     yield return (byte)crcEngine.Calculate(word);
428                 }
429                 ++wordsNumber;
430             }
431             if(wordsNumber % 2 == 1)
432             {
433                 throw new InvalidOperationException("Data length has to be multiple of 2 bytes");
434             }
435         }
436 
GetCommandString()437         private string GetCommandString() => command != null ? $"0x{command:X} ({command})" : "None";
438 
HandleWriteDefault(byte[] data, int offset)439         private void HandleWriteDefault(byte[] data, int offset)
440         {
441             this.Log(LogLevel.Warning, "Unhandled write, command: {0}.", GetCommandString());
442         }
443 
HandleReadDefault(int count)444         private IEnumerable<byte> HandleReadDefault(int count)
445         {
446             this.Log(LogLevel.Warning, "Unhandled read, command: {0}.", GetCommandString());
447             return Enumerable.Empty<byte>();
448         }
449 
450         private Action<byte[], int> HandleWrite => writeHandlers.TryGetValue(command.Value, out var handler) ? handler : HandleWriteDefault;
451         private Func<int, IEnumerable<byte>> HandleRead => readHandlers.TryGetValue(command.Value, out var handler) ? handler : HandleReadDefault;
452 
453         private Command? command;
454 
455         private long defaultPressure;
456         private long pressure;
457         private decimal defaultTemperature;
458         private decimal temperature;
459 
460         private readonly CRCEngine crcEngine;
461         private readonly IMachine machine;
462         private readonly Dictionary<Command, Action<byte[], int>> writeHandlers;
463         private readonly Dictionary<Command, Func<int, IEnumerable<byte>>> readHandlers;
464 
465         // Which calibration value is read in incremental read-from OTP
466         private int calibrationValueIndex = 0;
467 
468         private RESDStream<TemperatureSample> temperatureResdStream;
469         private RESDStream<PressureSample> pressureResdStream;
470 
471         // Configuration constants, taken directly from the datasheet (https://invensense.tdk.com/wp-content/uploads/2021/06/DS-000408-ICP-10101-v1.2.pdf)
472         private readonly int[] p_Pa = new int [] { 45000, 80000, 105000 };
473         private const int LUT_lower = 3670016; //3.5 * (1 << 20);
474         private const int LUT_upper = 12058624; //11.5 * (1 << 20);
475         private const int InverseQuadraticFactor = 16777216;
476         private const int OffsetFactor = 2048;
477 
478         private const int CalibrationAddress = 0x00669C;
479 
480         public enum OperationMode
481         {
482             LowPower,
483             Normal,
484             LowNoise,
485             UltraLowNoise,
486         }
487 
488         public enum Command : ushort
489         {
490             LowPowerTemperaturePressure = 0x609C,
491             LowPowerPressureTemperature = 0x401A,
492             NormalTemperaturePressure = 0x6825,
493             NormalPressureTemperature = 0x48A3,
494             LowNoiseTemperaturePressure = 0x70DF,
495             LowNoisePressureTemperature = 0x5059,
496             UltraLowNoiseTemperaturePressure = 0x7866,
497             UltraLowNoisePressureTemperature = 0x58E0,
498             SoftReset = 0x805D,
499             Id = 0xEFC8,
500             CalibrationParametersPointer = 0xC595,
501             CalibrationParameters = 0xC7F7,
502         }
503     }
504 }
505