// 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;
using System.Device.Gpio;
using System.Device.Gpio.Drivers;
using System.Device.I2c;
using System.Device.Pwm;
using System.Device.Spi;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using UnitsNet;
namespace Iot.Device.Board
{
///
/// Raspberry Pi specific board implementation.
/// Contains all the knowledge about which pins can be used for what purpose.
///
public class RaspberryPiBoard : GenericBoard
{
private readonly object _initLock = new object();
private readonly string[] _possibleI2cActivations = new string[]
{
"dtparam=i2c=on",
"dtparam=i2c_arm=on",
"dtparam=i2c_baudrate=",
"dtparam=i2c_arm_baudrate=",
// Activating only 1 I2C with the default pins:
"dtoverlay=i2c0",
"dtoverlay=i2c1",
"dtoverlay=i2c3",
"dtoverlay=i2c4",
"dtoverlay=i2c5",
"dtoverlay=i2c6",
// We will use those ones to ensure valid options.
"dtoverlay=i2c0,pins_0_1",
"dtoverlay=i2c0,pins_28_29",
"dtoverlay=i2c0,pins_44_45",
"dtoverlay=i2c0,pins_46_47",
"dtoverlay=i2c1,pins_2_3",
"dtoverlay=i2c1,pins_44_45",
"dtoverlay=i2c3,pins_2_3",
"dtoverlay=i2c3,pins_4_5",
"dtoverlay=i2c4,pins_6_7",
"dtoverlay=i2c4,pins_8_9",
"dtoverlay=i2c5,pins_10_11",
"dtoverlay=i2c5,pins_12_13",
"dtoverlay=i2c6,pins_0_1",
"dtoverlay=i2c6,pins_22_23",
};
private readonly string[] _possibleSpiActivations = new string[]
{
"dtparam=spi=on",
"dtoverlay=spi0-0cs",
"dtoverlay=spi0-1cs",
"dtoverlay=spi0-2cs",
"dtoverlay=spi1-1cs",
"dtoverlay=spi1-2cs",
"dtoverlay=spi1-3cs",
"dtoverlay=spi2-1cs",
"dtoverlay=spi2-2cs",
"dtoverlay=spi2-3cs",
"dtoverlay=spi3-1cs",
"dtoverlay=spi3-2cs",
"dtoverlay=spi4-1cs",
"dtoverlay=spi4-2cs",
"dtoverlay=spi5-1cs",
"dtoverlay=spi5-2cs",
"dtoverlay=spi6-1cs",
"dtoverlay=spi6-2cs",
};
private readonly string[] _possiblePwmActivations = new string[]
{
"dtoverlay=pwm",
"dtoverlay=pwm-2chan",
};
private ManagedGpioController? _managedGpioController;
private RaspberryPi3Driver? _raspberryPi3Driver;
private bool _initialized;
private List _activateI2c = new List();
private List _activateSpi = new List();
private List _activatePwm = new List();
///
/// Creates an instance of a Rasperry Pi board.
///
public RaspberryPiBoard()
{
// TODO: Ideally detect board type, so that invalid combinations can be prevented (i.e. I2C bus 2 on Raspi 3)
PinCount = 28;
_initialized = false;
}
///
/// Number of pins of the board
///
public int PinCount
{
get;
protected set;
}
///
/// Gets or sets the path to the configuration file for Raspberry PI.
///
public string ConfigurationFile { get; set; } = "/boot/config.txt";
///
protected override GpioDriver? TryCreateBestGpioDriver()
{
return new RaspberryPi3Driver();
}
///
/// Initializes this instance
///
/// The current hardware could not be identified as a valid Raspberry Pi type
protected override void Initialize()
{
if (_initialized)
{
return;
}
lock (_initLock)
{
if (_initialized)
{
return;
}
// Needs to be a raspi 3 driver here (either unix or windows)
GpioDriver? driver = TryCreateBestGpioDriver();
if (driver == null)
{
throw new NotSupportedException("Could not initialize the RaspberryPi GPIO driver");
}
_managedGpioController = new ManagedGpioController(this, driver);
_raspberryPi3Driver = driver as RaspberryPi3Driver;
PinCount = _managedGpioController.PinCount;
_initialized = true;
}
base.Initialize();
}
///
public override int[] GetDefaultPinAssignmentForI2c(int busId)
{
int scl;
int sda;
switch (busId)
{
case 0:
{
// Bus 0 is the one on logical pins 0 and 1. According to the docs, it should not
// be used by application software and instead is reserved for HATs, but if you don't have one, it is free for other purposes
sda = 0;
scl = 1;
break;
}
case 1:
{
// This is the bus commonly used by application software.
sda = 2;
scl = 3;
break;
}
case 2:
{
throw new NotSupportedException("I2C Bus number 2 doesn't exist");
}
case 3:
{
sda = 4;
scl = 5;
break;
}
case 4:
{
sda = 6;
scl = 7;
break;
}
case 5:
{
sda = 10;
scl = 11;
break;
}
case 6:
sda = 22;
scl = 23;
break;
default:
throw new NotSupportedException($"I2C bus {busId} does not exist.");
}
return new int[]
{
// Return in the default scheme of the board
sda,
scl
};
}
///
public override int[] GetDefaultPinAssignmentForSpi(SpiConnectionSettings connectionSettings)
{
int cs = connectionSettings.ChipSelectLine;
// If hardware CS is used, the CS selection must be 0 or 1, since only that is supported
// (except for bus 1, which has 3 pre-defined CS lines)
if ((cs >= 2 || cs < -1) && !((connectionSettings.BusId == 1) && cs == 2))
{
throw new ArgumentOutOfRangeException(nameof(connectionSettings), "Chip select line must be 0 or 1");
}
List pins = new List();
switch (connectionSettings.BusId)
{
case 0:
pins.Add(9);
pins.Add(10);
pins.Add(11);
if (cs == 0)
{
pins.Add(8);
}
else if (cs == 1)
{
pins.Add(7);
}
break;
case 1:
pins.Add(19);
pins.Add(20);
pins.Add(21);
if (cs == 0)
{
pins.Add(18);
}
else if (cs == 1)
{
pins.Add(17);
}
else if (cs == 2)
{
pins.Add(16);
}
break;
case 2:
// Available only on the compute module
// GPIO40 / PWM0 / SPI2 MISO / UART1 TX
// GPIO41 / PWM1 / SPI2 MOSI / UART1 RX
// GPIO42 / GPCLK1 / SPI2 SCLK / UART1 RTS
// GPIO43 / GPCLK2 / SPI2 CE0 / UART1 CTS
// GPIO44 / GPCLK1 / I2C0 SDA / I2C1 SDA / SPI2 CE1
// GPIO45 / PWM1 / I2C0 SCL / I2C1 SCL / SPI2 CE2
pins.Add(40);
pins.Add(41);
pins.Add(42);
if (cs == 0)
{
pins.Add(43);
}
else if (cs == 1)
{
pins.Add(44);
}
else if (cs == 2)
{
pins.Add(45);
}
break;
case 3:
pins.Add(1);
pins.Add(2);
pins.Add(3);
if (cs == 0)
{
pins.Add(0);
}
else if (cs == 1)
{
pins.Add(24);
}
break;
case 4:
pins.Add(5);
pins.Add(6);
pins.Add(7);
if (cs == 0)
{
pins.Add(4);
}
else if (cs == 1)
{
pins.Add(25);
}
break;
case 5:
pins.Add(13);
pins.Add(14);
pins.Add(15);
if (cs == 0)
{
pins.Add(12);
}
else if (cs == 1)
{
pins.Add(26);
}
break;
case 6:
pins.Add(19);
pins.Add(20);
pins.Add(21);
if (cs == 0)
{
pins.Add(18);
}
else if (cs == 1)
{
pins.Add(27);
}
break;
default:
throw new NotSupportedException($"No bus number {connectionSettings.BusId}");
}
return pins.ToArray();
}
///
/// Gets the board-specific hardware mode for a particular pin and pin usage (i.e. the different ALTn modes on the raspberry pi)
///
/// Pin number to use
/// Requested usage
/// Pin numbering scheme for the pin provided (logical or physical)
/// Optional bus argument, for SPI and I2C pins
///
/// A member of describing the mode the pin is in.
private RaspberryPi3Driver.AltMode GetHardwareModeForPinUsage(int pinNumber, PinUsage usage, PinNumberingScheme pinNumberingScheme = PinNumberingScheme.Logical, int bus = 0)
{
if (pinNumber >= PinCount)
{
throw new InvalidOperationException($"Invalid pin number {pinNumber}");
}
if (usage == PinUsage.Gpio)
{
// all pins support GPIO
return RaspberryPi3Driver.AltMode.Input;
}
if (usage == PinUsage.I2c)
{
// The Pi4 has a big number of pins that can become I2C pins
switch (pinNumber)
{
// Busses 0 and 1 run on Alt0
case 0:
case 1:
case 2:
case 3:
return RaspberryPi3Driver.AltMode.Alt0;
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
return RaspberryPi3Driver.AltMode.Alt5;
case 22:
case 23:
return RaspberryPi3Driver.AltMode.Alt5;
}
throw new NotSupportedException($"No I2C support on Pin {pinNumber}.");
}
if (usage == PinUsage.Pwm)
{
if (pinNumber == 12 || pinNumber == 13)
{
return RaspberryPi3Driver.AltMode.Alt0;
}
if (pinNumber == 18 || pinNumber == 19)
{
return RaspberryPi3Driver.AltMode.Alt5;
}
throw new NotSupportedException($"No Pwm support on Pin {pinNumber}.");
}
if (usage == PinUsage.Spi)
{
switch (pinNumber)
{
case 7: // Pin 7 can be assigned to either SPI0 or SPI4
return bus == 0 ? RaspberryPi3Driver.AltMode.Alt0 : RaspberryPi3Driver.AltMode.Alt3;
case 8:
case 9:
case 10:
case 11:
return RaspberryPi3Driver.AltMode.Alt0;
case 0:
case 1:
case 2:
case 3:
return RaspberryPi3Driver.AltMode.Alt3;
case 4:
case 5:
case 6:
return RaspberryPi3Driver.AltMode.Alt3;
case 12:
case 13:
case 14:
case 15:
return RaspberryPi3Driver.AltMode.Alt3;
case 16:
case 17:
return RaspberryPi3Driver.AltMode.Alt4;
case 18:
case 19:
case 20:
case 21:
return bus == 6 ? RaspberryPi3Driver.AltMode.Alt3 : RaspberryPi3Driver.AltMode.Alt4;
case 24:
case 25:
case 26:
case 27:
return RaspberryPi3Driver.AltMode.Alt5;
}
throw new NotSupportedException($"No SPI support on Pin {pinNumber}.");
}
if (usage == PinUsage.Uart)
{
switch (pinNumber)
{
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
return RaspberryPi3Driver.AltMode.Alt4;
case 14:
case 15:
if (bus == 0)
{
return RaspberryPi3Driver.AltMode.Alt0;
}
else if (bus == 5)
{
return RaspberryPi3Driver.AltMode.Alt4;
}
else if (bus == 1)
{
return RaspberryPi3Driver.AltMode.Alt5;
}
break;
case 16:
case 17:
return (bus == 0) ? RaspberryPi3Driver.AltMode.Alt3 : RaspberryPi3Driver.AltMode.Alt5;
}
throw new NotSupportedException($"No Uart support on Pin {pinNumber}.");
}
throw new NotSupportedException($"There are no known pins for {usage}.");
}
///
public override int GetDefaultI2cBusNumber()
{
return 1;
}
///
public override int GetDefaultPinAssignmentForPwm(int chip, int channel)
{
// The default assignment is 12 & 13, but 18 and 19 is supported as well
if (chip == 0 && channel == 0)
{
return 12;
}
if (chip == 0 && channel == 1)
{
return 13;
}
throw new NotSupportedException($"No such PWM Channel: Chip {chip} channel {channel}.");
}
///
/// Switches a pin to a certain alternate mode. (ALTn mode)
///
/// The pin number in the logical scheme
/// The desired usage
protected override void ActivatePinMode(int pinNumber, PinUsage usage)
{
if (_managedGpioController == null)
{
throw new InvalidOperationException("Board not initialized");
}
if (_raspberryPi3Driver == null || !_raspberryPi3Driver.AlternatePinModeSettingSupported)
{
throw new NotSupportedException("Alternate pin mode setting not supported by driver");
}
var modeToSet = GetHardwareModeForPinUsage(pinNumber, usage, PinNumberingScheme.Logical);
if (modeToSet != RaspberryPi3Driver.AltMode.Unknown)
{
_raspberryPi3Driver.SetAlternatePinMode(pinNumber, modeToSet);
}
base.ActivatePinMode(pinNumber, usage);
}
///
/// Gets the current alternate pin mode. (ALTn mode)
///
/// Pin number, in the logical scheme
/// The current pin usage
/// This also works for closed pins, but then uses a bit of heuristics to get the correct mode
public override PinUsage DetermineCurrentPinUsage(int pinNumber)
{
if (_managedGpioController == null)
{
throw new InvalidOperationException("Board not initialized");
}
PinUsage cached = base.DetermineCurrentPinUsage(pinNumber);
if (cached != PinUsage.Unknown)
{
return cached;
}
if (_raspberryPi3Driver == null || !_raspberryPi3Driver.AlternatePinModeSettingSupported)
{
throw new NotSupportedException("Alternate pin mode setting not supported by driver");
}
var pinMode = _raspberryPi3Driver.GetAlternatePinMode(pinNumber);
if (pinMode == RaspberryPi3Driver.AltMode.Input || pinMode == RaspberryPi3Driver.AltMode.Output)
{
return PinUsage.Gpio;
}
// Do some heuristics: If the given pin number can be used for I2C with the same Alt mode, we can assume that's what it
// it set to.
var possibleAltMode = GetHardwareModeForPinUsage(pinNumber, PinUsage.I2c, DefaultPinNumberingScheme);
if (possibleAltMode == pinMode)
{
return PinUsage.I2c;
}
possibleAltMode = GetHardwareModeForPinUsage(pinNumber, PinUsage.Spi, DefaultPinNumberingScheme);
if (possibleAltMode == pinMode)
{
return PinUsage.Spi;
}
possibleAltMode = GetHardwareModeForPinUsage(pinNumber, PinUsage.Pwm, DefaultPinNumberingScheme);
if (possibleAltMode == pinMode)
{
return PinUsage.Pwm;
}
return PinUsage.Unknown;
}
///
/// Checks if the I2C overlay is activated in the configuraztion file.
///
/// True if it is.
public bool IsI2cActivated()
{
_activateI2c.Clear();
// We are checking possible activation from here: https://github.com/dotnet/iot/blob/main/Documentation/raspi-i2c.md
var config = File.ReadAllText(ConfigurationFile).Split('\n').Where(m => !m.Trim().StartsWith("#")).Where(m => m.Length > 1);
foreach (var possibleActivation in _possibleI2cActivations)
{
// We need the actual line as the configuration can be more complex than the list.
var choices = config.Where(m => m.Trim().StartsWith(possibleActivation));
if (choices.Any())
{
_activateI2c.Add(choices.First().Replace("\r", string.Empty));
}
}
return _activateI2c.Any();
}
///
/// Gets the overlay pin assignment for I2C.
///
/// Bus Id.
/// The set of pins for the given I2C bus.
public int[] GetOverlayPinAssignmentForI2c(int busId)
{
int scl = -1;
int sda = -1;
if (!IsI2cActivated())
{
return new int[0];
}
// Checks if there is an overlay because it does override the default configuration
var dtoverlay = _activateI2c.Where(m => m.StartsWith($"dtoverlay=i2c{busId},")).FirstOrDefault();
if (string.IsNullOrEmpty(dtoverlay))
{
// Give another try without the parameters
dtoverlay = _activateI2c.Where(m => m.StartsWith($"dtoverlay=i2c{busId}")).FirstOrDefault();
}
if (!string.IsNullOrEmpty(dtoverlay))
{
// we have an overlay, extract the pins and check them
// dtoverlay=i2c1,pins_2_3
if (dtoverlay.IndexOf('_') > 0)
{
var pins = dtoverlay.Substring(dtoverlay.IndexOf('_') + 1).Split('_');
sda = int.Parse(pins[0]);
scl = int.Parse(pins[1]);
// Rebuild the chain and check those are valid options
var rebuilt = $"dtoverlay=i2c{busId},pins_{sda}_{scl}";
if (!_possibleI2cActivations.Contains(rebuilt))
{
throw new InvalidOperationException($"Invalid I2C overlay configuration: {dtoverlay}");
}
}
else
{
return GetDefaultPinAssignmentForI2c(busId);
}
}
else
{
var dtparam = _activateI2c.Where(m => m.StartsWith($"dtparam"));
if (dtparam.Any())
{
// We're using the default one
return GetDefaultPinAssignmentForI2c(busId);
}
}
return new int[]
{
// Return in the default scheme of the board
sda,
scl
};
}
///
/// Checks if the SPI overlay is activated in the configuraztion file.
///
/// True if it is.
public bool IsSpiActivated()
{
_activateI2c.Clear();
// We are checking possible activation from here: https://github.com/dotnet/iot/blob/main/Documentation/raspi-i2c.md
var config = File.ReadAllText(ConfigurationFile).Split('\n').Where(m => !m.Trim().StartsWith("#")).Where(m => m.Length > 1);
foreach (var possibleActivation in _possibleSpiActivations)
{
// We need the actual line as the configuration can be more complex than the list.
var choices = config.Where(m => m.Trim().StartsWith(possibleActivation));
if (choices.Any())
{
// Remove the \r as introduced on the Windows machine for the tests.
_activateSpi.Add(choices.First().Replace("\r", string.Empty));
}
}
return _activateSpi.Any();
}
///
/// Gets the overlay pin assignment for Spi.
///
/// Connection settings to check.
/// The set of pins for the given SPI bus. If no miso, it will be marked as -1.
public int[] GetOverlayPinAssignmentForSpi(SpiConnectionSettings connectionSettings)
{
int[] pins = new int[0];
if (!IsSpiActivated())
{
return pins;
}
// Checks if there is an overlay because it does override the default configuration
var dtoverlay = _activateSpi.Where(m => m.StartsWith($"dtoverlay=spi{connectionSettings.BusId}")).FirstOrDefault();
if (!string.IsNullOrEmpty(dtoverlay))
{
// Overlays look like this:
// dtoverlay=spi4-2cs,cs1_pin=17,cs1_spidev=disabled
// dtoverlay=spi0-2cs,cs0_pin=27,cs1_pin=22
// Can be as well: dtoverlay=spi0-2cs
// Or: dtoverlay=spi0-1cs,nomiso
// First find number of Chip Select
var numberCs = int.Parse(dtoverlay.Substring(dtoverlay.IndexOf('-') + 1, 1));
if ((connectionSettings.ChipSelectLine >= numberCs))
{
throw new ArgumentException($"SPI {connectionSettings.BusId} is setup with {numberCs} chip select and you ask for number {connectionSettings.ChipSelectLine}.");
}
// This does returns
// MISO, MISO, CLK and CS
pins = GetDefaultPinAssignmentForSpi(connectionSettings);
// If SPI Bus is 0, check if we have no_miso
if ((connectionSettings.BusId == 0) && dtoverlay.Contains("no_miso"))
{
pins[0] = -1;
}
// Now let's check the CS if it's the default value or not
string csDefined = $"cs{connectionSettings.ChipSelectLine}_pin=";
if (dtoverlay.Contains(csDefined))
{
// In case it's part of a string, we keep only the first one
var pinValueStr = dtoverlay.Substring(dtoverlay.IndexOf(csDefined) + csDefined.Length).Split(',')[0].Trim();
pins[3] = int.Parse(pinValueStr);
}
return pins;
}
else
{
var dtparam = _activateSpi.Where(m => m.StartsWith($"dtparam"));
if (dtparam.Any())
{
// We're using the default one
return GetDefaultPinAssignmentForSpi(connectionSettings);
}
}
return pins;
}
///
/// Checks if the I2C overlay is activated in the configuraztion file.
///
/// True if it is.
public bool IsPwmActivated()
{
_activatePwm.Clear();
// We are checking possible activation from here: https://github.com/dotnet/iot/blob/main/Documentation/raspi-pwm.md
var config = File.ReadAllText(ConfigurationFile).Split('\n').Where(m => !m.Trim().StartsWith("#")).Where(m => m.Length > 1);
foreach (var possibleActivation in _possiblePwmActivations)
{
// We need the actual line as the configuration can be more complex than the list.
var choices = config.Where(m => m.Trim().StartsWith(possibleActivation));
if (choices.Any())
{
_activatePwm.Add(choices.First().Replace("\r", string.Empty));
}
}
return _activatePwm.Any();
}
///
/// Gets the overlay pin assignment for Pwm.
///
/// The PWM channel.
/// The set of pins for the given Pwm bus on chipn 0 as only one supported.
public int GetOverlayPinAssignmentForPwm(int pwmChannel)
{
int[] validPwm0 = new int[] { 12, 18, 40, 52 };
int[] validPwm1 = new int[] { 13, 19, 41, 45, 53 };
int pin = -1;
if (!IsPwmActivated())
{
return pin;
}
// Checks if there is an overlay because it does override the default configuration
var dtoverlay = _activatePwm.Where(m => m.StartsWith($"dtoverlay=pwm")).FirstOrDefault();
if (!string.IsNullOrEmpty(dtoverlay))
{
// we have an overlay, extract the pins and check them
// dtoverlay=pwm,pin=19,func=2
// dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4
// Single or dual?
if ((pwmChannel == 1) && dtoverlay.Contains("2chan"))
{
// Do we have an overlay with pins?
var possibleDtoverlay = _activatePwm.Where(m => m.StartsWith($"dtoverlay=pwm-2chan,")).FirstOrDefault();
if (!string.IsNullOrEmpty(possibleDtoverlay))
{
dtoverlay = possibleDtoverlay;
}
if (dtoverlay.IndexOf("pin2=") > 0)
{
// 2 channels
pin = int.Parse(dtoverlay.Substring(dtoverlay.IndexOf("pin2=") + 5).Split(',')[0]);
// Is it a potential valid pin?
if (!validPwm1.Contains(pin))
{
throw new ArgumentException($"PWM{pwmChannel} pin2 is not a valid pin.");
}
}
else
{
// We'll use the default one
pin = 19;
}
}
else if (pwmChannel == 0)
{
// Do we have an overlay with pins?
var possibleDtoverlay = _activatePwm.Where(m => m.StartsWith($"dtoverlay=pwm,")).FirstOrDefault();
if (!string.IsNullOrEmpty(possibleDtoverlay))
{
dtoverlay = possibleDtoverlay;
}
if (dtoverlay.IndexOf("pin=") > 0)
{
pin = int.Parse(dtoverlay.Substring(dtoverlay.IndexOf("pin=") + 4).Split(',')[0]);
// Is it a potential valid pin?
if (!validPwm0.Contains(pin))
{
throw new ArgumentException($"PWM{pwmChannel} pin is not a valid pin.");
}
}
else
{
// We'll use the default one
pin = 18;
}
}
}
return pin;
}
///
protected override void Dispose(bool disposing)
{
if (disposing)
{
_managedGpioController?.Dispose();
_managedGpioController = null;
}
base.Dispose(disposing);
}
///
public override ComponentInformation QueryComponentInformation()
{
ComponentInformation self = base.QueryComponentInformation();
var ret = self with
{
Description = $"Raspberry Pi with {PinCount} pins"
};
ret.Properties["PinCount"] = PinCount.ToString(CultureInfo.InvariantCulture);
return ret;
}
}
}