Рейтинг@Mail.ru

Реализация интерфейса 1-Wire на микросхемах FTDI

Print Friendly, PDF & Email
С интерфейсом 1-Wire мы сталкиваемся ежедневно: он применяется для чтения ключей-«таблеток» от домофона. Микросхемы фирмы FTDI (FT2232, FT4232 и другие) не поддерживают реализацию интерфейса 1-Wire «из коробки». Но программно можно реализовать его самостоятельно. Чем и займёмся в статье.

1Описание однопроводного последовательного интерфейса 1-Wire

Интерфейс 1-Wire (или OneWire) распространён весьма широко и применяется нами в повседневной жизни каждый день. Например, он используется для считывания ключа от домофона. Также для чтения показаний всевозможных датчиков. Его основные преимущества в том, что для информационного обмена используется всего один провод, который также может являться и питающим для подключённых устройств. На одной шине могут находиться несколько ведомых устройств одновременно.

Линия 1-Wire подтянута к высокому уровню (чаще всего от 3 до 5 В). Весь обмен инициирует ведущее устройство (master). Существуют 4 типа сигналов: сброс, чтение, запись бита "0" и запись бита "1".

Для сигнала сброса ведущий притягивает линию к нулю на определённый промежуток времени, а затем отпускает. Линия снова подтягивается к высокому уровню. Если на линии присутствует ведомый, то после короткой паузы он отмечает своё присутствие также притягиванием линии к нулю, а затем отпускает. Если на линии кроме мастера нет других устройств, то линия так и останется в логической единице.

Временная диаграмма сброса ведущим на линии 1-Wire
Временная диаграмма сброса ведущим на линии 1-Wire

Для записи одного бита ведущий притягивает линию к нулю: для записи "1" на короткий промежуток времени, для записи "0" – на более длинный.

Временная диаграмма записи бита "1" ведущим на линии 1-Wire
Временная диаграмма записи бита "1" ведущим на линии 1-Wire
Временная диаграмма записи бита "0" ведущим на линии 1-Wire
Временная диаграмма записи бита "0" ведущим на линии 1-Wire

Для чтения одного бита ведущий притягивает линию к нулю и отпускает. Если ведомый хочет передать "0", то он продолжает удерживать линию в нуле, а если хочет передать "1", то отпускает линию, и она подтягивается к высокому уровню.

Временная диаграмма чтения бита от ведомого на линии 1-Wire
Временная диаграмма чтения бита от ведомого на линии 1-Wire

Будем работать с библиотекой libMPSSE.dll, которую предоставляет разработчик микросхем для управления ими через ПК. Но прежде чем переходить к программному коду, следует пояснить суть идеи, т.к. микросхемы FTDI аппаратно не поддерживают режим 1-Wire. Указанная библиотека также не имеет программной реализации 1-Wire. Она позволяет управлять устройством лишь в режимах SPI, I2C, а также выводами GPIO общего назначения. Казалось бы, можно использовать выводы GPIO микросхемы. Но интерфейс 1-Wire крайне чувствителен к временным параметрам сигнала на шине. А операционная система Windows, под которой мы работаем, не является системой реального времени и не даст нам возможности соблюсти необходимые рамки. Поэтому необходимо использовать аппаратные возможности чипов FTDI.

Будем использовать микросхему FTDI в режиме SPI. Импульсы будем генерировать на выводе MOSI (на порте 1 микросхемы это ножка ADBUS1), и одновременно с этим снимать значения с вывода MISO (на порте 1 это ножка ADBUS2). Линии MOSI и MISO между собой будут соединены через резистор 480 Ом. Будем ориентироваться на то, чтобы 1 байт SPI (8 тактовых импульсов) генерировался столько времени, чтобы ведущий успел передать команду (запись бита или сигнал сброса) и прочитать ответ на неё. При этом длительность импульса будем регулировать значением байта, который будем передавать.

В интерфейсе 1-Wire существуют 2 скоростных режима: стандартный (standard, скорость 16.3 кбит/сек) и ускоренный (overdrive).

