1. Тема дня

Сегодня разбираем промышленный последовательный транспорт:

text
UART
→ RS-485 transceiver
→ двухпроводная half-duplex шина
→ управление DE/~RE
→ границы кадров
→ CRC
→ Modbus RTU или собственный протокол

Главная мысль: RS-485 определяет электрический интерфейс, но не определяет кадры, адреса и команды. Протокол вроде Modbus RTU должен отдельно решить, кто и когда получает право передавать, где заканчивается кадр и что делать после ошибки.

2. Зачем это нужно в твоих проектах

RS-485 может использоваться для связи с удалёнными IO-модулями, релейными блоками, контроллерами подсветки, HIL-стендом, промышленной панелью или ПЛК. Для контроллера фаз светофора удалённый модуль может отдавать:

text
регистр 0: текущая фаза
регистр 1: raw input mask
регистр 2: conflict flags
регистр 3: glitch counter
регистр 4-5: напряжение питания
регистр 6-7: sequence

Нужно отличать:

text
устройство не ответило;
CRC неверен;
кадр оборван;
линия занята;
ответ пришёл от другого адреса;
UART потерял байты;
DE выключился слишком рано.

3. Теория

3.3.1 3.1. UART и RS-485 - разные уровни

UART формирует start/data/parity/stop биты. RS-485 transceiver преобразует логические уровни MCU в дифференциальную пару A/B.

text
MCU UART_TX/RX/DE
→ RS-485 transceiver
→ A/B twisted pair

Часто DE и ~RE соединяют вместе:

text
0: transmitter disabled, receiver enabled
1: transmitter enabled, receiver disabled

Но при такой схеме узел не слышит собственную передачу и хуже обнаруживает коллизии.

3.3.2 3.2. Half-duplex требует владельца шины

На двухпроводной линии все устройства используют одну пару и для TX, и для RX. В каждый момент времени передавать должен только один узел. Типовая модель:

text
client/master создаёт запрос
server/slave отвечает только на свой адрес

3.3.3 3.3. DMA complete и UART Transmission Complete - не одно и то же

DMA complete означает, что DMA перенёс последний байт в UART data register/FIFO. Но UART ещё может передавать последний data byte, parity bit и stop bit. Правильная TX-последовательность:

text
1. DE = 1.
2. Подождать включение трансивера.
3. Запустить UART TX DMA.
4. Дождаться DMA complete.
5. Дождаться UART TC - Transmission Complete.
6. DE = 0.
7. Перейти в RX.

3.3.4 3.4. State machine передатчика

c
typedef enum {
    RS485_STATE_RX_IDLE = 0,
    RS485_STATE_WAIT_BUS_IDLE,
    RS485_STATE_DE_ASSERT,
    RS485_STATE_TX_DMA,
    RS485_STATE_WAIT_UART_TC,
    RS485_STATE_DE_RELEASE,
    RS485_STATE_TURNAROUND,
    RS485_STATE_WAIT_RESPONSE,
    RS485_STATE_ERROR,
} rs485_state_t;

Нельзя разрешать нескольким задачам напрямую вызывать UART TX. Один rs485_task или modbus_task должен владеть UART, DE, RX framing, TX queue, request timeout и текущей транзакцией. 3.3.5 3.5. Character time

Для формата 8E1:

text
1 start + 8 data + 1 parity + 1 stop = 11 бит

Время одного символа:

text
Tchar = character_bits / baudrate

Пример для 9600 baud, 11 бит:

text
Tchar = 11 / 9600 ≈ 1.146 мс
t1.5 ≈ 1.719 мс
t3.5 ≈ 4.010 мс

Helper:

c
typedef struct {
    uint32_t baudrate;
    uint8_t data_bits;
    bool parity_enabled;
    uint8_t stop_bits;
} serial_format_t;
static uint32_t serial_char_time_us(const serial_format_t *fmt)
{
    uint32_t bits = 1U + fmt->data_bits +
                    (fmt->parity_enabled ? 1U : 0U) +
                    fmt->stop_bits;
    return (uint32_t)(((uint64_t)bits * 1000000ULL +
                       fmt->baudrate - 1U) /
                      fmt->baudrate);
}

3.3.6 3.6. Modbus RTU framing

Кадр:

text
Address | Function | Data | CRC16

Границы задаются тишиной:

text
до начала кадра: не менее 3.5 символов тишины
внутри кадра: пауза не более 1.5 символов
после кадра: не менее 3.5 символов тишины

Если внутри кадра пауза больше t1.5, кадр считается неполным и должен быть отброшен. Для скоростей выше 19200 bit/s часто используют фиксированные значения:

text
t1.5 = 750 мкс
t3.5 = 1750 мкс

3.3.7 3.7. Почему одного UART IDLE недостаточно

UART IDLE часто срабатывает примерно после одного character time. Modbus требует t3.5. Поэтому IDLE можно использовать только как ранний сигнал:

text
UART IDLE
→ зафиксировать DMA position
→ запустить one-shot timer t3.5
→ если новых байтов нет до timeout
→ frame complete

