Реализация интерфейса 1-Wire на микросхемах FTDI
1Описание однопроводного последовательного интерфейса 1-Wire
Интерфейс 1-Wire (или OneWire) распространён весьма широко и применяется нами в повседневной жизни каждый день. Например, он используется для считывания ключа от домофона. Также для чтения показаний всевозможных датчиков. Его основные преимущества в том, что для информационного обмена используется всего один провод, который также может являться и питающим для подключённых устройств. На одной шине могут находиться несколько ведомых устройств одновременно.
Линия 1-Wire подтянута к высокому уровню (чаще всего от 3 до 5 В). Весь обмен инициирует ведущее устройство (master). Существуют 4 типа сигналов: сброс, чтение, запись бита "0" и запись бита "1".
Для сигнала сброса ведущий притягивает линию к нулю на определённый промежуток времени, а затем отпускает. Линия снова подтягивается к высокому уровню. Если на линии присутствует ведомый, то после короткой паузы он отмечает своё присутствие также притягиванием линии к нулю, а затем отпускает. Если на линии кроме мастера нет других устройств, то линия так и останется в логической единице.
Для записи одного бита ведущий притягивает линию к нулю: для записи "1" на короткий промежуток времени, для записи "0" – на более длинный.
Для чтения одного бита ведущий притягивает линию к нулю и отпускает. Если ведомый хочет передать "0", то он продолжает удерживать линию в нуле, а если хочет передать "1", то отпускает линию, и она подтягивается к высокому уровню.
Будем работать с библиотекой 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 в разных командах. А в таблице приведены средние значения длительности этих участков для обоих скоростных режимов.
Обозначение | Длительность в режиме standard, мкс | Длительность в режиме overdrive, мкс |
---|---|---|
A | 6 | 1 |
B | 64 | 7.5 |
C | 60 | 7.5 |
D | 10 | 2.5 |
E | 9 | 1 |
F | 55 | 7 |
G | 0 | 2.5 |
H | 480 | 70 |
I | 70 | 8.5 |
J | 410 | 40 |
Более детальное описание интерфейса с наглядными иллюстрациями и примерами можно почитать здесь на русском языке, а также на сайте создателя интерфейса 1-Wire.
Для примера на рисунке ниже показаны две ситуации. В первой мы посылаем сигнал сброса и не получаем ответ ведомого, а во второй – получаем ответ. Оба раза мы передаём по линии MOSI SPI число 0x0F: 4 такта низкий уровень и 4 такта высокий. В первом случае, когда на линии нет ведомого устройства, с линии MISO SPI будет прочитано 0x0F (число не изменилось), а во втором – 0x07. Во второй ситуации ведомый опустил линию примерно на 1 такт SPI (линия на 5-ом такте также в низком уровне), из-за чего мы прочитали не то, что записали. Длительность рассчитана так, чтобы соответствовать требованиям интерфейса 1-Wire. Длительность низкого уровня за эти 4 такта равняется необходимым 480 мкс в стандартном скоростном режиме или 1 мкс в ускоренном. Для этого тактовая частота SPI должна быть около 8200 бит/сек в стандартном режиме и 56300 бит/сек в ускоренном режиме.
По аналогии с рассмотренным примером будут реализованы команды для записи и для чтения битов интерфейса 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).
Код класса для работы в режиме 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
Вот как выглядит макет, на котором всё это отлаживалось. Не удивляйтесь такому количеству проводов для однопроводного интерфейса :) Большая их часть не задействована.
Приведённого низкоуровневого программного кода достаточно для того чтобы реализовать протокол обмена с конкретными устройствами на шине 1-Wire. Например, теми же домофонными ключами iButton.
3Чтение показаний температуры датчика DS18B20по интерфейсу 1-Wire
Для примера прочитаем показания датчика температуры DS18B20 по интерфейсу 1-Wire с помощью микросхемы FT2232H и программы SPI via FTDI. С одним устройством на шине One-wire работать проще, т.к. можно не учитывать идентификационный номер устройства. Поэтому повесим на шину 1-Wire сразу два датчика, чтобы немного усложнить задачу.
Скачать техническое описание DS18B20 можно во вложении внизу статьи, а вот здесь можно заказать DS18B20.
Для обмена с датчиком 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 в выпадающий список.
Теперь выбираем из выпадающего списка датчик, с которого мы хотим прочитать температуру. Нажимаем кнопку RESET, далее MATCH ROM, далее в поле «Команда» вводим 44 и нажимаем «Записать». Этим мы сообщили датчику, чтобы он измерил температуру и записал её в свои регистры.
Можно на этом шаге, кстати, вместо MATCH ROM нажать кнопку SKIP ROM. Таким образом мы пропустим обработку уникального идентификатора ROM, и оба датчика начнут измерение температуры. А уже следующей командой мы заберём данные каждого сенсора адресно.
Для чтения температуры нажимаем RESET, MATCH ROM, в разделе «Чтение» записываем команду BE и указываем, что хотим прочитать 9 байт, и нажимаем «Прочитать». В таблице прочитанных данных отбразятся 9 байтов из памяти выбранного датчика. В приведённом случае (см. иллюстрацию выше) это
Как видно, первые два байта в памяти – это закодированное значение температуры. Следующие два байта – пользовательские данные или установка температуры, при которой датчик перейдёт в «тревожный» режим. Пятый байт – регистр конфигурации; он определяет, с какой точностью (и, следовательно, как быстро) будет измеряться и выводиться температура. Далее идут не значащие 3 байта, а последний байт – контрольная сумма. Регистр конфигурации датчика DS18B20 в нашем случае содержит число 0x7F (в двоичном виде 0111_1111). Значит разрядность измерений температуры составляет 12 бит:
В таблице также представлено время, требуемое на одно измерение температуры при разной точности сенсора DS18B20.
Итак, мы прочитали содержимое памяти. Код температуры в нашем случае 7B 01, а разрядность измерений – 12 бит. Как же это число перевести в градусы Цельсия? Для этого посмотрим на формат регистра, в котором хранятся данные температуры. На рисунке S – это знак температуры ("0" означает положительную температуру, "1" – отрицательную), LS – младший байт, MS – старший байт. А биты с 0 по 10 хранят код температуры. Для 12-битового разрешения все биты с 0 по 10 являются значащими. Для 11-битового разрешения бит 0 является незначащим, для 10-битового – незначащими являются 0 и 1 биты, а для 9-разрядного биты 0, 1 и 2 – незначащие.
Нужно записать это значение 0x7B01 в двоичном представлении таким образом: 0000_0001_0111_1011 (байты меняются местами), поставить на место каждого ненулевого разряда "2" в соответствующей степени, а далее суммировать полученные числа. В нашем примере:
Код знака (5 разрядов) |
Целая часть (7 разрядов) |
Дробная часть (4 разряда) |
||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Сумма |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 24 | 0 | 22 | 21 | 20 | 2-1 | 0 | 2-3 | 2-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 необходимо повторить всю последовательность действий.
Download attachments:
- Скачать datasheet на DS18B20 (172 Downloads)