На иллюстрации ниже показаны и обозначены буквами от A до J участки сигнала 1-Wire в разных командах. А в таблице приведены средние значения длительности этих участков для обоих скоростных режимов.

Тайминги интерфейса 1-Wire
Тайминги интерфейса 1-Wire
Тайминги интерфейса 1-Wire
ОбозначениеДлительность в режиме standard, мксДлительность в режиме overdrive, мкс
A61
B647.5
C607.5
D102.5
E91
F557
G02.5
H48070
I708.5
J41040

Более детальное описание интерфейса с наглядными иллюстрациями и примерами можно почитать здесь на русском языке, а также на сайте создателя интерфейса 1-Wire.

Для примера на рисунке ниже показаны две ситуации. В первой мы посылаем сигнал сброса и не получаем ответ ведомого, а во второй – получаем ответ. Оба раза мы передаём по линии MOSI SPI число 0x0F: 4 такта низкий уровень и 4 такта высокий. В первом случае, когда на линии нет ведомого устройства, с линии MISO SPI будет прочитано 0x0F (число не изменилось), а во втором – 0x07. Во второй ситуации ведомый опустил линию примерно на 1 такт SPI (линия на 5-ом такте также в низком уровне), из-за чего мы прочитали не то, что записали. Длительность рассчитана так, чтобы соответствовать требованиям интерфейса 1-Wire. Длительность низкого уровня за эти 4 такта равняется необходимым 480 мкс в стандартном скоростном режиме или 1 мкс в ускоренном. Для этого тактовая частота SPI должна быть около 8200 бит/сек в стандартном режиме и 56300 бит/сек в ускоренном режиме.

Пример использования FTDI в режиме SPI для эмуляции интерфейса 1-Wire
Пример использования FTDI в режиме SPI для эмуляции интерфейса 1-Wire

По аналогии с рассмотренным примером будут реализованы команды для записи и для чтения битов интерфейса 1-Wire.

2Программная реализация интерфейса 1-Wire на микросхемах FTDI и библиотеке libMPSSE.dll

В предыдущей статье мы довольно подробно рассматривали и микросхемы фирмы FTDI, и библиотеки для работы с ними (FTD2XX.dll, FTCSPI.dll и libMPSSE.dll), и отладочные платы, на которых можно самостоятельно «пощупать» данные микросхемы. Обсуждали установку драйверов и приводили ссылки на их скачивание.

Кроме того, в статье шёл разговор о реализации интерфейса I2C и был приведён программный код абстрактного базового класса MpsseBase для работы с устройствами MPSSE (Multi Protocol Synchronous Serial Engine – механизм, позволяющий микросхемам FTDI работать с различными последовательными интерфейсами). Указанный базовый класс будет использоваться и здесь, т.к. реализует основную функциональность, общую для всех последовательных режимов (SPI, I2C, 1-Wire).

Ещё нам понадобится код класса MpsseSpi, который приведён в статье.

Данные от ведущего устройства будем генерировать на выводе MOSI (ADBUS1 для порта 1 или BDBUS1 для порта 2) микросхемы FTDI, а принимать будем на выводе MISO (ADBUS2 для порта 1 или BDBUS2 для порта 2). Соединяться всё это будет почти так же, как в случае I2C, только между выводами MOSI и MISO следует поставить резистор сопротивлением около 480 Ом. Для примера показана схема соединения на первом порту микросхемы FT2232D (ножки ADBUS1 и ADBUS2).

Схема подключения FT2232D к линии 1-Wire и iButton
Схема подключения FT2232D к линии 1-Wire и iButton
Код класса для работы в режиме 1-Wire (разворачивается)
Imports System.Diagnostics
Imports System.Runtime.InteropServices
Imports Ftdi.MpsseSpi