3.3.8 3.8. CRC16 Modbus

c
uint16_t modbus_crc16(const uint8_t *data, size_t length)
{
    uint16_t crc = 0xFFFFU;
    for (size_t i = 0; i < length; i++) {
        crc ^= data[i];
        for (unsigned bit = 0; bit < 8; bit++) {
            if ((crc & 1U) != 0U) {
                crc = (uint16_t)((crc >> 1U) ^ 0xA001U);
            } else {
                crc >>= 1U;
            }
        }
    }
    return crc;
}

CRC проверяется до изменения прикладного состояния.

3.3.9 3.9. Broadcast

Broadcast-запрос не должен порождать ответ. Иначе все серверы ответят одновременно и создадут коллизию.

3.3.10 3.10. ESP32 RS-485 mode

ESP-IDF позволяет настроить UART в half-duplex RS-485 mode, где драйвер управляет RTS/DE:

c
uart_set_pin(UART_NUM_1, TX_GPIO, RX_GPIO, RTS_GPIO, UART_PIN_NO_CHANGE);
uart_set_mode(UART_NUM_1, UART_MODE_RS485_HALF_DUPLEX);

После этого другие модули не должны вручную переключать тот же DE GPIO.

4. Схемотехнические ограничения

RS-485 требует правильной топологии:

text
termination                           termination
    │                                     │
────┴───────────┬──────────┬─────────────┴──── trunk
                │          │
             short stub  short stub

Termination должна быть только на двух концах магистрали, не на каждом модуле. Biasing/polarization также должна быть в одной контролируемой точке.

5. Типичные ошибки

text
1. Выключить DE по DMA complete.
2. Использовать vTaskDelay() для управления DE.
3. Считать UART IDLE концом Modbus-кадра.
4. Делить UART между несколькими задачами.
5. Не проверять CRC до выполнения команды.
6. Отвечать на broadcast.
7. Терминировать каждый узел.
8. Установить bias на каждой плате.
9. Перепутать A/B.
10. Не учитывать Common.
11. После RX overrun продолжать parser.
12. Использовать software timeout с низкой точностью.

6. Практическое задание на 30-60 минут

Создай RS485_MODBUS_POLICY.md:

markdown
# RS-485 / Modbus policy
1. UART and DE have one owner task.
2. DE is released only after UART Transmission Complete.
3. DMA Complete is not treated as end of wire transmission.
4. RX framing follows t1.5 and t3.5 rules.
5. CRC is checked before application processing.
6. Broadcast requests never produce a reply.
7. RX loss moves parser to DESYNC.
8. Termination exists only at both ends of the trunk.
9. Biasing exists at no more than one designated location.
10. Every request has timeout, retry limit and diagnostics.

Добавь тест CRC для запроса:

text
01 03 00 00 00 0A

Полный кадр должен быть:

text
01 03 00 00 00 0A C5 CD

Добавь диагностику:

c
typedef struct {
    uint32_t tx_frames;
    uint32_t rx_frames;
    uint32_t timeout;
    uint32_t crc_error;
    uint32_t interchar_error;
    uint32_t rx_overflow;
    uint32_t collision;
    uint32_t wrong_address;
    uint32_t wrong_function;
    uint32_t retries;
    uint32_t devices_offline;
    uint32_t max_response_us;
    uint32_t max_turnaround_us;
} modbus_diag_t;

7. Что почитать или попробовать дальше

  • ESP-IDF UART/RS-485 documentation.
  • STM32 HAL/LL UART DMA, UART TC, IDLE detection и hardware DE.
  • MODBUS over Serial Line Specification and Implementation Guide V1.02.

Короткий итог

text
application request
→ modbus service queue
→ RTU encoder + CRC
→ RS-485 owner task
→ DE assert
→ UART DMA
→ UART Transmission Complete
→ DE release
→ RX DMA/ring
→ t1.5/t3.5 framer
→ CRC/address/function validation
→ immutable response event

Задание

DMA complete сработал, пока UART ещё передаёт последний stop bit. Объясните отказ при немедленном DE release и правильный state transition.

Критерии самопроверки: Отделить DMA completion от UART completion и назначить одного DE owner; произвольный task delay не является доказательством.

Показать ответ автора

DE release может обрезать frame. DMA completion означает завершение memory-to-UART transfer, а не wire transmission. Держать DE до UART Transmission Complete, затем отпустить и перейти в RX по transceiver timing contract.

Задание

UART IDLE произошёл до Modbus t3.5 boundary. Определите framing при следующем byte и при достижении требуемой тишины.

Критерии самопроверки: Определить оба timing cases и invalid-frame path; не приравнивать IDLE к protocol completion.

Показать ответ автора

Использовать IDLE как раннее наблюдение, учитывать byte arrival и framing timer, объявлять complete frame только после нужной тишины. Отклонить неполный frame при нарушении internal timing t1.5; проверить CRC/address/function до изменения application state.