context: #region References
  
  using System;
  using System.Globalization;
  using System.Runtime.InteropServices;
  using Raspberry.IO.GeneralPurpose;
  using Raspberry.Timers;
  
  #endregion
  
  namespace Raspberry.IO.InterIntegratedCircuit
  {
      /// <summary>
      /// Represents a driver for I2C devices.
      /// </summary>
      public class I2cDriver : IDisposable
      {
          #region Fields
  
          private readonly object driverLock = new object();
  
          private readonly ProcessorPin sdaPin;
          private readonly ProcessorPin sclPin;
          private readonly bool wasSdaPinSet;
          private readonly bool wasSclPinSet;
  
          private readonly IntPtr gpioAddress;
          private readonly IntPtr bscAddress;
  
          private int currentDeviceAddress;
          private int waitInterval;
  
          #endregion
  
          #region Instance Management
  
          /// <summary>
          /// Initializes a new instance of the <see cref="I2cDriver"/> class.
          /// </summary>
          /// <param name="sdaPin">The SDA pin.</param>
          /// <param name="sclPin">The SCL pin.</param>
          public I2cDriver(ProcessorPin sdaPin, ProcessorPin sclPin)
          {
              this.sdaPin = sdaPin;
              this.sclPin = sclPin;
  
              var bscBase = GetBscBase(sdaPin, sclPin);
  
              var memoryFile = Interop.open("/dev/mem", Interop.O_RDWR + Interop.O_SYNC);
              try
              {
                  gpioAddress = Interop.mmap(
                      IntPtr.Zero, 
                      Interop.BCM2835_BLOCK_SIZE, 
                      Interop.PROT_READ | Interop.PROT_WRITE, 
                      Interop.MAP_SHARED, 
                      memoryFile, 
                      GetProcessorGpioAddress(Board.Current.Processor));
                  
                  bscAddress = Interop.mmap(
                      IntPtr.Zero, 
                      Interop.BCM2835_BLOCK_SIZE, 
                      Interop.PROT_READ | Interop.PROT_WRITE, 
                      Interop.MAP_SHARED, 
                      memoryFile, 
                      bscBase);
              }
              finally
              {
                  Interop.close(memoryFile);
              }
  
              if (bscAddress == (IntPtr) Interop.MAP_FAILED)
                  throw new InvalidOperationException("Unable to access device memory");
  
              // Set the I2C pins to the Alt 0 function to enable I2C access on them
              // remembers if the values were actually changed to clear them or not upon dispose
              wasSdaPinSet = SetPinMode((uint)(int)sdaPin, Interop.BCM2835_GPIO_FSEL_ALT0); // SDA
              wasSclPinSet = SetPinMode((uint) (int) sclPin, Interop.BCM2835_GPIO_FSEL_ALT0); // SCL
  
              // Read the clock divider register
              var dividerAddress = bscAddress + (int) Interop.BCM2835_BSC_DIV;
              var divider = (ushort) SafeReadUInt32(dividerAddress);
              waitInterval = GetWaitInterval(divider);
  
              var addressAddress = bscAddress + (int) Interop.BCM2835_BSC_A;
              SafeWriteUInt32(addressAddress, (uint) currentDeviceAddress);
          }
  
          /// <summary>
          /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
          /// </summary>
          public void Dispose()
          {
              // Set all the I2C/BSC1 pins back to original values if changed
              if (wasSdaPinSet)
              {
                  SetPinMode((uint)(int)sdaPin, Interop.BCM2835_GPIO_FSEL_INPT); // SDA
              }
              if (wasSclPinSet)
              {
                  SetPinMode((uint)(int)sclPin, Interop.BCM2835_GPIO_FSEL_INPT); // SCL
              }
  
              Interop.munmap(gpioAddress, Interop.BCM2835_BLOCK_SIZE);
              Interop.munmap(bscAddress, Interop.BCM2835_BLOCK_SIZE);
          }
  
          #endregion
  
          #region Properties
  
          /// <summary>
          /// Gets or sets the clock divider.
          /// </summary>
          /// <value>
          /// The clock divider.
          /// </value>
          public int ClockDivider
          {
              get
              {
                  var dividerAddress = bscAddress + (int) Interop.BCM2835_BSC_DIV;
                  return (ushort) SafeReadUInt32(dividerAddress);
              }
              set
              {
                  var dividerAddress = bscAddress + (int) Interop.BCM2835_BSC_DIV;
                  SafeWriteUInt32(dividerAddress, (uint) value);
  
                  var actualDivider = (ushort) SafeReadUInt32(dividerAddress);
                  waitInterval = GetWaitInterval(actualDivider);
              }
          }
  
          #endregion
  
          #region Methods
  
          /// <summary>
          /// Connects the specified device address.
          /// </summary>
          /// <param name="deviceAddress">The device address.</param>
          /// <returns>The device connection</returns>
          public I2cDeviceConnection Connect(int deviceAddress)
          {
              return new I2cDeviceConnection(this, deviceAddress);
          }
  
          #endregion
  
          #region Internal Methods
  
          /// <summary>
          /// Executes the specified transaction.
          /// </summary>
          /// <param name="deviceAddress">The address of the device.</param>
          /// <param name="transaction">The transaction.</param>
          internal void Execute(int deviceAddress, I2cTransaction transaction)
          {
              lock (driverLock)
              {
                  var control = bscAddress + (int)Interop.BCM2835_BSC_C;
  
                  foreach (I2cAction action in transaction.Actions)
                  {
                      if (action is I2cWriteAction)
                      {
                          Write(deviceAddress, action.Buffer);
                      }
                      else if (action is I2cReadAction)
                      {
                          Read(deviceAddress, action.Buffer);
                      }
                      else
                      {
                          throw new InvalidOperationException("Only read and write transactions are allowed.");
                      }
                  }
  
                  WriteUInt32Mask(control, Interop.BCM2835_BSC_S_DONE, Interop.BCM2835_BSC_S_DONE);
              }
          }
  
          #endregion
  
          #region Private Helpers
  
          private void Write(int deviceAddress, byte[] buffer)
          {
              this.EnsureDeviceAddress(deviceAddress);
  
              var len = (uint)buffer.Length;
  
              var dlen = this.bscAddress + (int)Interop.BCM2835_BSC_DLEN;
              var fifo = this.bscAddress + (int)Interop.BCM2835_BSC_FIFO;
              var status = this.bscAddress + (int)Interop.BCM2835_BSC_S;
              var control = this.bscAddress + (int)Interop.BCM2835_BSC_C;
  
              var remaining = len;
              var i = 0;
  
              // Clear FIFO
              WriteUInt32Mask(control, Interop.BCM2835_BSC_C_CLEAR_1, Interop.BCM2835_BSC_C_CLEAR_1);
  
              // Clear Status
              WriteUInt32(status, Interop.BCM2835_BSC_S_CLKT | Interop.BCM2835_BSC_S_ERR | Interop.BCM2835_BSC_S_DONE);
  
              // Set Data Length
              WriteUInt32(dlen, len);
  
              while (remaining != 0 && i < Interop.BCM2835_BSC_FIFO_SIZE)
              {
                  WriteUInt32(fifo, buffer[i]);
                  i++;
                  remaining--;
              }
  
              // Enable device and start transfer
              WriteUInt32(control, Interop.BCM2835_BSC_C_I2CEN | Interop.BCM2835_BSC_C_ST);
  
  
gt:             while ((ReadUInt32(status) & Interop.BCM2835_BSC_S_DONE) == 0)