Namespace Ftdi

    ''' <summary>
    ''' Режим 1-Wire.
    ''' </summary>
    Public Class MpsseOneWire
        Inherits MpsseBase 'Не будем наследовать данный класс MpsseOneWire от MpsseSpi; вместо этого включим объект класса MpsseSpi, чтобы можно было использовать его функциональность
        
        'Режимы скорости
        Public Enum TimingModes
            Standard
            Overdrive
        End Enum

#Region "КОНСТРУКТОР"

        Private ReadOnly Spi As MpsseSpi 

        ''' <summary>
        ''' Открывает устройство с индексом <paramref name="index"/> в режиме <paramref name="mode"/>.
        ''' </summary>
        Public Sub New(index As Integer, Optional mode As TimingModes = TimingModes.Standard)
            MyBase.New(index)
            Spi = New MpsseSpi(index)
            Me.Mode = mode
        End Sub

#End Region '/КОНСТРУКТОР

#Region "СВОЙСТВА"

        Public Overrides ReadOnly Property DeviceMode As DeviceWorkingMode
            Get
                Return DeviceWorkingMode.OneWire
            End Get
        End Property

        ''' <summary>
        ''' Скоростной режим.
        ''' </summary>
        Public Overridable Property Mode As TimingModes = TimingModes.Standard

        ''' <summary>
        ''' Открыто ли устройство.
        ''' </summary>
        Public Overrides ReadOnly Property IsOpened As Boolean
            Get
                If (Spi IsNot Nothing) Then
                    Return (Spi.IsOpened)
                End If
                Return False
            End Get
        End Property

#End Region '/СВОЙСТВА

#Region "ОТКРЫТИЕ, ЗАКРЫТИЕ, ИНФО"

        Public Overrides Function GetChannelInfo() As FT_DEVICE_LIST_INFO_NODE
            Return Spi.GetChannelInfo()
        End Function

        Public Overrides Sub OpenChannel()
            Spi.OpenChannel()
        End Sub

        Public Overrides Sub CloseChannel()
            Spi.CloseChannel()
        End Sub

#End Region '/ОТКРЫТИЕ, ЗАКРЫТИЕ, ИНФО

