未验证 提交 a0cf397b 编写于 作者: K Krzysztof Wicher 提交者: GitHub

Add simple SoftwareSpi implementation (no timing control) and extract it from MCP3008 (#607)

* Add simple SoftwareSpi implementation (no timing control) and extract it from MCP3008

* remove tests project

* address feedback

* Update src/devices/SoftwareSpi/SoftwareSpi.cs

* Add space between params

* periods in the end of summary and params
上级 d32ad7a6
......@@ -10,19 +10,7 @@ namespace Iot.Device.Mcp3008
{
public class Mcp3008 : IDisposable
{
private readonly SpiDevice _spiDevice;
private GpioController _controller;
private readonly CommunicationProtocol _protocol;
private readonly int _clk;
private readonly int _miso;
private readonly int _mosi;
private readonly int _cs;
private enum CommunicationProtocol
{
Gpio,
Spi
}
private SpiDevice _spiDevice;
public enum InputConfiguration
{
......@@ -33,31 +21,12 @@ namespace Iot.Device.Mcp3008
public Mcp3008(SpiDevice spiDevice)
{
_spiDevice = spiDevice;
_protocol = CommunicationProtocol.Spi;
}
public Mcp3008(int clk, int miso, int mosi, int cs)
{
_controller = new GpioController();
_clk = clk;
_miso = miso;
_mosi = mosi;
_cs = cs;
_controller.OpenPin(_clk, PinMode.Output);
_controller.OpenPin(_miso, PinMode.Input);
_controller.OpenPin(_mosi, PinMode.Output);
_controller.OpenPin(_cs, PinMode.Output);
_protocol = CommunicationProtocol.Gpio;
}
public void Dispose()
{
if (_controller != null)
{
_controller.Dispose();
_controller = null;
}
_spiDevice?.Dispose();
_spiDevice = null;
}
public int Read(int channel, InputConfiguration inputConfiguration = InputConfiguration.SingleEnded)
......@@ -67,14 +36,7 @@ namespace Iot.Device.Mcp3008
throw new ArgumentException("ADC channel must be within 0-7 range.");
}
if (_protocol == CommunicationProtocol.Spi)
{
return ReadSpi(channel, inputConfiguration);
}
else
{
return ReadGpio(channel, inputConfiguration);
}
return ReadSpi(channel, inputConfiguration);
}
private static byte GetConfigurationBits(int channel, InputConfiguration inputConfiguration)
......@@ -83,59 +45,12 @@ namespace Iot.Device.Mcp3008
if (inputConfiguration == InputConfiguration.Differential)
{
configurationBits &= 0b1011_1111; // Clear mode bit.
configurationBits &= 0b1011_1111; // Clear mode bit.
}
return (byte)configurationBits;
}
// Ported: https://gist.github.com/ladyada/3151375
private int ReadGpio(int channel, InputConfiguration inputConfiguration)
{
while (true)
{
int result = 0;
byte command = GetConfigurationBits(channel, inputConfiguration);
_controller.Write(_cs, PinValue.High);
_controller.Write(_clk, PinValue.Low);
_controller.Write(_cs, PinValue.Low);
for (int cnt = 0; cnt < 5; cnt++)
{
if ((command & 0b1000_0000) > 0)
{
_controller.Write(_mosi, PinValue.High);
}
else
{
_controller.Write(_mosi, PinValue.Low);
}
command <<= 1;
_controller.Write(_clk, PinValue.High);
_controller.Write(_clk, PinValue.Low);
}
for (int cnt = 0; cnt < 12; cnt++)
{
_controller.Write(_clk, PinValue.High);
_controller.Write(_clk, PinValue.Low);
result <<= 1;
if (_controller.Read(_miso) == PinValue.High)
{
result |= 0b0000_0001;
}
}
_controller.Write(_cs, PinValue.High);
result >>= 1;
return result;
}
}
// Ported: https://github.com/adafruit/Adafruit_Python_MCP3008/blob/master/Adafruit_MCP3008/MCP3008.py
private int ReadSpi(int channel, InputConfiguration inputConfiguration)
{
......
......@@ -5,6 +5,7 @@
using System;
using System.Device.Spi;
using System.Threading;
using Iot.Device.Spi;
namespace Iot.Device.Samples
{
......@@ -12,50 +13,22 @@ namespace Iot.Device.Samples
{
static void Main(string[] args)
{
Console.WriteLine("Hello Mcp3008!");
// This sample implements two different ways of accessing the MCP3008.
// The SPI option is enabled in the sample by default, but you can switch
// to the GPIO bit-banging option by switching which one is commented out.
// The sample uses local functions to make it easier to switch between
// the two implementations.
// SPI implementation
Mcp3008.Mcp3008 GetMcp3008WithSpi()
{
Console.WriteLine("Using SPI protocol.");
var connection = new SpiConnectionSettings(0, 0)
{
ClockFrequency = 1000000,
Mode = SpiMode.Mode0
};
var spi = SpiDevice.Create(connection);
var mcp3008 = new Mcp3008.Mcp3008(spi);
return mcp3008;
}
// GPIO (via bit banging) implementation
Mcp3008.Mcp3008 GetMcp3008WithGpio()
var hardwareSpiSettings = new SpiConnectionSettings(0, 0)
{
Console.WriteLine("Using GPIO pins.");
var mcp3008 = new Mcp3008.Mcp3008(18, 23, 24, 25);
return mcp3008;
}
Mcp3008.Mcp3008 mcp = GetMcp3008WithSpi();
// Uncomment next line to use GPIO instead.
// Mcp3008 mcp = GetMcp3008WithGpio();
ClockFrequency = 1000000
};
using (mcp)
using (SpiDevice spi = new SoftwareSpi(clk: 6, miso: 23, mosi: 5, cs: 24))
// For hardware implementation replace it with following
// using (SpiDevice spi = SpiDevice.Create(hardwareSpiSettings))
using (Mcp3008.Mcp3008 mcp = new Mcp3008.Mcp3008(spi))
{
while (true)
{
double value = mcp.Read(0, Mcp3008.Mcp3008.InputConfiguration.SingleEnded);
value = value / 10.24;
value = Math.Round(value);
Console.WriteLine(value);
Console.WriteLine($"{value}%");
Thread.Sleep(500);
}
}
......
......@@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Mcp3008.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\..\SoftwareSpi\SoftwareSpi.csproj" />
</ItemGroup>
</Project>
......@@ -8,14 +8,26 @@ The Raspberry Pi cannot read analog values directly so relies on an analog to di
The Raspberry Pi has support for SPI. You need to [enable the SPI interface on the Raspberry Pi](https://www.raspberrypi-spy.co.uk/2014/08/enabling-the-spi-interface-on-the-raspberry-pi/) since it is not enabled by default.
You can use the following code to [access the MCP3008 via SPI](Mcp3008.Sample.cs#L17-L22):
You can use the following code to [access the MCP3008 via hardware SPI](Mcp3008.Sample.cs):
```csharp
var connection = new SpiConnectionSettings(0,0);
connection.ClockFrequency = 1000000;
connection.Mode = SpiMode.Mode0;
var spi = SpiDevice.Create(connection);
var mcp = new Mcp3008(spi);
var hardwareSpiSettings = new SpiConnectionSettings(0, 0)
{
ClockFrequency = 1000000
};
using (SpiDevice spi = SpiDevice.Create(hardwareSpiSettings))
using (Mcp3008.Mcp3008 mcp = new Mcp3008.Mcp3008(spi))
{
while (true)
{
double value = mcp.Read(0, Mcp3008.Mcp3008.InputConfiguration.SingleEnded);
value = value / 10.24;
value = Math.Round(value);
Console.WriteLine($"{value}%");
Thread.Sleep(500);
}
}
```
The following pin layout can be used (also shown in a [fritzing diagram](rpi-trimpot-spi.fzz)):
......@@ -36,11 +48,21 @@ The following pin layout can be used (also shown in a [fritzing diagram](rpi-tri
You can also access the MCP3008 via GPIO pins, implementing SPI manually. This method is referred to as [bit-banging](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface#Example_of_bit-banging_the_master_protocol).
You can use the following code to [access the MCP3008 via GPIO](Mcp3008.Sample.cs#L29-L30):
You can use the following code to [access the MCP3008 via GPIO](Mcp3008.Sample.cs):
```csharp
GpioController controller = new GpioController(PinNumberingScheme.Gpio);
var mcp = new Mcp3008(controller, 18, 23, 24, 25);
using (SpiDevice spi = new SoftwareSpi(clk: 18, miso: 23, mosi: 24, cs: 25))
using (Mcp3008.Mcp3008 mcp = new Mcp3008.Mcp3008(spi))
{
while (true)
{
double value = mcp.Read(0, Mcp3008.Mcp3008.InputConfiguration.SingleEnded);
value = value / 10.24;
value = Math.Round(value);
Console.WriteLine($"{value}%");
Thread.Sleep(500);
}
}
```
The following pin layout can be used (also shown in a [fritzing diagram](rpi-trimpot-gpio.fzz)):
......
# Software implementation of SPI
Usage is very simple:
```csharp
using (SpiDevice spi = new SoftwareSpi(clk: 6, miso: 23, mosi: 5, cs: 24))
{
// do stuff over SPI
}
```
For more advanced usage it optionally also takes GpioController and SpiConnectionSettings.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Device.Gpio;
using System.Device.Spi;
namespace Iot.Device.Spi
{
public class SoftwareSpi : SpiDevice
{
private GpioController _controller;
private readonly int _clk;
private readonly int _miso;
private readonly int _mosi;
private readonly int _cs;
private readonly SpiConnectionSettings _settings;
/// <summary>
/// Software implementation of the SPI.
/// </summary>
/// <param name="clk">Clock pin.</param>
/// <param name="miso">Master Input Slave Output pin.</param>
/// <param name="mosi">Master Output Slave Input pin.</param>
/// <param name="cs">Chip select pin (or negated chip select).</param>
/// <param name="settings">Settings of the SPI connection.</param>
/// <param name="controller">GPIO controller used for pins.</param>
public SoftwareSpi(int clk, int miso, int mosi, int cs, SpiConnectionSettings settings = null, GpioController controller = null)
{
_controller = controller ?? new GpioController();
_settings = settings ?? new SpiConnectionSettings(-1, -1);
_clk = clk;
_miso = miso;
_mosi = mosi;
_cs = cs;
_controller.OpenPin(_clk, PinMode.Output);
_controller.OpenPin(_miso, PinMode.Input);
_controller.OpenPin(_mosi, PinMode.Output);
_controller.OpenPin(_cs, PinMode.Output);
// aka. CPOL - tells us which state of the clock means idle (false means 'low' or 'ground' or '0')
bool idle = ((int)_settings.Mode & 0b10) == 0b10;
// aka. CPHA - tells us when read/write is 'captured'
bool onPulseEnd = ((int)_settings.Mode & 1) == 1;
_controller.Write(_cs, !(bool)_settings.ChipSelectLineActiveState);
_controller.Write(_clk, idle);
// TODO: To respect ClockFrequency we need to inject the right delays here
// and have some very accurate way to measure time.
// Ideally we should verify the output with an oscilloscope.
// pulse start pulse end
// v v
// ---------
// | |
// ------ ------
// idle !idle idle
// note: vertical axis represents idle or !idle (pulse) state and is orthogonal
// to low/high related with GPIO - that part is defined by SPI Mode which
// tells us what GPIO state represents idle and also if the measurement happens
// on pulse start or end
if (onPulseEnd)
{
// When we capture onPulseEnd then we need to start pulse before we send the data
// and then trigger the capture on exit
_bitTransfer = new ScopeData(
enter: () => {
_controller.Write(_clk, !idle);
},
exit: () => {
controller.Write(_clk, idle);
});
}
else
{
_bitTransfer = new ScopeData(
exit: () => {
_controller.Write(_clk, !idle);
_controller.Write(_clk, idle);
});
}
_chipSelect = new ScopeData(
enter: () => {
_controller.Write(_cs, _settings.ChipSelectLineActiveState);
},
exit: () => {
_controller.Write(_cs, !(bool)_settings.ChipSelectLineActiveState);
});
}
/// <inheritdoc />
public override SpiConnectionSettings ConnectionSettings => _settings;
/// <inheritdoc />
public override void TransferFullDuplex(ReadOnlySpan<byte> dataToWrite, Span<byte> dataToRead)
{
if (dataToRead.Length != dataToRead.Length)
{
throw new ArgumentException(nameof(dataToWrite));
}
int bitLen = _settings.DataBitLength;
int lastBit = bitLen - 1;
using (StartChipSelect())
{
for (int i = 0; i < dataToRead.Length; i++)
{
byte readByte = 0;
for (int j = 0; j < bitLen; j++)
{
using (StartBitTransfer())
{
int bit = _settings.DataFlow == DataFlow.MsbFirst ? lastBit - j : j;
bool bitToWrite = ((dataToWrite[i] >> bit) & 1) == 1;
if (ReadWriteBit(bitToWrite))
{
readByte |= (byte)(1 << bit);
}
}
}
dataToRead[i] = readByte;
}
}
}
private bool ReadWriteBit(bool bitToWrite)
{
_controller.Write(_mosi, bitToWrite);
return (bool)_controller.Read(_miso);
}
/// <inheritdoc />
public override void Read(Span<byte> data)
{
Span<byte> dataToWrite = stackalloc byte[data.Length];
TransferFullDuplex(dataToWrite, data);
}
/// <inheritdoc />
public override void Write(ReadOnlySpan<byte> data)
{
Span<byte> dataToRead = stackalloc byte[data.Length];
TransferFullDuplex(data, dataToRead);
}
/// <inheritdoc />
public override void WriteByte(byte data)
{
Span<byte> outData = stackalloc byte[1];
outData[0] = data;
Write(outData);
}
/// <inheritdoc />
public override byte ReadByte()
{
Span<byte> data = stackalloc byte[1];
Read(data);
return data[0];
}
/// <inheritdoc />
public override void Dispose(bool disposing)
{
_controller?.Dispose();
_controller = null;
base.Dispose();
}
private ScopeData _bitTransfer;
private ScopeData _chipSelect;
private Scope StartBitTransfer() => new Scope(_bitTransfer);
private Scope StartChipSelect() => new Scope(_chipSelect);
private class ScopeData
{
Action _enter, _exit;
public ScopeData(Action enter = null, Action exit = null)
{
_enter = enter;
_exit = exit;
}
public void Enter()
{
if (_enter != null)
{
_enter();
}
}
public void Exit()
{
if (_exit != null)
{
_exit();
}
}
}
private struct Scope : IDisposable
{
ScopeData _data;
public Scope(ScopeData data)
{
_data = data;
data.Enter();
}
public void Dispose()
{
_data.Exit();
}
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<!--Disabling default items so samples source won't get build by the main library-->
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="*.cs" />
<None Include="README.md" />
<PackageReference Include="System.Device.Gpio" Version="$(SystemDeviceGpioPackageVersion)" />
</ItemGroup>
</Project>
# Software SPI sample
(See MCP3008 sample)[../../Mcp3008/samples/README.md]
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册