Чтение энкодера ИДМ39А по BiSS-C через микросхемы Ftdi
Чтение многооборотного энкодера ИДМ39А по BiSS-C
Как подружить промышленный энкодер с дифференциальным интерфейсом BiSS-C и обычным ПК, не покупая специализированный контроллер. С подводными камнями MPSSE, «магической» константой в CRC и собственным парсером для Saleae Logic 2.
Задача
Нужно было считать абсолютные показания углового положения и количества оборотов вала с промышленного энкодера ИДМ39А-4096-MT-BISS. Датчик выдаёт данные по цифровому интерфейсу BiSS-C, но в ПК нет ни одного готового порта BiSS-C — только USB. Готовые шлюзы BiSS-C↔USB стоят неприлично дорого, а задача разовая. Решено было собрать собственный интерфейс на доступных компонентах.
Энкодер ИДМ39А-4096-MT-BISS
Это многооборотный абсолютный энкодер с разрешением 4096 позиций на оборот (12 бит) и счётчиком до 5400 оборотов. Ключевые характеристики:
- Диапазон измерения углов: 0…360°
- Информационная ёмкость: 12 бит на оборот
- Количество оборотов: 5400
- Погрешность: ±0,5°
- Максимальная частота интерфейса BiSS: 4 МГц
- Период обновления: не более 50 мкс
- Ток потребления: до 120 мА
- Питание: 5 В
- Интерфейс: BiSS-C, дифференциальный RS-422
Физически от энкодера отходят восемь проводов:
| № | Обозн. | Назначение | Цвет |
|---|---|---|---|
| 1 | + | Питание 5 В | красный |
| 2 | DP | Выход данных, положительный | белый |
| 3 | DN | Выход данных, отрицательный | серый |
| 4 | CP | Вход тактовой частоты, положительный | зелёный |
| 5 | CN | Вход тактовой частоты, отрицательный | коричневый |
| 6 | G | Общий (0 В) | чёрный |
| 7 | Y | Установка нуля | жёлтый |
| 8 | O | Установка направления | оранжевый |
Протокол BiSS-C
BiSS-C (Bidirectional Synchronous/Serial — Compact) — это простой полудуплексный последовательный интерфейс, разработанный для датчиков. Мастер (контроллер) генерирует такты, слейв (энкодер) отвечает кадром фиксированной длины. Никаких линий запроса, никаких адресов — только такты и данные.
Кадр ИДМ39А содержит 37 бит (в спецификации указано «35 бит»; документация здесь особенно доставляет):
| Бит | 35 | 34 | 33 | 32 | 31…20 | 19…8 | 7 | 6 | 5…0 |
|---|---|---|---|---|---|---|---|---|---|
| Поле | MSB=1 | Ack=0 | Start=1 | CDS | TURNS[12:0] | ANGLE[11:0] | E | W | CRC |
| Длина | 1 | 1 | 1 | 1 | 13 | 12 | 1 | 1 | 6 |
Здесь стоит остановиться на важном моменте: поле TURNS[12:0] — это 13 бит, а не 12. Иначе невозможно закодировать заявленные 5400 оборотов (12 бит дали бы максимум 4095). Эта деталь стоила мне пары часов отладки, о чём ниже.
CRC считается полиномом X^6 + X^1 + X^0 (0x43) с инверсией результата. Окно расчёта — биты данных от старшего до W включительно.
Выбор интерфейса: FT2232D
Для связи с ПК я выбрал микросхему FT2232D. Причины:
- работает с 5-вольтовой логикой (в отличие от FT2232H, который 3,3 В) — это важно, так как энкодер питается от 5 В;
- поддерживает режим MPSSE — аппаратный движок для SPI/I²C/JTAG, который позволяет генерировать такты и читать данные на скоростях до 6 МГц без участия процессора;
- есть готовая библиотека
FTD2XX.NETдля .NET; - распространена и недорога.
Однако FT2232D выдаёт однополярные TTL-сигналы, а энкодер ждёт дифференциальные RS-422. Напрямую соединять их нельзя — ни по уровням, ни по принципу передачи.
Схема подключения через RS-422 драйвер
Между FT2232D и энкодером необходим полнодуплексный RS-422 трансивер. Я использовал связку на базе MAX3491 (или аналог — THVD1424, SN65HVD3082). Схема получилась такая:
ADBUS0(TCK/SCLK) FT2232D → входDIпередатчика тактов трансивера → выходыCP/CNэнкодера;- выход
ROприёмника данных трансивера →ADBUS2(TDO/MISO) FT2232D; входыA/Bприёмника ←DP/DNэнкодера; ADBUS1(TDI/MOSI) остаётся в покое — BiSS-C полудуплексный, мастер ничего не пишет в линию данных;DEтрансивера подтянут кVCC,RE— кGND(приёмник и передатчик всегда активны);- общая земля источника питания 5 В, FT2232D и энкодера обязательно соединяется в одной точке.
Питание 5 В для энкодера (до 120 мА) лучше брать от внешнего стабилизатора, а не с VCC самого модуля FT2232D — USB-порт не всегда вытягивает такую нагрузку стабильно.
MPSSE: команды и особенности
Весь обмен с MPSSE идёт через FT_Write / FT_Read библиотеки FTD2XX. Первым делом микросхему нужно перевести в режим MPSSE и настроить такты:
' Инициализация MPSSE
ftdi.SetBitMode(0, FT_BIT_MODES.FT_BIT_MODE_RESET)
ftdi.SetBitMode(0, FT_BIT_MODES.FT_BIT_MODE_MPSSE)
' Делитель тактовой частоты. Базовая частота 60 МГц.
' Формула: TCK = 60 МГц / (2 * (Divisor + 1))
' Для 100 кГц: Divisor = 299 = 0x012B
Dim initBytes As Byte() = {
&H86, &H2B, &H01, ' Set TCK divisor → 100 кГц
&H80, &H00, &H0B, ' ADBUS0(TCK)=Out, ADBUS1(TDI)=Out, ADBUS2(TDO)=In
&H82, &H00, &H00 ' ACBUS все входы
}
Для чтения кадра от энкодера нужна одна команда, которая одновременно генерирует 40 тактов и читает 5 байт с TDO. На TDI ничего не пишется — мастер в BiSS-C молчит.
Я перепробовал несколько вариантов и пришёл к такой паре команд:
' 0x20 — Clock Data Bytes In, -VE Clock, LSB First
' 0x04, 0x00 — длина = 4 + 1 = 5 байт (40 бит)
' 0x87 — Send Immediate (КРИТИЧНО!)
Dim readCmd As Byte() = { &H20, &H04, &H00, &H87 }
Без байта 0x87 FTDI не отправляет прочитанные данные на хост сразу, а копит их в буфере до 64 байт или до USB-таймаута (16 мс). Именно из-за этого у меня сначала приходило то 40 бит, то 64, то 24 — данные от нескольких вызовов склеивались в буфере. Send Immediate решает проблему полностью.
Читаем ровно 5 байт:
ftdi.Purge(FT_PURGE_RX Or FT_PURGE_TX)
ftdi.Write(readCmd, readCmd.Length, bytesWritten)
' Ждём появления 5 байт в RX-буфере
Dim rxBytes As UInteger = 0
Do While rxBytes < 5
Await Task.Delay(1)
ftdi.GetQueueStatus(rxBytes)
Loop
Dim buffer(4) As Byte
ftdi.Read(buffer, 5, bytesRead)
Сборка кадра и разбор полей
Из 40 прочитанных битов полезными являются младшие 37 — старшие 3 отбрасываются маской:
Dim raw40 As ULong = (CULng(buffer(0)) << 32) Or
(CULng(buffer(1)) << 24) Or
(CULng(buffer(2)) << 16) Or
(CULng(buffer(3)) << 8) Or
CULng(buffer(4))
Dim data36 As ULong = raw40 And &H1FFFFFFFFFUL ' маска на 37 бит
' Проверка префикса кадра
Dim msb = (data36 >> 36) And 1
Dim ack = (data36 >> 35) And 1
Dim start = (data36 >> 34) And 1
If msb <> 1 OrElse ack <> 0 OrElse start <> 1 Then
' кадр невалиден
End If
' Разбор полей
turns = CInt((data36 >> 20) And &H1FFF) ' 13 бит! (биты 32…20)
angle = CInt((data36 >> 8) And &HFFF) ' 12 бит (биты 19…8)
hasError = ((data36 >> 7) And 1) = 0 ' E инверсный
hasWarning = ((data36 >> 6) And 1) = 1
crcRx = CByte(data36 And &H3F)
CRC-6 и «магическая» константа 9
Самая интересная часть отладки. Реализация CRC по спецификации — полином 0x43, init=0, инверсия результата. Но работала она только при малых оборотах. На конце диапазона (примерно от 4096) CRC переставал сходиться.
Эмпирически я обнаружил, что если к рассчитанному CRC применить XOR с числом 0b1001 = 9, то всё сходится. Но хардкодить константу — плохое решение. Разобраться помогла математика:
CRC линеен по данным. Если пропустить один бит в фиксированной позиции, разница между правильным и «обрезанным» CRC — это вклад единичного бита на этой позиции, то есть x^n mod p(x). Для бита 32 и полинома x^6 + x + 1:
x^32 mod (x^6 + x + 1) = x^3 + 1 = 0b001001 = 9
Ровно та константа, что я нашёл эмпирически! Вывод: в окне расчёта CRC не хватало бита 32 — старшего бита поля TURNS[12]. Пока оборотов было меньше 4096, этот бит был равен нулю и не влиял на CRC. Как только вал перешагивал отметку 4096 — появлялась ошибка ровно на 9.
Исправленная функция:
Private Shared Function CalculateBissCrc6(data As ULong) As Byte
Const POLY As Byte = &H3 ' 0x43 без старшего бита
Const MASK As Byte = &H3F
Dim crc As Byte = 0
' Окно 32..6 (27 бит): TURNS[12:0] + ANGLE[11:0] + E + W
For i As Integer = 32 To 6 Step -1
Dim bit = CInt((data >> i) And 1UL)
Dim feedback = ((crc >> 5) And 1) Xor bit
crc = (crc << 1) And MASK
If feedback <> 0 Then crc = crc Xor POLY
Next
Return crc Xor MASK
End Function
Отладка через Saleae Logic 2
Без осциллографа и логического анализатора отладка BiSS-C превращается в гадание. У меня был Saleae Logic 2 — его двух каналов (SCLK + MISO) хватило, чтобы снять весь кадр.
Встроенный SPI-анализатор Saleae корректно нарезал поток на байты, но интерпретировать их как BiSS-кадры не умел. Пришлось написать собственный High Level Analyzer (HLA) на Python. HLA в Logic 2 — это надстройка, которая получает на вход фреймы от низкоуровневого анализатора (в нашем случае SPI) и выдаёт свои, уже осмысленные.
Ключевые моменты реализации:
- HLA не имеет прямого доступа к цифровым каналам — только через LLA (SPI);
- объекты времени в новых версиях Saleae — это
SaleaeTime, а неfloat; арифметика с ними работает через операторы иto_nanoseconds(); - SPI-анализатор отдаёт данные в ключе
misoкакbytearray; - автоматический поиск выравнивания по префиксу
1-0-1и CRC снимает проблему «сдвига на 4/5 бит».
Код парсера
from saleae.analyzers import HighLevelAnalyzer, AnalyzerFrame, ChoicesSetting
MASK36 = 0xFFFFFFFFF # 36 бит
class Hla(HighLevelAnalyzer):
"""BiSS-C для ИДМ39А-4096-MT-BISS. Вход: SPI-анализатор (MISO = DP)."""
alignment = ChoicesSetting(choices=(
'Auto (поиск 1-0-1 + CRC)',
'Fixed (отбросить 4 бита)'
))
crc_check = ChoicesSetting(choices=('On', 'Off'))
result_types = {
'biss_frame': {
'format': '{{data.status}} T={{data.turns}} '
'A={{data.angle}} ({{data.angle_deg}}°) {{data.flags}}'
},
}
def __init__(self):
self._bits = []
self._start = None
def _reset(self):
self._bits = []
self._start = None
def decode(self, frame: AnalyzerFrame):
if frame.type == 'enable':
self._reset()
return None
if frame.type != 'result':
return None
raw_bytes = frame.data.get('miso')
if raw_bytes is None:
return None
if isinstance(raw_bytes, int):
raw_bytes = bytes([raw_bytes])
for byte in raw_bytes:
for i in range(7, -1, -1):
self._bits.append((byte >> i) & 1)
if self._start is None:
self._start = frame.start_time
if len(self._bits) < 40:
return None
result = self._process(frame.end_time)
if result is not None:
self._reset()
return result
else:
# невалидный кадр — сдвигаемся на бит и пробуем снова
self._bits.pop(0)
if not self._bits:
self._start = None
return None
def _process(self, end_time):
raw40 = 0
for b in self._bits[-40:]:
raw40 = (raw40 << 1) | b
candidates = []
if self.alignment.startswith('Fixed'):
candidates = [raw40 & MASK36]
else:
for shift in (4, 3, 2, 1, 0):
candidates.append((raw40 >> shift) & MASK36)
chosen = None
for cand in candidates:
prefix_ok = (((cand >> 35) & 1) == 1 and
((cand >> 34) & 1) == 0 and
((cand >> 33) & 1) == 1)
crc_ok = ((cand & 0x3F) == self._crc6(cand))
if prefix_ok and (crc_ok or self.crc_check == 'Off'):
chosen = cand
break
if chosen is None:
return None
raw = chosen
turns = (raw >> 20) & 0x1FFF # 13 бит!
angle = (raw >> 8) & 0xFFF
e_bit = (raw >> 7) & 1
w_bit = (raw >> 6) & 1
crc_rx = raw & 0x3F
crc_calc = self._crc6(raw)
angle_deg = angle * 360.0 / 4096.0
flags = ''
if e_bit == 0: flags += '[E]'
if w_bit == 1: flags += '[W]'
if self.crc_check == 'On' and crc_rx != crc_calc:
flags += f'[CRC rx={crc_rx:02X} calc={crc_calc:02X}]'
ok = crc_rx == crc_calc or self.crc_check == 'Off'
return AnalyzerFrame('biss_frame', self._start, end_time, {
'status': 'OK' if ok else 'ERR',
'turns': turns,
'angle': angle,
'angle_deg': round(angle_deg, 3),
'flags': flags,
'raw_hex': f'{raw:09X}',
'crc_rx': f'{crc_rx:02X}',
'crc_calc': f'{crc_calc:02X}',
})
@staticmethod
def _crc6(raw36: int) -> int:
"""CRC-6 BiSS-C: окно 32..6, полином 0x43, инверсия."""
crc = 0
for i in range(32, 5, -1):
bit = (raw36 >> i) & 1
feedback = ((crc >> 5) & 1) ^ bit
crc = (crc << 1) & 0x3F
if feedback:
crc ^= 0x03
return crc ^ 0x3F
Подводные камни, о которые я набил шишки
- Буферизация MPSSE. Без
Send Immediate(0x87) FTDI копит данные и отдаёт их пачками. Всегда добавляйте 0x87 в конец команды чтения. - Длина посылки в документации. В PDF написано «35 бит», а в таблице — индексы 35…0. Это 36 бит. Всегда проверяйте арифметику таблицы, а не верьте заголовку.
- TURNS — 13 бит, а не 12. Иначе на 4096-м обороте счётчик обнулится, а CRC перестанет сходиться.
- Бит 32 входит в CRC. Окно расчёта — 32…6, а не 31…6. Эмпирическая константа
XOR 9— этоx^32 mod p(x), прямое указание на пропущенный бит. - Общая земля. Без соединения GND источника питания 5 В, FT2232D и энкодера дифференциальный приёмник трансивера не сможет корректно определять уровни.
- Питание 120 мА. Не тяните с USB-порта через FT2232D — используйте отдельный стабилизатор.
Итоги
Связка FT2232D + RS-422 трансивер + FTD2XX + собственный HLA-парсер обошлась в копейки и закрыла задачу полностью. Главное, что я вынес из этой работы: в BiSS-C, как и в любом «простом» последовательном протоколе, дьявол живёт в деталях — в количестве бит окна CRC, в длине поля счётчика оборотов, в команде Send Immediate. И без логического анализатора отладка превращается в мучение.
Если у вас похожая задача — не покупайте готовый шлюз BiSS-C↔USB, пока не попробуете собрать его сами. Получится дешевле, а понимание протокола останется с вами надолго.
Скачать вложения:
- ИДМ39а - программа и парсер (5 Скачиваний)