#Region "ЧТЕНИЕ И ЗАПИСЬ"

        ''' <summary>
        ''' Генерирует импульс сброса на линии и проверяет импульс присутствия от ведомого.
        ''' </summary>
        ''' <returns>
        ''' Если получен импульс присутствия от ведомого, возвращает True, в обратном случае - False.
        ''' </returns>
        Public Function Reset() As Boolean
            Dim c As New SpiConfig() With {
                .ClockRate = 8200, 'при такой частоте длительность импульса сброса H получается ~480 мкс
                .Pin = &HFFFFFFFFUI,
                .ConfigOptions = SPI_CONFIG_OPTION.MODE0
            }
            If (Mode = TimingModes.Overdrive) Then
                c.ClockRate = 56200 '=> импульс сброса H ~70 мкс
            End If
            Spi.InitChannel(c)
            Dim r As Byte() = Spi.ReadWriteSim({&H0F}, 8, SPI_TRANSFER_OPTIONS.SIZE_IN_BITS)
            Debug.WriteLine($"rst={r(0):X2}")
            Return (r(0) < &H0F) 'какой-то из битов младшей тетрады будет "0", что уменьшит число 0x0F.
        End Function

        ''' <summary>
        ''' Записывает массив байтов на линию.
        ''' </summary>
        Public Sub WriteBytes(bytes As IEnumerable(Of Byte))
            Dim c As New SpiConfig() With {
                .ClockRate = 110000, '=> "1": A=9us, B=63us; "0": C=63us, D=9us
                .Pin = &HFFFFFFFFUI,
                .ConfigOptions = SPI_CONFIG_OPTION.MODE0
            }
            If (Mode = TimingModes.Overdrive) Then
                c.ClockRate = 1000000 '=> "1": A=1us, B=7us; "0": C=7us, D=1us
            End If
            Spi.InitChannel(c)
            For Each b As Byte In bytes
                For i As Integer = 0 To 7
                    WriteBit(CBool(b And 1))
                    b >>= 1
                Next
            Next
        End Sub

        ''' <summary>
        ''' Читает массив байтов с линии.
        ''' </summary>
        ''' <param name="length"></param>
        Public Function ReadBytes(length As Integer) As Byte()
            Dim c As New SpiConfig() With {
               .ClockRate = 110000, '=> "1": A=9us, B=63us; "0": C=63us, D=9us
               .Pin = &HFFFFFFFFUI,
               .ConfigOptions = SPI_CONFIG_OPTION.MODE0
           }
            If (Mode = TimingModes.Overdrive) Then
                c.ClockRate = 1000000 '=> "1": A=1us, B=7us; "0": C=7us, D=1us
            End If
            Spi.InitChannel(c)
            Dim bytes As New List(Of Byte)
            For len As Integer = 0 To length - 1
                Dim b As Byte = 0
                For i As Integer = 0 To 7
                    b >>= 1 'сдвигаем, чтобы получить следующий бит
                    If ReadBit() Then 'если считанный бит=1, выставляем старший бит
                        b = CByte(b Or &H80)
                    End If
                Next
                bytes.Add(b)
            Next
            Return bytes.ToArray()
        End Function

        ''' <summary>
        ''' Записывает 1 бит на линию.
        ''' </summary>
        ''' <param name="bit">Значение бита.</param>
        ''' <remarks>
        ''' Перед записью бита нужно выставить параметры SPI.
        ''' Инициализация SPI проводится в методе <see cref="WriteBytes(IEnumerable(Of Byte))"/>.
        ''' </remarks>
        Private Sub WriteBit(bit As Boolean)
            If bit Then
                Dim r As Byte() = Spi.ReadWriteSim({&H7F}, 8, SPI_TRANSFER_OPTIONS.SIZE_IN_BITS) 'записываем бит "1"
            Else
                Dim r As Byte() = Spi.ReadWriteSim({&H1}, 8, SPI_TRANSFER_OPTIONS.SIZE_IN_BITS) 'записываем бит "0"
            End If
        End Sub

        ''' <summary>
        ''' Читает 1 бит с линии.
        ''' </summary>
        ''' <remarks>
        ''' Перед чтением бита нужно выставить параметры SPI.
        ''' Инициализация SPI проводится в методе <see cref="ReadBytes(Integer)"/>.
        ''' </remarks>
        Private Function ReadBit() As Boolean
            Const tx As Byte = &H7F
            Dim r As Byte() = Spi.ReadWriteSim({tx}, 8, SPI_TRANSFER_OPTIONS.SIZE_IN_BITS)
            Return (r(0) = tx) 'если данные на линии не изменились, значит ведомый хочет передать "1".
        End Function

#End Region '/ЧТЕНИЕ И ЗАПИСЬ

    End Class 

End Namespace

Чтобы использовать этот код, импортируем пространство имён, в котором находятся наши классы для работы с устройствами FTDI:

Imports Ftdi

Далее в коде создадим экземпляр устройства 1-Wire и назначим ему некоторые действия (сброс, запись и чтение). Например, запросим у ключа-«таблетки» (iButton) её идентификатор ID:

Using ow As New MpsseOneWire(0) 
    Do
        Dim presence as Boolean = ow.Reset()
        Console.WriteLine($"Presence = {presence}") 'сброс и опрос
        If presence Then 'если на линии есть ведомый
            Console.WriteLine("Key found: ")
            ow.WriteBytes({&H33}) 'запись команды чтения ID ключа, для подробностей см. datasheet
            Dim bytes as Byte() = ow.ReadBytes(8) 'чтение ID ключа 
            For Each b as Byte in bytes
                Console.Write(b.ToString("X2"))
            Next 
            Console.WriteLine("")
        Else
            Console.WriteLine("No key found...")
        End If
        Threading.Thread.Sleep(1000)
    Loop
End Using

Вот как выглядит макет, на котором всё это отлаживалось. Не удивляйтесь такому количеству проводов для однопроводного интерфейса :) Большая их часть не задействована.

Чтение ключа iButton по интерфейсу 1-Wire с помощью микросхемы FT2232H
Чтение ключа iButton по интерфейсу 1-Wire с помощью микросхемы FT2232H

