1 // 2 // Copyright (c) 2010-2025 Antmicro 3 // 4 // This file is licensed under the MIT License. 5 // Full license text is available in 'licenses/MIT.txt'. 6 // 7 8 using System; 9 using System.Collections.Generic; 10 using Antmicro.Renode.Core; 11 using Antmicro.Renode.Debugging; 12 using Antmicro.Renode.Time; 13 using Antmicro.Renode.Core.Structure.Registers; 14 using Antmicro.Renode.Logging; 15 using Antmicro.Renode.Peripherals.Sensor; 16 using Antmicro.Renode.Peripherals.SPI; 17 using Antmicro.Renode.Utilities; 18 using Antmicro.Renode.Utilities.RESD; 19 20 namespace Antmicro.Renode.Peripherals.Sensors 21 { 22 public class LSM6DSO_IMU : BasicBytePeripheral, ISPIPeripheral, IProvidesRegisterCollection<ByteRegisterCollection>, ITemperatureSensor, IUnderstandRESD 23 { LSM6DSO_IMU(IMachine machine)24 public LSM6DSO_IMU(IMachine machine) : base(machine) 25 { 26 Interrupt1 = new GPIO(); 27 28 commonFifo = new LSM6DSO_FIFO(machine, this, "fifo", MaxFifoWords); 29 commonFifo.OnOverrun += UpdateInterrupts; 30 31 DefineRegisters(); 32 Reset(); 33 } 34 FeedAccelerationSample(decimal x, decimal y, decimal z, uint repeat = 1)35 public void FeedAccelerationSample(decimal x, decimal y, decimal z, uint repeat = 1) 36 { 37 for(var i = 0; i < repeat; i++) 38 { 39 commonFifo.FeedAccelerationSample(x, y, z); 40 } 41 } 42 FeedAccelerationSamplesFromRESD(string path, uint channel = 0, ulong startTime = 0, RESDStreamSampleOffset sampleOffsetType = RESDStreamSampleOffset.Specified, long sampleOffsetTime = 0)43 public void FeedAccelerationSamplesFromRESD(string path, uint channel = 0, ulong startTime = 0, 44 RESDStreamSampleOffset sampleOffsetType = RESDStreamSampleOffset.Specified, long sampleOffsetTime = 0) 45 { 46 accelerometerResdStream = this.CreateRESDStream<AccelerationSample>(path, channel, sampleOffsetType, sampleOffsetTime); 47 accelerometerFeederThread?.Stop(); 48 accelerometerFeederThread = accelerometerResdStream.StartSampleFeedThread(this, 49 DataRateToFrequency(accelerometerFifoBatchingDataRateSelection.Value), 50 startTime: startTime 51 ); 52 } 53 FeedAngularRateSample(decimal x, decimal y, decimal z, uint repeat = 1)54 public void FeedAngularRateSample(decimal x, decimal y, decimal z, uint repeat = 1) 55 { 56 for(var i = 0; i < repeat; i++) 57 { 58 commonFifo.FeedAngularRateSample(x, y, z); 59 } 60 } 61 FeedAngularRateSamplesFromRESD(string path, uint channel = 0, ulong startTime = 0, RESDStreamSampleOffset sampleOffsetType = RESDStreamSampleOffset.Specified, long sampleOffsetTime = 0)62 public void FeedAngularRateSamplesFromRESD(string path, uint channel = 0, ulong startTime = 0, 63 RESDStreamSampleOffset sampleOffsetType = RESDStreamSampleOffset.Specified, long sampleOffsetTime = 0) 64 { 65 gyroResdStream = this.CreateRESDStream<AngularRateSample>(path, channel, sampleOffsetType, sampleOffsetTime); 66 gyroFeederThread?.Stop(); 67 gyroFeederThread = gyroResdStream.StartSampleFeedThread(this, 68 DataRateToFrequency(gyroscopeFifoBatchingDataRateSelection.Value), 69 startTime: startTime 70 ); 71 } 72 FeedTemperatureSamplesFromRESD(string path, uint channel)73 public void FeedTemperatureSamplesFromRESD(string path, uint channel) 74 { 75 temperatureResdStream = this.CreateRESDStream<TemperatureSample>(path, channel); 76 } 77 FinishTransmission()78 public void FinishTransmission() 79 { 80 this.Log(LogLevel.Noisy, "Finishing transmission, going to the Idle state"); 81 commandInProgress = CommandTypes.None; 82 } 83 Reset()84 public override void Reset() 85 { 86 SoftwareReset(); 87 88 accelerometerFeederThread?.Stop(); 89 accelerometerResdStream?.Dispose(); 90 accelerometerResdStream = null; 91 accelerometerFeederThread = null; 92 93 gyroFeederThread?.Stop(); 94 gyroResdStream?.Dispose(); 95 gyroResdStream = null; 96 gyroFeederThread = null; 97 98 temperatureResdStream?.Dispose(); 99 temperatureResdStream = null; 100 } 101 Transmit(byte data)102 public byte Transmit(byte data) 103 { 104 var value = (byte)0; 105 // Datasheet page 20 106 switch(commandInProgress) 107 { 108 case CommandTypes.None: 109 // The first transmission byte: 110 // b0-b6: address 111 // b7: 0 -- write; 1 -- read 112 address = BitHelper.GetValue(data, offset: 0, size: 7); 113 commandInProgress = (CommandTypes)BitHelper.GetValue(data, offset: IOTypeFlagPosition, size: 1); 114 this.Log(LogLevel.Noisy, "Received 0x{0:X2}; setting commandInProgress to {1} and address to 0x{2:X2}", data, commandInProgress, address); 115 break; 116 case CommandTypes.Read: 117 value = ReadByte(address); 118 this.Log(LogLevel.Noisy, "Read from 0x{0:X2} ({1}): returning 0x{2:X2}", address, (Registers)address, value); 119 TryIncrementAddress(); 120 break; 121 case CommandTypes.Write: 122 this.Log(LogLevel.Noisy, "Write to 0x{0:X2} ({1}): 0x{2:X2}", address, (Registers)address, data); 123 WriteByte(address, data); 124 TryIncrementAddress(); 125 break; 126 default: 127 throw new ArgumentException($"Invalid commandInProgress: {commandInProgress}"); 128 } 129 return value; 130 } 131 132 public bool FifoOverrunStatus => commonFifo.OverrunOccurred; 133 public uint FifoWatermarkThreshold => (uint)fifoThresholdBits0_7.Value | (fifoThresholdBit8.Value ? 0x100u : 0x0u); 134 public bool IsAccelerometerDataBatchedInFifo => IsDataRateEnabledAndDefined(accelerometerFifoBatchingDataRateSelection.Value); 135 public bool IsAccelerometerPoweredOn => IsDataRateEnabledAndDefined(accelerometerOutputDataRateSelection.Value); 136 public bool IsGyroscopeDataBatchedInFifo => IsDataRateEnabledAndDefined(gyroscopeFifoBatchingDataRateSelection.Value); 137 public bool IsGyroscopePoweredOn => IsDataRateEnabledAndDefined(gyroscopeOutputDataRateSelection.Value, isGyroscopeOutputDataRate: true); 138 public GPIO Interrupt1 { get; } 139 140 public decimal DefaultAccelerationX 141 { 142 get => defaultAccelerationX; 143 set => defaultAccelerationX = value; 144 } 145 146 public decimal DefaultAccelerationY 147 { 148 get => defaultAccelerationY; 149 set => defaultAccelerationY = value; 150 } 151 152 public decimal DefaultAccelerationZ 153 { 154 get => defaultAccelerationZ; 155 set => defaultAccelerationZ = value; 156 } 157 158 public decimal DefaultAngularRateX 159 { 160 get => defaultAngularRateX; 161 set => defaultAngularRateX = value; 162 } 163 164 public decimal DefaultAngularRateY 165 { 166 get => defaultAngularRateY; 167 set => defaultAngularRateY = value; 168 } 169 170 public decimal DefaultAngularRateZ 171 { 172 get => defaultAngularRateZ; 173 set => defaultAngularRateZ = value; 174 } 175 176 public decimal Temperature { get; set; } 177 DefineRegisters()178 protected override void DefineRegisters() 179 { 180 Registers.PinControl.Define(this) 181 // These bits are always set. 182 .WithValueField(0, 6, FieldMode.Read, valueProviderCallback: _ => 0x3F) 183 .WithTaggedFlag("SDO_PU_EN", 6) 184 .WithTaggedFlag("OIS_PU_DIS", 7) 185 ; 186 187 Registers.WhoAmI.Define(this) 188 .WithValueField(0, 8, FieldMode.Read, name: "WHO_AM_I", valueProviderCallback: _ => 0x6C) 189 ; 190 191 Registers.Control1_Accelerometer.Define(this) 192 .WithReservedBits(0, 1) 193 .WithTaggedFlag("LPF2_XL_EN: Accelerometer high-resolution selection", 1) 194 .WithEnumField(2, 2, out accelerationFullScaleSelection, name: "FS_XL: Accelerometer full-scale selection", 195 changeCallback: (_, __) => UpdateAccelerationSensitivity()) 196 .WithEnumField(4, 4, out accelerometerOutputDataRateSelection, name: "ODR_XL: Accelerometer Output Data Rate") 197 ; 198 199 Registers.Control2_Gyroscope.Define(this) 200 .WithReservedBits(0, 1) 201 .WithFlag(1, out angularRate125DPSSelection, name: "FS_125: Selects gyro UI chain full-scale 125 dps", 202 changeCallback: (_, __) => UpdateAngularRateSensitivity()) 203 .WithEnumField(2, 2, out angularRateFullScaleSelection, name: "FS_G: Gyroscope UI chain full-scale selection", 204 changeCallback: (_, __) => UpdateAngularRateSensitivity()) 205 .WithEnumField(4, 4, out gyroscopeOutputDataRateSelection, name: "ODR_G: Gyroscope Output Data Rate") 206 ; 207 208 Registers.Control3.Define(this, resetValue: 0b00000100) 209 .WithFlag(0, FieldMode.WriteOneToClear, name: "SW_RESET", changeCallback: (_, newValue) => { if(newValue) SoftwareReset(); }) 210 .WithReservedBits(1, 1) 211 .WithFlag(2, out addressAutoIncrement, name: "IF_INC") 212 .WithTaggedFlag("SIM", 3) 213 .WithTaggedFlag("PP_OD", 4) 214 .WithTaggedFlag("H_LACTIVE", 5) 215 .WithTaggedFlag("BDU", 6) 216 .WithTaggedFlag("BOOT", 7) 217 ; 218 219 Registers.Control8_Accelerometer.Define(this) 220 .WithTaggedFlag("LOW_PASS_ON_6D", 0) 221 .WithFlag(1, out accelerationFullScaleMode, name: "XL_FS_MODE: Accelerometer full-scale management between UI chain and OIS chain", 222 changeCallback: (_, __) => UpdateAccelerationSensitivity()) 223 .WithTaggedFlag("HP_SLOPE_XL_EN", 2) 224 .WithTaggedFlag("FASTSETTL_MODE_XL", 3) 225 .WithTaggedFlag("HP_REF_MODE_XL", 4) 226 .WithTag("HPCF_XL", 5, 3) 227 ; 228 229 Registers.TemperatureLow.Define(this) 230 .WithValueField(0, 8, FieldMode.Read, name: "OUT_TEMP_L", valueProviderCallback: _ => 231 { 232 TryUpdateCurrentTemperatureSample(); 233 return GetScaledTemperatureValue(upperByte: false); 234 }) 235 ; 236 237 Registers.TemperatureHigh.Define(this) 238 .WithValueField(0, 8, FieldMode.Read, name: "OUT_TEMP_H", valueProviderCallback: _ => GetScaledTemperatureValue(upperByte: true)) 239 ; 240 241 Registers.FifoStatus1.Define(this) 242 .WithValueField(0, 8, FieldMode.Read, name: "DIFF_FIFO_0-7", valueProviderCallback: _ => BitHelper.GetValue(commonFifo.SamplesCount, 0, 8)) 243 ; 244 245 Registers.FifoStatus2.Define(this) 246 .WithValueField(0, 2, FieldMode.Read, name: "DIFF_FIFO_8-9", valueProviderCallback: _ => BitHelper.GetValue(commonFifo.SamplesCount, 8, 2)) 247 .WithReservedBits(2, 1) 248 // "This bit is reset when this register is read." 249 .WithFlag(3, FieldMode.Read, name: "FIFO_OVR_LATCHED", valueProviderCallback: _ => FifoOverrunStatus && !previousFifoOverrunStatus) 250 .WithTaggedFlag("COUNTER_BDR_IA", 4) 251 // "FIFO will be full at the next Output Data Rate." 252 // Let's check which sensor provider has both power and FIFO batching enabled. Providers are accelerometer and gyroscope. 253 // Return true if there are more than 'MAX - (number of such providers)' samples in FIFO. 254 .WithFlag(5, FieldMode.Read, name: "FIFO_FULL_IA", valueProviderCallback: _ => 255 { 256 var samplesAddedToFifoEveryODR = (IsAccelerometerDataBatchedInFifo && IsAccelerometerPoweredOn ? 1u : 0u) + (IsGyroscopeDataBatchedInFifo && IsGyroscopePoweredOn ? 1u : 0u); 257 return commonFifo.CountReached(MaxFifoWords - samplesAddedToFifoEveryODR); 258 }) 259 .WithFlag(6, FieldMode.Read, name: "FIFO_OVR_IA", valueProviderCallback: _ => FifoOverrunStatus) 260 .WithFlag(7, FieldMode.Read, name: "FIFO_WTM_IA", valueProviderCallback: _ => FifoWatermarkThreshold != 0 && commonFifo.CountReached(FifoWatermarkThreshold)) 261 .WithReadCallback((_, __) => previousFifoOverrunStatus = FifoOverrunStatus) 262 ; 263 264 Registers.FifoControl1.Define(this) 265 .WithValueField(0, 8, out fifoThresholdBits0_7, name: "WTM0-7") 266 ; 267 268 Registers.FifoControl2.Define(this) 269 .WithFlag(0, out fifoThresholdBit8, name: "WTM8") 270 .WithTag("UNCOPTR_RATE", 1, 2) 271 .WithReservedBits(3, 1) 272 .WithTaggedFlag("ODRCHG_EN", 4) 273 .WithReservedBits(5, 1) 274 .WithTaggedFlag("FIFO_COMPR_RT_EN", 6) 275 .WithTaggedFlag("STOP_ON_WTM", 7) 276 ; 277 278 Registers.FifoControl3.Define(this) 279 .WithEnumField(0, 4, out accelerometerFifoBatchingDataRateSelection, name: "BDR_XL", 280 changeCallback: (_, __) => 281 { 282 UpdateAccelerationSampleFrequency(); 283 if(accelerometerFifoBatchingDataRateSelection.Value != DataRates.Disabled && accelerometerFeederThread == null) 284 { 285 accelerometerFeederThread = CreateAccelerationDefaultSampleFeeder(); 286 } 287 } 288 ) 289 .WithEnumField(4, 4, out gyroscopeFifoBatchingDataRateSelection, name: "BDR_GY", 290 changeCallback: (_, __) => 291 { 292 UpdateAngularRateSampleFrequency(); 293 if(gyroscopeFifoBatchingDataRateSelection.Value != DataRates.Disabled && gyroFeederThread == null) 294 { 295 gyroFeederThread = CreateAngularRateDefaultSampleFeeder(); 296 } 297 } 298 ) 299 ; 300 301 Registers.FifoControl4.Define(this) 302 .WithEnumField<ByteRegister, FifoModes>(0, 3, name: "FIFO_MODE", writeCallback: (_, newValue) => commonFifo.Mode = newValue, valueProviderCallback: _ => commonFifo.Mode) 303 .WithReservedBits(3, 1) 304 .WithTag("ODR_T_BATCH", 4, 2) 305 .WithTag("DEC_TS_BATCH", 6, 2) 306 ; 307 308 // New sample is dequeued when this register is read as it's the first "FifoOutput" register. There's no information on when samples are dequeued in the datasheet. 309 Registers.FifoOutputTag.Define(this) 310 .WithTaggedFlag("TAG_PARITY", 0) 311 .WithTag("TAG_CNT", 1, 2) 312 .WithEnumField<ByteRegister, FifoTag>(3, 5, FieldMode.Read, valueProviderCallback: _ => commonFifo.TryDequeueNewSample() ? commonFifo.Sample.Tag : FifoTag.UNKNOWN) 313 ; 314 315 DefineOutputRegistersGroup(Registers.AccelerometerXLow, "OUT{0}_A", () => commonFifo.AccelerationSample); 316 DefineOutputRegistersGroup(Registers.FifoOutputXLow, "FIFO_DATA_OUT_{0}", () => commonFifo.Sample); 317 DefineOutputRegistersGroup(Registers.GyroscopeXLow, "OUT{0}_G", () => commonFifo.AngularRateSample); 318 319 Registers.Int1PinControl.Define(this) 320 .WithTaggedFlag("INT1_DRDY_XL", 0) 321 .WithTaggedFlag("INT1_DRDY_G", 1) 322 .WithTaggedFlag("INT1_BOOT", 2) 323 .WithTaggedFlag("INT1_TH", 3) 324 .WithFlag(4, out interrupt1EnableFifoOverrun, name: "INT1_FIFO_OVR") 325 .WithTaggedFlag("INT1_FULL", 5) 326 .WithTaggedFlag("INT1_CNT_BDR", 6) 327 .WithTaggedFlag("DEN_DRDY", 7) 328 .WithWriteCallback((_, __) => UpdateInterrupts()) 329 ; 330 } 331 DefineOutputRegistersGroup(Registers firstGroupRegister, string nameFormat, Func<LSM6DSO_Vector3DSample> sampleProvider)332 private void DefineOutputRegistersGroup(Registers firstGroupRegister, string nameFormat, Func<LSM6DSO_Vector3DSample> sampleProvider) 333 { 334 var subsequentRegistersInfo = new SampleReadingRegisterInfo[] 335 { 336 new SampleReadingRegisterInfo { Axis = LSM6DSO_Vector3DSample.Axes.X, NameSuffix = "X_L", UpperByte = false}, 337 new SampleReadingRegisterInfo { Axis = LSM6DSO_Vector3DSample.Axes.X, NameSuffix = "X_H", UpperByte = true}, 338 new SampleReadingRegisterInfo { Axis = LSM6DSO_Vector3DSample.Axes.Y, NameSuffix = "Y_L", UpperByte = false}, 339 new SampleReadingRegisterInfo { Axis = LSM6DSO_Vector3DSample.Axes.Y, NameSuffix = "Y_H", UpperByte = true}, 340 new SampleReadingRegisterInfo { Axis = LSM6DSO_Vector3DSample.Axes.Z, NameSuffix = "Z_L", UpperByte = false}, 341 new SampleReadingRegisterInfo { Axis = LSM6DSO_Vector3DSample.Axes.Z, NameSuffix = "Z_H", UpperByte = true}, 342 }; 343 344 firstGroupRegister.DefineMany(this, 6, (register, registerOffset) => 345 { 346 var registerInfo = subsequentRegistersInfo[registerOffset]; 347 var name = string.Format(nameFormat, registerInfo.NameSuffix); 348 register.WithValueField(0, 8, FieldMode.Read, name: name, 349 valueProviderCallback: _ => 350 { 351 var sample = sampleProvider(); 352 if(sample == null) 353 { 354 return 0u; 355 } 356 DebugHelper.Assert(sample.IsAccelerationSample || sample.IsAngularRateSample, $"Invalid sample: {sample}"); 357 358 var sensitivity = sample.IsAccelerationSample ? accelerationSensitivity : angularRateSensitivity; 359 var _byte = sample.GetScaledValueByte(registerInfo.Axis, sensitivity, registerInfo.UpperByte, out var realScaledValue); 360 361 // Log only when reading the lower byte to avoid logging it twice for each value. 362 if(realScaledValue.HasValue && !registerInfo.UpperByte) 363 { 364 var fullScaleSelection = sample.IsAccelerationSample ? $"{GetAccelerationFullScaleValue()}G" : $"{GetAngularRateFullScaleValue()}DPS"; 365 this.Log(LogLevel.Debug, "Invalid value for the current full scale selection ({0}): {1}", fullScaleSelection, realScaledValue.Value); 366 } 367 return _byte; 368 } 369 ); 370 }); 371 } 372 373 [OnRESDSample(SampleType.Acceleration)] 374 [BeforeRESDSample(SampleType.Acceleration)] HandleAccelerationSample(AccelerationSample sample, TimeInterval timestamp)375 private void HandleAccelerationSample(AccelerationSample sample, TimeInterval timestamp) 376 { 377 if(sample != null) 378 { 379 commonFifo.FeedAccelerationSample( 380 (decimal)sample.AccelerationX / 1e6m, 381 (decimal)sample.AccelerationY / 1e6m, 382 (decimal)sample.AccelerationZ / 1e6m 383 ); 384 } 385 else 386 { 387 commonFifo.FeedAccelerationSample( 388 DefaultAccelerationX, 389 DefaultAccelerationY, 390 DefaultAccelerationZ 391 ); 392 } 393 } 394 395 [AfterRESDSample(SampleType.Acceleration)] HandleAccelerationSampleEnded(AccelerationSample sample, TimeInterval timestamp)396 private void HandleAccelerationSampleEnded(AccelerationSample sample, TimeInterval timestamp) 397 { 398 accelerometerFeederThread?.Stop(); 399 accelerometerFeederThread = CreateAccelerationDefaultSampleFeeder(); 400 } 401 402 [OnRESDSample(SampleType.AngularRate)] 403 [BeforeRESDSample(SampleType.AngularRate)] HandleAngularRateSample(AngularRateSample sample, TimeInterval timestamp)404 private void HandleAngularRateSample(AngularRateSample sample, TimeInterval timestamp) 405 { 406 if(sample != null) 407 { 408 commonFifo.FeedAngularRateSample( 409 RadiansToDegrees * (decimal)sample.AngularRateX / 1e5m, 410 RadiansToDegrees * (decimal)sample.AngularRateY / 1e5m, 411 RadiansToDegrees * (decimal)sample.AngularRateZ / 1e5m 412 ); 413 } 414 else 415 { 416 commonFifo.FeedAngularRateSample( 417 DefaultAngularRateX, 418 DefaultAngularRateY, 419 DefaultAngularRateZ 420 ); 421 } 422 } 423 424 [AfterRESDSample(SampleType.AngularRate)] HandleAngularRateSampleEnded(AngularRateSample sample, TimeInterval timestamp)425 private void HandleAngularRateSampleEnded(AngularRateSample sample, TimeInterval timestamp) 426 { 427 gyroFeederThread.Stop(); 428 gyroFeederThread = CreateAngularRateDefaultSampleFeeder(); 429 } 430 431 GetAccelerationFullScaleValue()432 private short GetAccelerationFullScaleValue() 433 { 434 switch(accelerationFullScaleSelection.Value) 435 { 436 case AccelerationFullScaleSelection.Mode0_2G: 437 return 2; 438 case AccelerationFullScaleSelection.Mode1_16G_2G: 439 return (short)(accelerationFullScaleMode.Value ? 2 : 16); 440 case AccelerationFullScaleSelection.Mode2_4G: 441 return 4; 442 case AccelerationFullScaleSelection.Mode3_8G: 443 return 8; 444 default: 445 throw new Exception("Wrong acceleration full scale selection"); 446 } 447 } 448 449 // Sensitivity values come directly from the datasheet. The unit is 'mDPS/LSB' which is different than typical 450 // 'LSB/DPS' or 'LSB/G' sensitivity is expected to be for proper conversions (e.g. in 'GetScaledValueByte'). GetAngularRateSensitivityInMilliDPSPerLSB()451 private decimal GetAngularRateSensitivityInMilliDPSPerLSB() 452 { 453 var fullScaleValue = GetAngularRateFullScaleValue(); 454 switch(fullScaleValue) 455 { 456 case 125: 457 return 4.375m; 458 case 250: 459 return 8.75m; 460 case 500: 461 return 17.5m; 462 case 1000: 463 return 35m; 464 case 2000: 465 return 70m; 466 default: 467 throw new ArgumentException($"Invalid angular rate full scale value: {fullScaleValue}"); 468 } 469 } 470 GetAngularRateFullScaleValue()471 private short GetAngularRateFullScaleValue() 472 { 473 if(angularRate125DPSSelection.Value) 474 { 475 return 125; 476 } 477 478 switch(angularRateFullScaleSelection.Value) 479 { 480 case AngularRateFullScaleSelection.Mode0_250DPS: 481 return 250; 482 case AngularRateFullScaleSelection.Mode1_500DPS: 483 return 500; 484 case AngularRateFullScaleSelection.Mode2_1000DPS: 485 return 1000; 486 case AngularRateFullScaleSelection.Mode3_2000DPS: 487 return 2000; 488 default: 489 throw new Exception("Wrong angular rate full scale selection"); 490 } 491 } 492 GetScaledTemperatureValue(bool upperByte)493 private byte GetScaledTemperatureValue(bool upperByte) 494 { 495 var scaled = (short)((Temperature - TemperatureOffset) * TemperatureSensitivity); 496 return upperByte 497 ? (byte)(scaled >> 8) 498 : (byte)scaled; 499 } 500 IsDataRateEnabledAndDefined(DataRates value, bool isGyroscopeOutputDataRate = false)501 private bool IsDataRateEnabledAndDefined(DataRates value, bool isGyroscopeOutputDataRate = false) 502 { 503 var enabled = value != DataRates.Disabled && Enum.IsDefined(typeof(DataRates), value); 504 if(isGyroscopeOutputDataRate && enabled) 505 { 506 // This specific selection is invalid for the Gyroscope's Output Data Rate. 507 enabled = value != DataRates._1_6HzOr6_5HzOr12_5Hz; 508 } 509 return enabled; 510 } 511 SoftwareReset()512 private void SoftwareReset() 513 { 514 address = 0x0; 515 commandInProgress = CommandTypes.None; 516 previousFifoOverrunStatus = false; 517 518 commonFifo.Reset(); 519 520 base.Reset(); 521 522 UpdateAccelerationSensitivity(); 523 UpdateAngularRateSensitivity(); 524 UpdateInterrupts(); 525 } 526 TryUpdateCurrentTemperatureSample()527 private bool TryUpdateCurrentTemperatureSample() 528 { 529 if(temperatureResdStream == null) 530 { 531 return false; 532 } 533 534 if(machine.SystemBus.TryGetCurrentCPU(out var cpu)) 535 { 536 cpu.SyncTime(); 537 } 538 539 var currentTimestamp = machine.ClockSource.CurrentValue.TotalNanoseconds; 540 if(temperatureResdStream.TryGetSample(currentTimestamp, out var sample) == RESDStreamStatus.OK) 541 { 542 Temperature = (decimal)sample.Temperature / 1e3m; 543 } 544 545 return true; 546 } 547 TryIncrementAddress()548 private void TryIncrementAddress() 549 { 550 // automatic rounding when accessing FIFO registers 551 if(address == (byte)Registers.FifoOutputZHigh) 552 { 553 address = (byte)Registers.FifoOutputTag; 554 return; 555 } 556 557 if(!addressAutoIncrement.Value) 558 { 559 return; 560 } 561 562 // It's undocumented whether 0x0 should really be accessed after 0x7F. 563 address = (byte)((address + 1) % 0x80); 564 } 565 UpdateAccelerationSensitivity()566 private void UpdateAccelerationSensitivity() 567 { 568 accelerationSensitivity = decimal.Divide(MaxAbsoluteShortValue, GetAccelerationFullScaleValue()); 569 } 570 UpdateAngularRateSensitivity()571 private void UpdateAngularRateSensitivity() 572 { 573 // Gyroscope's sensitivity deviates from a typical '2**15 / full_scale' calculation. 574 // Sensitivities in the datasheet are in milliDPS per LSB so let's convert to LSB per DPS. 575 angularRateSensitivity = 1000m / GetAngularRateSensitivityInMilliDPSPerLSB(); 576 } 577 UpdateInterrupts()578 private void UpdateInterrupts() 579 { 580 var newInt1Status = false; 581 if(interrupt1EnableFifoOverrun.Value && FifoOverrunStatus) 582 { 583 newInt1Status = true; 584 } 585 586 if(Interrupt1.IsSet != newInt1Status) 587 { 588 this.Log(LogLevel.Debug, "New INT1 state: {0}", newInt1Status ? "set" : "reset"); 589 Interrupt1.Set(newInt1Status); 590 } 591 } 592 DataRateToFrequency(DataRates dr)593 private uint DataRateToFrequency(DataRates dr) 594 { 595 switch(dr) 596 { 597 // here we select the middle value, rounded down as currently we don't support fractions of Hz when declaring frequencies 598 case DataRates._1_6HzOr6_5HzOr12_5Hz: 599 return 5; 600 case DataRates._12_5Hz: 601 // here we need to round the value down as currently we don't support fractions of Hz when declaring frequencies 602 return 12; 603 case DataRates._26Hz: 604 return 26; 605 case DataRates._52Hz: 606 return 52; 607 case DataRates._104Hz: 608 return 104; 609 case DataRates._208Hz: 610 return 208; 611 case DataRates._416Hz: 612 return 416; 613 case DataRates._833Hz: 614 return 833; 615 case DataRates._1_66kHz: 616 return 1660; 617 case DataRates._3_33kHz: 618 return 3330; 619 case DataRates._6_66kHz: 620 return 6660; 621 case DataRates.Disabled: 622 return 0; 623 default: 624 throw new Exception($"Unexpected data rate: {dr}"); 625 } 626 } 627 628 private RESDStream<AccelerationSample> accelerometerResdStream; 629 private RESDStream<AngularRateSample> gyroResdStream; 630 private RESDStream<TemperatureSample> temperatureResdStream; 631 private IManagedThread accelerometerFeederThread; 632 private IManagedThread gyroFeederThread; 633 634 private decimal accelerationSensitivity; 635 private byte address; 636 private decimal angularRateSensitivity; 637 private CommandTypes commandInProgress; 638 private bool previousFifoOverrunStatus; 639 640 private IEnumRegisterField<AccelerationFullScaleSelection> accelerationFullScaleSelection; 641 private IFlagRegisterField accelerationFullScaleMode; 642 private IEnumRegisterField<DataRates> accelerometerFifoBatchingDataRateSelection; 643 private IEnumRegisterField<DataRates> accelerometerOutputDataRateSelection; 644 private IFlagRegisterField addressAutoIncrement; 645 private IFlagRegisterField angularRate125DPSSelection; 646 private IEnumRegisterField<AngularRateFullScaleSelection> angularRateFullScaleSelection; 647 private IValueRegisterField fifoThresholdBits0_7; 648 private IFlagRegisterField fifoThresholdBit8; 649 private IEnumRegisterField<DataRates> gyroscopeFifoBatchingDataRateSelection; 650 private IEnumRegisterField<DataRates> gyroscopeOutputDataRateSelection; 651 private IFlagRegisterField interrupt1EnableFifoOverrun; 652 653 private readonly LSM6DSO_FIFO commonFifo; 654 655 private const int IOTypeFlagPosition = 7; 656 private const int MaxAbsoluteShortValue = 32768; 657 private const uint MaxFifoWords = 512; 658 private const short TemperatureOffset = 25; 659 private const short TemperatureSensitivity = 256; 660 private const decimal RadiansToDegrees = 180m / (decimal)Math.PI; 661 662 private class LSM6DSO_FIFO 663 { LSM6DSO_FIFO(IMachine machine, LSM6DSO_IMU owner, string name, uint capacity)664 public LSM6DSO_FIFO(IMachine machine, LSM6DSO_IMU owner, string name, uint capacity) 665 { 666 Capacity = capacity; 667 this.machine = machine; 668 this.name = name; 669 this.owner = owner; 670 this.locker = new object(); 671 this.queue = new Queue<LSM6DSO_Vector3DSample>(); 672 } 673 CountReached(uint value)674 public bool CountReached(uint value) 675 { 676 return SamplesCount >= value; 677 } 678 FeedAccelerationSample(decimal x, decimal y, decimal z)679 public void FeedAccelerationSample(decimal x, decimal y, decimal z) 680 { 681 var sample = new LSM6DSO_Vector3DSample(DefaultAccelerometerTag, x, y, z); 682 FeedSample(sample); 683 } 684 FeedAngularRateSample(decimal x, decimal y, decimal z)685 public void FeedAngularRateSample(decimal x, decimal y, decimal z) 686 { 687 var sample = new LSM6DSO_Vector3DSample(DefaultGyroscopeTag, x, y, z); 688 FeedSample(sample); 689 } 690 FeedSample(LSM6DSO_Vector3DSample sample)691 public void FeedSample(LSM6DSO_Vector3DSample sample) 692 { 693 lock(locker) 694 { 695 if(Mode == FifoModes.Bypass) 696 { 697 // In bypass mode don't add samples to queue, just keep like the latest sample. 698 KeepSample(sample); 699 return; 700 } 701 702 if(Mode == FifoModes.Continuous && Full) 703 { 704 if(!OverrunOccurred) 705 { 706 owner.Log(LogLevel.Debug, $"{name}: Overrun"); 707 OverrunOccurred = true; 708 OnOverrun?.Invoke(); 709 } 710 711 owner.Log(LogLevel.Noisy, $"{name}: Fifo filled up. Dumping the oldest sample."); 712 queue.TryDequeue<LSM6DSO_Vector3DSample>(out _); 713 } 714 715 queue.Enqueue(sample); 716 } 717 } 718 Reset()719 public void Reset() 720 { 721 owner.Log(LogLevel.Debug, "Resetting FIFO"); 722 723 queue.Clear(); 724 accelerationSample = null; 725 angularRateSample = null; 726 mode = FifoModes.Bypass; 727 OverrunOccurred = false; 728 } 729 TryDequeueNewSample()730 public bool TryDequeueNewSample() 731 { 732 lock(locker) 733 { 734 if(CheckEnabled() && queue.TryDequeue(out var sample)) 735 { 736 owner.Log(LogLevel.Noisy, "New sample dequeued: {0}", sample); 737 KeepSample(sample); 738 return true; 739 } 740 } 741 owner.Log(LogLevel.Noisy, "Dequeueing new sample failed."); 742 return false; 743 } 744 745 public LSM6DSO_Vector3DSample AccelerationSample => GetSample(DefaultAccelerometerTag); 746 public LSM6DSO_Vector3DSample AngularRateSample => GetSample(DefaultGyroscopeTag); 747 public uint SamplesCount => (uint)queue.Count; 748 public bool Disabled => Mode == FifoModes.Bypass; 749 public bool Empty => SamplesCount == 0; 750 public bool Full => SamplesCount >= Capacity; 751 752 public FifoModes Mode 753 { 754 get => mode; 755 set 756 { 757 if(value == FifoModes.Bypass) 758 { 759 Reset(); 760 } 761 mode = value; 762 } 763 } 764 765 public bool OverrunOccurred { get; private set; } 766 public LSM6DSO_Vector3DSample Sample => latestSample?.Tag != FifoTag.UNKNOWN ? latestSample : null; 767 768 public event Action OnOverrun; 769 770 // Currently all Accelerometer/Gyroscope tags are treated the same so it doesn't matter which ones are default. 771 public const FifoTag DefaultAccelerometerTag = FifoTag.AccelerometerNC; 772 public const FifoTag DefaultGyroscopeTag = FifoTag.GyroscopeNC; 773 CheckEnabled()774 private bool CheckEnabled() 775 { 776 if(Disabled) 777 { 778 owner.Log(LogLevel.Debug, "Sample unavailable -- FIFO disabled."); 779 return false; 780 } 781 return true; 782 } 783 GetSample(FifoTag tag)784 private LSM6DSO_Vector3DSample GetSample(FifoTag tag) 785 { 786 LSM6DSO_Vector3DSample sample; 787 switch(tag) 788 { 789 case DefaultAccelerometerTag: 790 sample = accelerationSample; 791 break; 792 case DefaultGyroscopeTag: 793 sample = angularRateSample; 794 break; 795 default: 796 throw new ArgumentException($"Tried to get sample for an unsupported tag: {tag}"); 797 } 798 799 if(sample == null) 800 { 801 owner.Log(LogLevel.Warning, "{0}: No sample found with {1} tag", name, tag); 802 } 803 return sample; 804 } 805 KeepSample(LSM6DSO_Vector3DSample sample)806 private void KeepSample(LSM6DSO_Vector3DSample sample) 807 { 808 latestSample = sample; 809 if(sample.IsAccelerationSample) 810 { 811 accelerationSample = sample; 812 } 813 else if(sample.IsAngularRateSample) 814 { 815 angularRateSample = sample; 816 } 817 else 818 { 819 throw new Exception($"Invalid sample to keep: tag={sample.Tag}"); 820 } 821 } 822 823 private uint Capacity { get; } 824 825 private LSM6DSO_Vector3DSample accelerationSample; 826 private LSM6DSO_Vector3DSample angularRateSample; 827 private LSM6DSO_Vector3DSample latestSample; 828 private FifoModes mode; 829 830 private readonly object locker; 831 private readonly Queue<LSM6DSO_Vector3DSample> queue; 832 private readonly IMachine machine; 833 private readonly string name; 834 private readonly LSM6DSO_IMU owner; 835 } 836 837 private class LSM6DSO_Vector3DSample 838 { LSM6DSO_Vector3DSample(FifoTag tag)839 public LSM6DSO_Vector3DSample(FifoTag tag) 840 { 841 Tag = tag; 842 } 843 LSM6DSO_Vector3DSample(FifoTag tag, decimal x, decimal y, decimal z)844 public LSM6DSO_Vector3DSample(FifoTag tag, decimal x, decimal y, decimal z) : this(tag) 845 { 846 X = x; 847 Y = y; 848 Z = z; 849 } 850 GetAxisValue(Axes axis)851 public decimal GetAxisValue(Axes axis) 852 { 853 switch(axis) 854 { 855 case Axes.X: 856 return X; 857 case Axes.Y: 858 return Y; 859 case Axes.Z: 860 return Z; 861 default: 862 throw new Exception($"Invalid Axis: {axis}"); 863 } 864 } 865 GetScaledValueByte(Axes axis, decimal sensitivity, bool upperByte, out long? realScaledValue)866 public byte GetScaledValueByte(Axes axis, decimal sensitivity, bool upperByte, out long? realScaledValue) 867 { 868 realScaledValue = null; 869 var unscaledValue = GetAxisValue(axis); 870 var scaledValue = (long)decimal.Round(unscaledValue * sensitivity, 0); 871 short value; 872 if(scaledValue > short.MaxValue) 873 { 874 realScaledValue = scaledValue; 875 value = short.MaxValue; 876 } 877 else if(scaledValue < short.MinValue) 878 { 879 realScaledValue = scaledValue; 880 value = short.MinValue; 881 } 882 else 883 { 884 value = (short)scaledValue; 885 } 886 887 return upperByte 888 ? (byte)(value >> 8) 889 : (byte)value; 890 } 891 ToString()892 public override string ToString() 893 { 894 return $"[Tag: {Tag}, X: {X:F6}, Y: {Y:F6}, Z: {Z:F6}]"; 895 } 896 897 public bool IsAccelerationSample { get; private set; } 898 public bool IsAngularRateSample { get; private set; } 899 900 public decimal X { get; set; } 901 public decimal Y { get; set; } 902 public decimal Z { get; set; } 903 904 public FifoTag Tag 905 { 906 get => tag; 907 private set 908 { 909 tag = value; 910 switch(tag) 911 { 912 case FifoTag.Accelerometer2xC: 913 case FifoTag.Accelerometer3xC: 914 case FifoTag.AccelerometerNC: 915 case FifoTag.AccelerometerNC_T_1: 916 case FifoTag.AccelerometerNC_T_2: 917 IsAccelerationSample = true; 918 break; 919 case FifoTag.Gyroscope2xC: 920 case FifoTag.Gyroscope3xC: 921 case FifoTag.GyroscopeNC: 922 case FifoTag.GyroscopeNC_T_1: 923 case FifoTag.GyroscopeNC_T_2: 924 IsAngularRateSample = true; 925 break; 926 } 927 } 928 } 929 930 public enum Axes 931 { 932 X, 933 Y, 934 Z, 935 } 936 937 private FifoTag tag; 938 } 939 CreateAccelerationDefaultSampleFeeder()940 private IManagedThread CreateAccelerationDefaultSampleFeeder() 941 { 942 if(accelerometerFifoBatchingDataRateSelection.Value == DataRates.Disabled) 943 { 944 return null; 945 } 946 947 return CreateDefaultSampleFeeder( 948 () => commonFifo.FeedAccelerationSample(DefaultAccelerationX, DefaultAccelerationY, DefaultAccelerationZ), 949 DataRateToFrequency(accelerometerFifoBatchingDataRateSelection.Value), 950 "acceleration"); 951 } 952 CreateAngularRateDefaultSampleFeeder()953 private IManagedThread CreateAngularRateDefaultSampleFeeder() 954 { 955 if(gyroscopeFifoBatchingDataRateSelection.Value == DataRates.Disabled) 956 { 957 return null; 958 } 959 960 return CreateDefaultSampleFeeder( 961 () => commonFifo.FeedAngularRateSample(DefaultAngularRateX, DefaultAngularRateY, DefaultAngularRateZ), 962 DataRateToFrequency(gyroscopeFifoBatchingDataRateSelection.Value), 963 "gyro"); 964 } 965 CreateDefaultSampleFeeder(Action action, uint frequency, String name)966 private IManagedThread CreateDefaultSampleFeeder(Action action, uint frequency, String name) 967 { 968 var feeder = machine.ObtainManagedThread(action, frequency, name: $"{name} default feeder", owner: this); 969 970 action(); 971 feeder.Start(); 972 return feeder; 973 } 974 UpdateAccelerationSampleFrequency()975 private void UpdateAccelerationSampleFrequency() 976 { 977 if(accelerometerFeederThread == null) 978 { 979 return; 980 } 981 982 if(accelerometerFifoBatchingDataRateSelection.Value != DataRates.Disabled) 983 { 984 var freq = DataRateToFrequency(accelerometerFifoBatchingDataRateSelection.Value); 985 accelerometerFeederThread.Frequency = freq; 986 accelerometerFeederThread.Start(); 987 } 988 else 989 { 990 accelerometerFeederThread.Stop(); 991 } 992 } 993 UpdateAngularRateSampleFrequency()994 private void UpdateAngularRateSampleFrequency() 995 { 996 if(gyroFeederThread == null) 997 { 998 return; 999 } 1000 1001 if(gyroscopeFifoBatchingDataRateSelection.Value != DataRates.Disabled) 1002 { 1003 var freq = DataRateToFrequency(gyroscopeFifoBatchingDataRateSelection.Value); 1004 gyroFeederThread.Frequency = freq; 1005 gyroFeederThread.Start(); 1006 } 1007 else 1008 { 1009 accelerometerFeederThread.Stop(); 1010 } 1011 } 1012 1013 private decimal defaultAccelerationX; 1014 private decimal defaultAccelerationY; 1015 private decimal defaultAccelerationZ; 1016 private decimal defaultAngularRateX; 1017 private decimal defaultAngularRateY; 1018 private decimal defaultAngularRateZ; 1019 1020 private struct SampleReadingRegisterInfo 1021 { 1022 public LSM6DSO_Vector3DSample.Axes Axis; 1023 public string NameSuffix; 1024 public bool UpperByte; 1025 } 1026 1027 private enum AccelerationFullScaleSelection : ushort 1028 { 1029 Mode0_2G = 0, 1030 Mode1_16G_2G = 1, 1031 Mode2_4G = 2, 1032 Mode3_8G = 3 1033 } 1034 1035 private enum AngularRateFullScaleSelection : ushort 1036 { 1037 Mode0_250DPS = 0, 1038 Mode1_500DPS = 1, 1039 Mode2_1000DPS = 2, 1040 Mode3_2000DPS = 3 1041 } 1042 1043 // Write == 0 and Read == 1 match the first transmission's byte R/W bit value. 1044 private enum CommandTypes 1045 { 1046 Write = 0, 1047 Read = 1, 1048 None, 1049 } 1050 1051 private enum DataRates : ushort 1052 { 1053 Disabled = 0x0, 1054 _12_5Hz = 0x1, 1055 _26Hz = 0x2, 1056 _52Hz = 0x3, 1057 _104Hz = 0x4, 1058 _208Hz = 0x5, 1059 _416Hz = 0x6, 1060 _833Hz = 0x7, 1061 _1_66kHz = 0x8, 1062 _3_33kHz = 0x9, 1063 _6_66kHz = 0xA, 1064 _1_6HzOr6_5HzOr12_5Hz = 0xB, 1065 } 1066 1067 private enum FifoModes : byte 1068 { 1069 Bypass = 0b000, 1070 StoppingFifo = 0b001, 1071 ContinuousToFifo = 0b011, 1072 BypassToContinuous = 0b100, 1073 Continuous = 0b110, 1074 BypassToFifo = 0b111, 1075 } 1076 1077 private enum FifoTag : byte 1078 { 1079 UNKNOWN = 0x00, // Undefined in the datasheet. 1080 GyroscopeNC = 0x01, 1081 AccelerometerNC = 0x02, 1082 Temperature = 0x03, 1083 Timestamp = 0x04, 1084 ConfigurationChange = 0x05, 1085 AccelerometerNC_T_2 = 0x06, 1086 AccelerometerNC_T_1 = 0x07, 1087 Accelerometer2xC = 0x08, 1088 Accelerometer3xC = 0x09, 1089 GyroscopeNC_T_2 = 0x0A, 1090 GyroscopeNC_T_1 = 0x0B, 1091 Gyroscope2xC = 0x0C, 1092 Gyroscope3xC = 0x0D, 1093 SensorHubSlave0 = 0x0E, 1094 SensorHubSlave1 = 0x0F, 1095 SensorHubSlave2 = 0x10, 1096 SensorHubSlave3 = 0x11, 1097 StepCounter = 0x12, 1098 SensorHubNack = 0x19 1099 } 1100 1101 private enum Registers : byte 1102 { 1103 RegistersAccessConfiguration = 0x01, 1104 PinControl = 0x02, 1105 // Reserved: 0x03 - 0x06 1106 FifoControl1 = 0x07, 1107 FifoControl2 = 0x08, 1108 FifoControl3 = 0x09, 1109 FifoControl4 = 0x0A, 1110 CounterBatchDataRate1 = 0x0B, 1111 CounterBatchDataRate2 = 0x0C, 1112 Int1PinControl = 0x0D, 1113 Int2PinControl = 0x0E, 1114 WhoAmI = 0x0F, 1115 Control1_Accelerometer = 0x10, 1116 Control2_Gyroscope = 0x11, 1117 Control3 = 0x12, 1118 Control4 = 0x13, 1119 Control5 = 0x14, 1120 Control6 = 0x15, 1121 Control7_Gyroscope = 0x16, 1122 Control8_Accelerometer = 0x17, 1123 Control9_Accelerometer = 0x18, 1124 Control10 = 0x19, 1125 AllInterruptSource = 0x1A, 1126 WakeUpInterruptSource = 0x1B, 1127 TapInterruptSource = 0x1C, 1128 PositionInterruptSource = 0x1D, 1129 Status = 0x1E, // Works differently when read by the auxiliary SPI. 1130 // Reserved: 0x1F 1131 TemperatureLow = 0x20, 1132 TemperatureHigh = 0x21, 1133 GyroscopeXLow = 0x22, 1134 GyroscopeXHigh = 0x23, 1135 GyroscopeYLow = 0x24, 1136 GyroscopeYHigh = 0x25, 1137 GyroscopeZLow = 0x26, 1138 GyroscopeZHigh = 0x27, 1139 AccelerometerXLow = 0x28, 1140 AccelerometerXHigh = 0x29, 1141 AccelerometerYLow = 0x2A, 1142 AccelerometerYHigh = 0x2B, 1143 AccelerometerZLow = 0x2C, 1144 AccelerometerZHigh = 0x2D, 1145 // Reserved: 0x2E - 0x34 1146 EmbeddedFunctionInterruptStatus = 0x35, 1147 FiniteStateMachineInterruptStatusA = 0x36, 1148 FiniteStateMachineInterruptStatusB = 0x37, 1149 // Reserved: 0x38 1150 SensorHubInterruptStatus = 0x39, 1151 FifoStatus1 = 0x3A, 1152 FifoStatus2 = 0x3B, 1153 // Reserved: 0x3C - 0x3F 1154 Timestamp0 = 0x40, 1155 Timestamp1 = 0x41, 1156 Timestamp2 = 0x42, 1157 Timestamp3 = 0x43, 1158 // Reserved: 0x44 - 0x55 1159 TapConfiguration0 = 0x56, 1160 TapConfiguration1 = 0x57, 1161 TapConfiguration2 = 0x58, 1162 TapThreshold6D = 0x59, 1163 IntervalDuration2 = 0x5A, 1164 WakeUpThreshold = 0x5B, 1165 WakeUpDuration = 0x5C, 1166 FreeFallDuration = 0x5D, 1167 Int1FunctionRouting = 0x5E, 1168 Int2FunctionRouting = 0x5F, 1169 // Reserved: 0x60 - 0x61 1170 I3CBusAvailable = 0x62, 1171 InternalFrequencyFine = 0x63, 1172 // Reserved: 0x64 - 0x6E 1173 OISInterrupt = 0x6F, 1174 OISControl1 = 0x70, 1175 OISControl2 = 0x71, 1176 OISControl3 = 0x72, 1177 AccelerometerUserOffsetX = 0x73, 1178 AccelerometerUserOffsetY = 0x74, 1179 AccelerometerUserOffsetZ = 0x75, 1180 // Reserved: 0x76 - 0x77 1181 FifoOutputTag = 0x78, 1182 FifoOutputXLow = 0x79, 1183 FifoOutputXHigh = 0x7A, 1184 FifoOutputYLow = 0x7B, 1185 FifoOutputYHigh = 0x7C, 1186 FifoOutputZLow = 0x7D, 1187 FifoOutputZHigh = 0x7E, 1188 } 1189 } 1190 } 1191