Рейтинг@Mail.ru

Чтение энкодера ИДМ39А по BiSS-C через микросхемы Ftdi

Print Friendly, PDF & Email

Чтение многооборотного энкодера ИДМ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 Вкрасный
2DPВыход данных, положительныйбелый
3DNВыход данных, отрицательныйсерый
4CPВход тактовой частоты, положительныйзелёный
5CNВход тактовой частоты, отрицательныйкоричневый
6GОбщий (0 В)чёрный
7YУстановка нуляжёлтый
8OУстановка направленияоранжевый
В документации не описано как делать установку нуля и направления. Чтобы сделать сброс (угла и счётчика оборотов), достаточно на 1 сек заземлить жёлтый провод. Установка направления хитрее.

Протокол BiSS-C

BiSS-C (Bidirectional Synchronous/Serial — Compact) — это простой полудуплексный последовательный интерфейс, разработанный для датчиков. Мастер (контроллер) генерирует такты, слейв (энкодер) отвечает кадром фиксированной длины. Никаких линий запроса, никаких адресов — только такты и данные.

Кадр ИДМ39А содержит 37 бит (в спецификации указано «35 бит»; документация здесь особенно доставляет):

Бит3534333231…2019…8765…0
ПолеMSB=1Ack=0Start=1CDSTURNS[12:0]ANGLE[11:0]EWCRC
Длина11111312116

Здесь стоит остановиться на важном моменте: поле 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

Подводные камни, о которые я набил шишки

  1. Буферизация MPSSE. Без Send Immediate (0x87) FTDI копит данные и отдаёт их пачками. Всегда добавляйте 0x87 в конец команды чтения.
  2. Длина посылки в документации. В PDF написано «35 бит», а в таблице — индексы 35…0. Это 36 бит. Всегда проверяйте арифметику таблицы, а не верьте заголовку.
  3. TURNS — 13 бит, а не 12. Иначе на 4096-м обороте счётчик обнулится, а CRC перестанет сходиться.
  4. Бит 32 входит в CRC. Окно расчёта — 32…6, а не 31…6. Эмпирическая константа XOR 9 — это x^32 mod p(x), прямое указание на пропущенный бит.
  5. Общая земля. Без соединения GND источника питания 5 В, FT2232D и энкодера дифференциальный приёмник трансивера не сможет корректно определять уровни.
  6. Питание 120 мА. Не тяните с USB-порта через FT2232D — используйте отдельный стабилизатор.
В приложении к статье архив, в котором лежит программа для чтения показаний энкодера ИДМ39А, а также парсер для логического анализатора.

Итоги

Связка FT2232D + RS-422 трансивер + FTD2XX + собственный HLA-парсер обошлась в копейки и закрыла задачу полностью. Главное, что я вынес из этой работы: в BiSS-C, как и в любом «простом» последовательном протоколе, дьявол живёт в деталях — в количестве бит окна CRC, в длине поля счётчика оборотов, в команде Send Immediate. И без логического анализатора отладка превращается в мучение.

Если у вас похожая задача — не покупайте готовый шлюз BiSS-C↔USB, пока не попробуете собрать его сами. Получится дешевле, а понимание протокола останется с вами надолго.

Скачать вложения:

Последнее изменениеЧетверг, 12 Ноябрь 2026 18:03 Прочитано 29 раз

Поблагодарить автора:

Поделиться

Print Friendly, PDF & Email

Другие материалы в этой категории:

« Универсальный USB–TTL адаптер на базе CP2102