Приведённого низкоуровневого программного кода достаточно для того чтобы реализовать протокол обмена с конкретными устройствами на шине 1-Wire. Например, теми же домофонными ключами iButton.

3Чтение показаний температуры датчика DS18B20по интерфейсу 1-Wire

Для примера прочитаем показания датчика температуры DS18B20 по интерфейсу 1-Wire с помощью микросхемы FT2232H и программы SPI via FTDI. С одним устройством на шине One-wire работать проще, т.к. можно не учитывать идентификационный номер устройства. Поэтому повесим на шину 1-Wire сразу два датчика, чтобы немного усложнить задачу.

Скачать техническое описание DS18B20, а вот здесь можно заказать DS18B20.

Чтение датчика температуры DS18B20 по интерфейсу 1-Wire с помощью микросхемы FT2232H
Чтение датчика температуры DS18B20 по интерфейсу 1-Wire с помощью микросхемы FT2232H

Для обмена с датчиком DS18B20 существует чёткая последовательность, которую нельзя нарушать (иначе он не ответит):

  • послать импульс сброса RESET;
  • послать команду ROM;
  • послать управляющую (функциональную) команду.

Команды ROM для большинства устройств 1-Wire стандартизированы. Это такие команды как поиск Search ROM (0xF0), чтение Read ROM (0x33), совпадение Match ROM (0x55), пропуск Skip ROM (0xCC). Команда поиска (search ROM) используется для поиска устройств на шине. Команда чтения (read ROM) для чтения уникального номера ROM устройства; её можно использовать, если на шине только одно устройство. В противном случае одновременно ответят все устройства, и данные будут искажены (объединены по логическому "И"). Команда совпадения (match ROM) используется, когда необходимо обратиться к конкретному устройству по его уникальному идентификатору (номеру ROM). Команда пропуска (skip ROM) используется, когда можно опустить адрес; это такие случаи, когда либо устройство одно на шине, либо это команда сразу для всех устройств.

Управляющие (функциональные) команды для каждого устройства могут быть уникальны, могут частично совпадать, а могут отсутствовать вовсе. Для чтения температуры нам понадобятся две команды датчика DS18B20: подготовка данных 0x44 и чтение оперативной памяти (которая здесь называется scratchpad) 0xBE.

Прежде всего после запуска программы SPI via FTDI и подключения к выбранному порту (A или B) нужно просканировать шину 1-wire. Для этого нажимаем кнопку со значком лупы. Программа найдёт на шине два датчика и добавит их серийные номера ROM в выпадающий список.

Чтение датчика температуры DS18B20 по интерфейсу 1-Wire с помощью микросхемы FT2232H
Чтение датчика температуры DS18B20 по интерфейсу 1-Wire с помощью микросхемы FT2232H

Теперь выбираем из выпадающего списка датчик, с которого мы хотим прочитать температуру. Нажимаем кнопку RESET, далее MATCH ROM, далее в поле «Команда» вводим 44 и нажимаем «Записать». Этим мы сообщили датчику, чтобы он измерил температуру и записал её в свои регистры.

Можно на этом шаге, кстати, вместо MATCH ROM нажать кнопку SKIP ROM. Таким образом мы пропустим обработку уникального идентификатора ROM, и оба датчика начнут измерение температуры. А уже следующей командой мы заберём данные каждого сенсора адресно.

Для чтения температуры нажимаем RESET, MATCH ROM, в разделе «Чтение» записываем команду BE и указываем, что хотим прочитать 9 байт, и нажимаем «Прочитать». В таблице прочитанных данных отбразятся 9 байтов из памяти выбранного датчика. В приведённом случае (см. иллюстрацию выше) это 7B 01 55 05 7F A5 A5 66 28. На самом деле, данные о температуре хранятся в первых двух байтах, так что можно было прочитать только два байта. А 9 байт – это полное содержимое оперативной памяти (scratchpad), структура которой приведена ниже:

