Реализация интерфейса SPI с помощью библиотеки CH341DLL на VB.NET
автор:
aave
Для работы нам понадобится:
- отладочная плата с микросхемой CH341A;
- датчик BMP280;
- макетная плата (breadboard);
- соединительные провода.
1Работа с микросхемой CH341в режиме SPI
Напомню, ранее мы уже рассматривали микросхему CH341, её возможности, а также отладочную плату с микросхемой CH341A. Также мы написали базовый класс для общих методов работы с данными микросхемами и общий (базовый) для последовательных режимов класс. Поэтому дублировать эту часть кода не буду. Приведу лишь то, что относится непосредственно к режиму SPI.
Класс для работы с микросхемами CH341 в режиме SPI (разворачивается)
Imports System.Runtime.InteropServices
Namespace Ch341
''' <summary>
''' Работа с CH341 в последовательном режиме SPI.
''' </summary>
Public Class SpiMaster
Inherits Serial
#Region "CTOR"
''' <summary>
''' Открывает устройство в режиме SPI.
''' </summary>
''' <param name="index">Индекс устройства в системе, начиная с 0.</param>
''' <param name="isExclusive">Включить ли эксклюзивное использование чипа.</param>
''' <param name="isDoubleInOut">Число входов-выходов SPI, 0 = single-input single-out (D5 out / D7 in), 1 = double-in and double-out (D4,D5 out / D6,D7 in)</param>
''' <param name="isMsbFirst">MSB первым (стандартный режим).</param>
''' <param name="wires">Число проводников режима SPI.</param>
Public Sub New(index As Integer, Optional isExclusive As Boolean = False, Optional wires As WiresCountEnum = WiresCountEnum.Wires4, Optional isMsbFirst As Boolean = True, Optional isDoubleInOut As Boolean = False)
MyBase.New(index, isExclusive)
Me.WiresCount = wires
SetInOutCount(isMsbFirst, isDoubleInOut)
End Sub
#End Region '/CTOR
#Region "ENUMS"
''' <summary>
''' Число проводников режима SPI.
''' </summary>
Public Property WiresCount As WiresCountEnum = WiresCountEnum.Wires4
''' <summary>
''' Число проводов, используемых в SPI.
''' </summary>
Public Enum WiresCountEnum As Integer
''' <summary>
''' 3-проводной интерфейс SPI: тактовая частота на выводе DCK2/SCL, данные на DIO/SDA (квази-двунаправленный порт), выбор чипа D0/D1/D2.
''' Скорость примерно 51 кслов/с.
''' </summary>
Wires3
''' <summary>
''' 4-проводной интерфейс SPI: тактовая частота на выводе DCK/D3, выходные данные DOUT/D5, входные данные DIN/D7, выбор чипа D0/D1/D2.
''' Скорость примерно 68 кбайт/с.
''' </summary>
Wires4
''' <summary>
''' 5-проводной интерфейс SPI: Тактовая частота на выводе DCK/D3, выходные данные DOUT/D5 и DOUT2/D4, вхродные DIN/D7 b DIN2/D6, выбор чипа D0/D1/D2.
''' Скорость примерно 30 кбайт*2/с.
''' </summary>
Wires5
End Enum
#End Region '/ENUMS
''' <summary>
''' Задаёт число входов-выходов SPI, 0 = single-input single-out (D5 out / D7 in), 1 = double-in and double-out (D4,D5 out / D6,D7 in).
''' </summary>
''' <param name="isMsbFirst"></param>
''' <param name="isDoubleInOut"></param>
Public Sub SetInOutCount(ByVal isMsbFirst As Boolean, ByVal isDoubleInOut As Boolean)
Dim res As Boolean = False
Dim mode As Integer = 0
If isDoubleInOut Then
mode = 3
End If
If isMsbFirst Then
mode = mode Or &H80
End If
res = CH341SetStream(DeviceIndex, mode)
If res Then
Return
End If
Throw New Exception("Ошибка выставления режима SPI.")
End Sub
#Region "ЧТЕНИЕ И ЗАПИСЬ"
''' <summary>
''' Реализует обмен по 3-проводному интефрейсу SPI.
''' Тактовая частота на выводе DCK2/SCL, данные на DIO/SDA (квази-двунаправленный ввод-вывод), выбор чипа D0/D1/D2, скорость примерно 51 кслов/с.
''' </summary>
''' <param name="iChipSelect">Выбор ведомого:
''' бит 7 = 0 - выбор чипа игнорируется, бит 7 = 1 - данные валидны;
''' биты 1..0 = 00/01/10 соответственно активируют D0/D1/D2 низким уровнем.</param>
''' <param name="iLength">Число байтов для передачи.</param>
''' <param name="ioBuffer">Буфер для записи в DIO, и сюда же сохраняются прочитанные с DIO данные.</param>
<DllImport(DLL_PATH, SetLastError:=True, CallingConvention:=CallingConvention.StdCall)>
Private Shared Function CH341StreamSPI3(ByVal iIndex As Integer, ByVal iChipSelect As Integer, ByVal iLength As Integer, ByVal ioBuffer As Byte()) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
''' <summary>
''' Реализует обмен по 4-проводному интефрейсу SPI.
''' Тактовая частота на выводе DCK/D3, выходные данные на DOUT/D5, входные данные на DIN/D7, выбор чипа D0/D1/D2, скорость примерно 68 кбайт/с.
''' </summary>
''' <param name="iChipSelect">Выбор ведомого:
''' бит 7 = 0 - выбор чипа игнорируется, бит 7 = 1 - данные валидны;
''' биты 1..0 = 00/01/10 соответственно активируют D0/D1/D2 низким уровнем.</param>
''' <param name="iLength">Число байтов для передачи.</param>
''' <param name="ioBuffer">Буфер для записи в DOUT, и сюда же сохраняются прочитанные с DIN данные.</param>
<DllImport(DLL_PATH, SetLastError:=True, CallingConvention:=CallingConvention.StdCall)>
Private Shared Function CH341StreamSPI4(ByVal iIndex As Integer, ByVal iChipSelect As Integer, ByVal iLength As Integer, ByVal ioBuffer As Byte()) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
''' <summary>
''' Реализует обмен по 5-проводному интефрейсу SPI.
''' Тактовая частота на выводе DCK/D3, выходные данные на DOUT/D5 и DOUT2/D4, входные на DIN/D7 и DIN2/D6, выбор чипа D0/D1/D2, скорость примерно 30*2 кбайт/с.
''' </summary>
''' <param name="iChipSelect">Выбор ведомого:
''' бит 7 = 0, вывод CS игнорируется, бит 7 = 1 данные валидны;
''' биты 1..0 = 00/01/10 соответственно активируют D0/D1/D2 низким уровнем.</param>
''' <param name="iLength">Число байтов для передачи.</param>
''' <param name="ioBuffer">Буфер для записи в DOUT, и сюда же сохраняются прочитанные с DIN данные.</param>
''' <param name="ioBuffer2">Буфер для записи в DOUT2, и сюда же сохраняются прочитанные с DIN2 данные.</param>
<DllImport(DLL_PATH, SetLastError:=True, CallingConvention:=CallingConvention.StdCall)>
Private Shared Function CH341StreamSPI5(ByVal iIndex As Integer, ByVal iChipSelect As Integer, ByVal iLength As Integer, ByVal ioBuffer As Byte(), ByVal ioBuffer2 As Byte()) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
''' <summary>
''' Записывает заданный массив и возвращает считанный массив по SPI.
''' </summary>
''' <param name="writeBuffer">Буфер для записи.</param>
''' <param name="readLength">Число байтов для чтения.</param>
Public Function StreamSpi(ByVal writeBuffer As Byte(), ByVal readLength As Integer) As Byte()
'TODO "Забил гвоздями" вывод CS. Надо реализовать возможность управлять им.
Dim res As Boolean = False
Dim writeLength As Integer = writeBuffer.Length
ReDim Preserve writeBuffer(writeLength + readLength - 1)
Select Case WiresCount
Case WiresCountEnum.Wires3
res = CH341StreamSPI3(DeviceIndex, &H80, writeBuffer.Length, writeBuffer)
Case WiresCountEnum.Wires4
res = CH341StreamSPI4(DeviceIndex, &H80, writeBuffer.Length, writeBuffer)
Case WiresCountEnum.Wires5
Dim readBuffer2(readLength - 1) As Byte
res = CH341StreamSPI5(DeviceIndex, &H80, writeBuffer.Length, writeBuffer, readBuffer2)
End Select
If res Then
Return writeBuffer.Skip(writeLength).Take(readLength).ToArray()
End If
Throw New Exception("Ошибка чтения или записи массива байтов по SPI.")
End Function
<DllImport(DLL_PATH, SetLastError:=True, CallingConvention:=CallingConvention.StdCall)>
Private Shared Function CH341BitStreamSPI(ByVal iIndex As Integer, ByVal iLength As Integer, ByVal ioBuffer As Byte()) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
''' <summary>
''' Управляет битовой передачей по SPI в 4- и 5-проводном режимах.
''' Тактовая частота на выводе DCK/D3, по умолчанию LOW, DOUT/D5 и DOUT2/D4 - выходы по срезу, DIN/D7 и DIN2/D6 по спаду. */
''' One byte in ioBuffer corresponds to D7-D0 pin, bit 5 is output to DOUT, bit 4 is output to DOUT2, bit 2..0 is output to D2-D0, bit 7 is input from DIN, bit 6 input from DIN2, bit 3 data ignored */
''' </summary>
''' <param name="writeBuffer"></param>
''' <param name="bitsToWrite">Число битов, которые следует передать, до 896 за раз. Не рекомендуется передавать более 256.</param>
''' <param name="readLength"></param>
<Obsolete("Before calling this API, CH341Set_D5_D0 should be called to set the I/O direction of the D5-D0 pin of CH341 and set the default level of the pin")>
Public Function BitSreamSpi(ByVal writeBuffer As Byte(), ByVal bitsToWrite As Integer, ByVal readLength As Integer) As Byte()
If (readLength > writeBuffer.Length) Then
ReDim Preserve writeBuffer(readLength - 1)
End If
Dim res As Boolean = CH341BitStreamSPI(DeviceIndex, bitsToWrite, writeBuffer)
If res Then
Return writeBuffer
End If
Throw New Exception("Ошибка чтения или записи массива битов по SPI.")
End Function
#End Region '/чтение и запись
End Class
End Namespace
Полную и самую последнюю версию кода для работы с микросхемой CH341 можно скачать с репозитория на GitHub, где я буду его обновлять при появлении доработок/коррекции ошибок/добавлении новых возможностей.
2Использование CH341 в режиме SPI на примере работы с датчиком BMP280
Давайте протестируем работоспособность нашего кода с библиотекой CH341.dll на реальном «железе». Для этого нам нужно какое-либо ведомое устройство. У меня под рукой имеется уже многократно использованный нами в предыдущих статьях сенсор давления и температуры BMP280. Его-то мы и возьмём как первого подопытного.
Подключим датчик к отладочной плате по стандартной для SPI схеме. То есть, SCL датчика подключаем к SCK платы, SDA – к MOSI, SDO – к MISO, CSB – к CSO, VCC – к VDD, GND – к GND. Должно получиться что-то вроде этого:

Также обратите внимание, что перемычка режима работы (зелёная на фото) должна находиться в режиме IIC/SPI, а не UART.
Приведу пример использования класса для работы с нашей dll-кой. Этот кусок кода читает 1 байт из регистра ID датчика BMP280. Как мы знаем, в этом регистре должно храниться постоянное значение идентификатора, равное 0x58.
Пример использования класса для работы с микросхемами CH341 в режиме SPI (разворачивается)
Imports Ch341
''' <summary>
''' Тестирование библиотеки CH341DLL.DLL.
''' </summary>
Module Module1
Sub Main()
Console.ForegroundColor = ConsoleColor.Cyan
Console.WriteLine("Работа с библиотекой CH341.dll")
Console.WriteLine()
Do
Try
CheckBmp280_spi()
Catch ex As Exception
Console.ForegroundColor = ConsoleColor.Red
Console.WriteLine(ex.Message)
Finally
Console.ForegroundColor = ConsoleColor.DarkGray
Console.WriteLine("Нажмите любую клавишу чтобы повторить...")
Console.ReadKey(True)
Console.WriteLine()
End Try
Loop
End Sub
''' <summary>
''' Чтение регистра ID датчика BMP280 по интерфейсу SPI.
''' </summary>
Private Sub CheckBmp280_spi()
Console.ForegroundColor = ConsoleColor.Yellow
Console.WriteLine("Чтение регистра ID датчика BMP280 по интерфейсу SPI: ")
Console.ForegroundColor = ConsoleColor.Gray
Dim ch341 As New SpiMaster(0, True)
Dim buf As Byte() = ch341.StreamSpi({&HD0}, 1)
For Each b As Byte In buf
Console.Write(b.ToString("X2"))
Console.Write(" ")
Next
Console.WriteLine()
ch341.CloseDevice()
Console.WriteLine()
End Sub
End Module
Вот как будет выглядеть вывод нашей консольной программы, читающей и выводящей значение жёстко «зашитого» в ПЗУ идентификатора датчика BMP280.

Интересно посмотреть, как это будет выглядеть на уровне передаваемых сигналов. А тут, впрочем, всё стандартно для интерфейса SPI: линия ENABLE активирует обмен, линия CLOCK – для синхронизации, на линии MOSI данные от ведущего устройства (в данном случае микросхемы CH341), на линии MISO – данные от ведомого (датчика BMP280).

Первым байтом по линии ведущего устройства MOSI передаём команду 0xD0. На неё приходит один байт ответа от ведомого (по линии MISO) с постоянным значением идентификатора датчика BMP280 – 0x58.