Структура оперативной памяти (scratchpad) датчика температуры DS18B20
Структура оперативной памяти (scratchpad) датчика температуры DS18B20

Как видно, первые два байта в памяти – это закодированное значение температуры. Следующие два байта – пользовательские данные или установка температуры, при которой датчик перейдёт в «тревожный» режим. Пятый байт – регистр конфигурации; он определяет, с какой точностью (и, следовательно, как быстро) будет измеряться и выводиться температура. Далее идут не значащие 3 байта, а последний байт – контрольная сумма. Регистр конфигурации датчика DS18B20 в нашем случае содержит число 0x7F (в двоичном виде 0111_1111). Значит разрядность измерений температуры составляет 12 бит:

Структура регистра конфигурации датчика температуры DS18B20
Структура регистра конфигурации датчика температуры DS18B20

В таблице также представлено время, требуемое на одно измерение температуры при разной точности сенсора DS18B20.

Итак, мы прочитали содержимое памяти. Код температуры в нашем случае 7B 01, а разрядность измерений – 12 бит. Как же это число перевести в градусы Цельсия? Для этого посмотрим на формат регистра, в котором хранятся данные температуры. На рисунке S – это знак температуры ("0" означает положительную температуру, "1" – отрицательную), LS – младший байт, MS – старший байт. А биты с 0 по 10 хранят код температуры. Для 12-битового разрешения все биты с 0 по 10 являются значащими. Для 11-битового разрешения бит 0 является незначащим, для 10-битового – незначащими являются 0 и 1 биты, а для 9-разрядного биты 0, 1 и 2 – незначащие.

Формат регистра температуры датчика температуры DS18B20
Формат регистра температуры датчика температуры DS18B20.

Нужно записать это значение 0x7B01 в двоичном представлении таким образом: 0000_0001_0111_1011 (байты меняются местами), поставить на место каждого ненулевого разряда "2" в соответствующей степени, а далее суммировать полученные числа. В нашем примере:

Код знака
(5 разрядов)
Целая часть
(7 разрядов)
Дробная часть
(4 разряда)
1514131211 10987654 3210 Сумма
0000 0001 0111 1011
0000 00024 0222120 2-102-32-4 +23.6875°C
+ 23 11×0.0625=0.6875 +23.6875°C

Я выделил разными цветами логические группы в коде температуры. Здесь старшие 5 нулей – это просто код знака «плюс». Двоичное 0010111 (следующие 7 разрядов) – это десятичное число 23, и это целая часть температуры (получается +23°C). Двоичное 1011 (младшие 4 разряда) при переводе в десятичную систему это число 11, и это код дробной части температуры. Так как при разрядности 12 бит на 1 разряд приходится 0.0625°C (при разрядности 11 бит – 0.125°, 10 бит – 0.25°, 9 бит – 0.5°), то мы просто умножили код (11) на вес одного разряда (0.0625°). В сумме получается то же самое значение температуры 23.6875°

Для отрицательных чисел значение температуры ищется несколько иначе, т.к. оно представлено в виде дополнительного кода. Для перевода из допкода необходимо из значения вычесть 1, а полученное число инвертировать. Например, при температуре минус 55.0625° на позициях целой части будет двоичное 100_1001, а на позициях дробной части двоичное 1111. Для перевода целой части вычтем единицу: 100_1001−1=100_1000. Инвертируем: 100_1000 011_0111. Что и равняется десятичному числу 55. Для перевода дробной части: 1111−1=1110; 1110 0001. Теперь 0001×0.0625=0.0625. Складываем дробную и целую части; не забудем также про знак «минус». Итого получилось −55.0625°C.

Более подробно и интересно про дополнительный код (и в целом о представлении целых чисел в ПК) можно почитать в книге Азы программирования на страницах 167 и далее (§1.6.2).

Для чтения нового показания температуры с датчиков DS18B20 необходимо повторить всю последовательность действий.

Последнее изменениеПятница, 11 Июнь 2021 12:33 Прочитано 2415 раз

Поделиться

Print Friendly, PDF & Email

Оставить комментарий