115б. Последовательный порт со стороны компьютера: мосты, драйверы, терминалы, задержки
Раздел 16 устроил порт со стороны STM32: кадр, делитель, TXE и TC.
Раздел 20.7 — виртуальный порт на USB самого камня, раздел 194 — сырой
режим порта в Linux на языке C. Между проводом TX и строкой в программе
на компьютере стоит ещё целая цепочка: мост или отладчик, драйвер, имя
порта, терминал с его настройками. Здесь — кто в этой цепочке, как порт
называется в Windows и Linux, почему плата перезагружается при открытии
порта, откуда берутся задержки и почему порт USB CDC пропадает.
Опоры: исходники pyserial 3.5 из образа stm-pio:1 (list_ports_linux.py,
serialposix.py, serialwin32.py, терминал miniterm.py), правила udev
PlatformIO из того же образа, файлы INF драйверов из хранилища драйверов
Windows этой машины (CH341SER.INF, stlink_vcp.inf, stmcdc.inf,
usbser.inf), заголовки ядра Linux 6.1 из stm-sys:1, библиотека USB
Device и пример CDC_Standalone из пакета STM32Cube F1 1.8.7. Опыты — на
виртуальной паре портов (псевдотерминалы Linux в stm-arm:1) и с прошивкой
в QEMU netduinoplus2 через -serial pty. Живых мостов и плат под рукой
не было: всё, что зависит от железа, помечено.
Как это называется в документации
Оригинал Перевод USB-to-UART bridge мост USB–UART, переходник virtual COM port (VCP) виртуальный COM-порт CDC ACM класс USB «последовательный порт» DTR (Data Terminal Ready), RTS (Request To Send) сигналы управления: «терминал готов», «запрос на передачу» local echo местное эхо: терминал сам показывает набранное line ending: CR, LF, CRLF конец строки: 0x0D, 0x0A или оба подряд SET_LINE_CODING, SET_CONTROL_LINE_STATE запросы CDC: формат линии; состояние DTR и RTS round-trip latency круговая задержка: от отправки вопроса до ответа exclusive access монопольный доступ к порту
115б.1 Кто стоит между прибором и программой
Путей три, и для программы они выглядят одинаково — как последовательный порт:
- Мост USB–UART — отдельная микросхема на переходнике (раздел 16.7) или
на плате: USART камня идёт на неё, она — в USB. Мосту нужен свой драйвер.
В правилах udev PlatformIO: CH340 —
1a86:7523, CP210x —10c4:ea60, FT232 —0403:6001, PL2303 —067b:2303. Драйвер CH340 для Windows этой машины —CH341SER.INFверсии 3.9 от WCH. - Виртуальный порт отладчика. ST-LINK на Nucleo — составное устройство:
отладка и порт в одном кабеле. В
stlink_vcp.infпорт — отдельный интерфейс:0483:374Aи0483:374B— интерфейс 2,0483:374C— интерфейс 1. У NUCLEO-F103RB CubeMX заранее отдаёт USART2 на PA2 и PA3 с меткамиUSART_TXиUSART_RX: по ним плата и говорит с отладчиком. - USB CDC самого STM32 (раздел 20.7) — моста нет, порт — это прошивка.
В примерах ST —
0483:5740, в правилах PlatformIO он так и подписан: «STM32 virtual COM port».
Два последних — класс CDC ACM, и драйвер у них один. stlink_vcp.inf и
stmcdc.inf от ST не несут своего драйвера: оба лишь указывают на
usbser.sys. А usbser.inf самой Windows (на этой машине — версия
10.0.26100) сам подхватывает любое устройство класса 02, подкласса 02
(USB\Class_02&SubClass_02) как «USB Serial Device». В ядре Linux это
разные драйверы: для мостов — CONFIG_USB_SERIAL_CH341, _CP210X,
_FTDI_SIO, _PL2303, для CDC — CONFIG_USB_ACM; в ядре Debian 6.1 все
они собраны модулями.
115б.2 Имена портов: COMn, /dev/ttyUSB, /dev/ttyACM
Windows даёт порту номер COMn. pyserial перечисляет устройства
классов «Ports» и «Modem» — все четыре INF выше ставят устройство в класс
Ports. Для COM1…COM8 имя передаётся как есть, начиная с COM9 pyserial
сам дописывает \\.\ — так требует Windows (serialwin32.py).
Linux называет порт по драйверу. pyserial ищет порты по шаблонам
(list_ports_linux.py), главные из них:
| Имя | Что это |
|---|---|
/dev/ttyS* |
встроенный порт |
/dev/ttyUSB* |
мост со своим драйвером: CH340, CP210x, FT232, PL2303 |
/dev/ttyACM* |
класс CDC ACM: порт ST-LINK, USB CDC самого STM32 |
/dev/ttyAMA* |
встроенный порт ARM (Raspberry Pi) |
/dev/rfcomm* |
порт поверх Bluetooth |
Номер в имени — порядок подключения, а не разъём: второй переходник станет
ttyUSB1. Постоянное имя дают символические ссылки udev в
/dev/serial/by-id; list_ports.comports(include_links=True) добавляет
к списку и ссылки на найденные порты. Открыть порт мешает чаще всего не
имя, а права: порты принадлежат группе dialout (раздел 194.5).
Для USB-порта pyserial собирает описание из дескрипторов. hwid строится
так (list_ports_common.py), для подставленных в опыте значений:
USB VID:PID=0483:374B SER=066DFF LOCATION=1-1.2:1.2
Выбирать порт по номеру — лотерея, по VID:PID — надёжно:
def find_port(vid, pid):
"""Первый порт с нужными VID:PID — имя COMn или /dev/tty… не важно."""
for p in list_ports.comports():
if (p.vid, p.pid) == (vid, pid):
return p.device
return None
На подставленном списке функция нашла /dev/ttyACM0 по 0483:374B,
/dev/ttyUSB0 по 1a86:7523 и вернула None для FT232, которого в списке
не было. Два одинаковых переходника различают по serial_number —
если у моста он есть.
На Windows-машине, где писался раздел, pyserial нашёл 7 портов, COM3…COM10
— и ни одного кабеля: все семь — «Standard Serial over Bluetooth link»,
vid у них пустой. Порт «наугад» здесь почти наверняка чужой.
115б.3 Терминал: CR и LF, местное эхо, шестнадцатеричный вид
Терминал — программа, которая шлёт в порт нажатые клавиши и печатает
принятое. У всех терминалов три одинаковые настройки, и все три ломают
разговор с прибором. Опыт — miniterm из pyserial, настоящий терминал,
которому клавиши «нажимались» через псевдотерминал. На том конце — прошивка
в QEMU: она не повторяет принятое, а на строку отвечает ok <текст> rx=<байты
строки>. Прошивка собрана в двух вариантах: конец строки только \n, как
cdc_line в разделе 20.7, и «\r или \n, пустые строки пропустить», как
консоль термостата в разделе 196.
Конец строки. Клавиша Enter уходит в порт как CR, LF или оба —
по настройке терминала; у miniterm по умолчанию CRLF. Набрано get и Enter:
| Терминал шлёт | Прошивка «конец — LF» | Прошивка «CR или LF» |
|---|---|---|
| CR | молчит | 67 65 74 0D |
| LF | 67 65 74 0A |
67 65 74 0A |
| CRLF | 67 65 74 0D 0A |
67 65 74 0D |
Прошивке «только LF» терминал с настройкой CR не дождётся ответа никогда:
строка так и не кончилась. Прошивка «CR или LF» отвечает на всё, а 0A
после 0D принимает за пустую строку и пропускает. Отсюда правило: прибор
принимает любой конец строки, а сам шлёт \r\n — его понимают все
терминалы.
Местное эхо. Прибор принятое не повторяет, поэтому набранное get на
экране не видно — только ответ ok get rx=67 65 74 0A. С ключом --echo
терминал сам печатает набранное, и на экране get, а под ним ответ.
Если прибор и сам отвечает эхом, а в терминале включено местное эхо,
каждая буква удвоится.
Шестнадцатеричный вид. Для двоичного протокола текстовый вид бесполезен:
0x00, 0x0D и байты старше 0x7F на экране не различить. С ключом
--encoding hexlify miniterm и принятое показывает, и набранное понимает
в шестнадцатеричном виде: набрано 70 69 6E 67 0A — ушло ping и LF,
на экране ответ 70 6F 6E 67 0D 0A, то есть pong\r\n. Для отладки
есть и фильтр --filter debug: он подписывает каждую порцию [TX:…] и [RX:…].
Выход из miniterm — Ctrl+], меню — Ctrl+T.
115б.4 DTR и RTS: почему плата перезагружается при открытии порта
Кроме TX и RX у порта есть сигналы управления, оставшиеся от модемов:
DTR — «терминал готов», RTS — «запрос на передачу». Мост выводит их на
свои ножки. В USB CDC они приходят прибору запросом
SET_CONTROL_LINE_STATE (0x22): бит 0 — DTR, бит 1 — RTS (заголовок
cdc.h ядра Linux). В шаблоне ST usbd_cdc_if_template.c этот запрос
пустой — «Add your code here»: прошивка может, например, не слать ничего,
пока DTR не поднят, то есть пока порт никто не открыл.
Плата перезагружается при открытии порта, если DTR или RTS заведены на
сброс, — так делают платы с автоматическим входом в загрузчик. А pyserial
при открытии поднимает оба: в serialutil.py начальное состояние DTR и
RTS — True, и open сразу выставляет его. В опыте перехвачен вызов,
который pyserial делает при открытии:
| Как открыли | Вызов | Состояние после |
|---|---|---|
serial.Serial(port, 115200) |
TIOCMBIS, DTR — поднять |
DTR и RTS — True |
open_quiet(port) |
TIOCMBIC, DTR — опустить |
DTR и RTS — False |
На виртуальной паре проводов DTR нет: вызов возвращает ENOTTY, pyserial
эту ошибку молча пропускает, и до RTS дело не доходит. На настоящем порту
за DTR следует RTS. Чтобы не дёргать линии, их состояние задают до
открытия:
def open_quiet(port, baud=115200):
"""Открыть порт, не поднимая DTR и RTS: плата с автосбросом не перезагрузится."""
s = serial.Serial()
s.port, s.baudrate, s.timeout = port, baud, 1
s.dtr = False
s.rts = False
s.open()
return s
В miniterm то же — ключи --dtr 0 --rts 0; по ходу работы линии
переключают Ctrl+T Ctrl+D (DTR) и Ctrl+T Ctrl+R (RTS). Линии может трогать не только
программа: в CH341SER.INF есть закомментированная строка, которая ставит
на порт фильтр Windows serenum. По умолчанию WCH его не включает; что
этот фильтр делает с DTR и RTS, здесь не проверено.
115б.5 Задержка: кадры USB, такт моста, ответ пачкой
Круговая задержка — от записи вопроса до прихода ответа. Сам UART здесь
почти ни при чём: ответ pong\r\n — 6 байт, на 115 200 бод это полмиллисекунды.
Время съедают звенья посередине:
- кадры USB. Хост отмечает начало кадра раз в миллисекунду и опрашивает устройство по своему расписанию (раздел 20.2 и 20.9) — устройство само начать передачу не может;
- такт моста. Мост копит принятое из UART и отдаёт компьютеру пачкой.
В примере ST CDC_Standalone для F1, где STM32 сам работает мостом
USB–UART, принятое из UART уходит в USB по таймеру раз в
CDC_POLLING_INTERVAL= 5 мс (допустимо от 1 до 65). У мостов других производителей свои таймеры в драйвере; их значения здесь не проверены.
Опыт — модель на виртуальной паре: «прибор» отвечает pong на ping
мгновенно, но ответ уходит в порт только по такту, как у моста ST.
Программа — pyserial, время — time.perf_counter:
def ask(s, cmd):
"""Команда и ответ одной строкой; время круга — по perf_counter."""
t0 = time.perf_counter()
s.write(cmd + b"\n")
reply = s.readline()
return reply, time.perf_counter() - t0
| Модель | Вопрос и ответ, медиана | 200 обменов подряд | 100 вопросов одной записью | Чтений на 100 ответов |
|---|---|---|---|---|
| без такта | 0,065 мс | 14,255 мс | 0,141 мс | 1 |
| такт 1 мс | 0,998 мс | 199,722 мс | 0,999 мс | 1 |
| такт 5 мс | 4,999 мс | 999,735 мс | 5,007 мс | 1 |
Прошивка в QEMU отвечала на ping за 0,202 мс (медиана из 200). Это не
USB: псевдотерминал и QEMU — самые быстрые звенья, какие бывают, и
показывают только, что задержку добавляет такт, а не программа.
Вывод из таблицы: каждый обмен «вопрос — ответ» стоит целый такт. Ответ готов сразу, но ждёт ближайшего такта, а следующий вопрос уходит сразу после ответа — и снова ждёт полный такт. 200 вопросов подряд при такте 5 мс — секунда, а 100 вопросов одной записью — один такт, и все 100 ответов пришли одним чтением. Отсюда два правила:
- протокол строят так, чтобы вопросов было мало: один запрос «дай всё» вместо двадцати «дай параметр N», поток телеметрии без вопросов;
- ответы приходят пачкой — несколько кадров в одном чтении и половина кадра в другом; разборщик обязан собирать кадры из потока (разделы 194.4 и 189.6).
115б.6 USB CDC: скорость «не важна», порт пропадает при сбросе
Скорость. У порта USB CDC самого STM32 нет UART — байты идут
пакетами USB до 64 байт (CDC_DATA_FS_MAX_PACKET_SIZE, раздел 20.7), и
число бод ни на что не влияет. Виртуальная пара ведёт себя так же: 1000 байт
туда и обратно при «9600» — 0,109 мс, при «115200» — 0,106 мс. Настоящий
UART на 9600 бод вёз бы их в одну сторону 1041,667 мс.
Выставленная программой скорость всё же доходит до прибора — запросом
SET_LINE_CODING (0x20): 7 байт, скорость 4 байтами, затем стоп-биты,
чётность и число бит (struct usb_cdc_line_coding в заголовке ядра).
В шаблоне ST скорость просто сохраняется. А в примере CDC_Standalone,
где STM32 работает мостом, по этому запросу перенастраивается UART —
у моста скорость важна, у «чистого» CDC нет.
Порт пропадает при сбросе. Мост и ST-LINK — отдельные микросхемы, и их
порт живёт, пока плата перезагружается. Порт USB CDC — это прошивка:
сброс, перепрошивка, HardFault — и устройство уходит с шины, а порт
исчезает. Программа, державшая его открытым, получает ошибку. В опыте
«прибор» на виртуальной паре остановлен посреди чтения, и pyserial ответил:
device reports readiness to read but returned no data (device disconnected or multiple access on port?)
Вернуться прибор может под другим именем: если старое ещё занято
(программа не закрыла порт), новому дают следующее — ttyACM1 вместо
ttyACM0. Поэтому программу пишут так, чтобы она открывала порт заново по
постоянному имени — ссылке из /dev/serial/by-id или по VID:PID через
find_port:
def read_lines(port, handle):
"""Читать строки; порт пропал — подождать и открыть заново по тому же имени."""
while True:
try:
with serial.Serial(port, 115200, timeout=1) as s:
for line in iter(s.readline, None):
if line and handle(line) is False:
return
except serial.SerialException as e:
print("порт пропал:", e)
time.sleep(0.5)
В опыте прибор слал строку каждые 20 мс и был остановлен; старое имя
в это время держала другая программа, и новый прибор получил другое
/dev/pts/N, но ссылка осталась той же. read_lines получила 15 строк
от первого прибора, одну ошибку и ещё 15 строк от второго.
115б.7 Грабли
Порт открыт, а байты приходят через раз. Порт держит ещё одна программа:
второй терминал, монитор порта в среде, забытый скрипт. В Linux открытие
не монопольное, и две программы делят поток как придётся. В опыте прибор
выдал 1800 байт двум открытым pyserial: одна получила 841 байт, другая 951,
восемь байт не досталось никому, у обеих — по 60 с лишним ошибок с той же
подсказкой «multiple access on port?», что и при пропавшем приборе. Кто
держит порт, покажет fuser или обход /proc:
def who_holds(dev):
"""Кто держит порт открытым — то же делает fuser: обход /proc/*/fd."""
real, pids = os.path.realpath(dev), []
for pid in filter(str.isdigit, os.listdir("/proc")):
try:
fds = os.listdir("/proc/%s/fd" % pid)
if any(os.path.realpath("/proc/%s/fd/%s" % (pid, fd)) == real for fd in fds):
pids.append(int(pid))
except OSError:
pass
return pids
exclusive=True не защитил. pyserial ставит на порт блокировку
flock: второй Serial(port, exclusive=True) в опыте упал с «Could not
exclusively lock port», а третий, открытый без этого ключа, спокойно
открылся. Блокировка работает только между программами, которые её
спрашивают; miniterm, например, спрашивает по умолчанию.
Порт захватывает служба. Кроме своих программ, новый порт в Linux
могут открыть системные службы. Правила udev PlatformIO ставят мостам
CH340, CP210x, FT232 и PL2303, порту STM32 0483:5740 и всем устройствам
ST метку ID_MM_DEVICE_IGNORE — «не трогать» для службы модемов. Второй
вид захвата — консоль и служба входа на порту (раздел 194.5).
Windows: порт не открывается, «Отказано в доступе». Порт уже открыт.
В Windows открытие монопольное: pyserial передаёт CreateFile нулевой
режим совместного доступа. Закрыть терминал или монитор порта в среде,
потом открывать.
COM-портов много, а платы среди них нет. На машине автора семь портов,
и все — Bluetooth. Искать по VID:PID (find_port), а не брать первый.
Плата перезагружается при каждом открытии. DTR или RTS заведены на
сброс, а pyserial их поднимает. Задать dtr и rts до open
(open_quiet) или --dtr 0 --rts 0 в miniterm.
Команда ушла, ответа нет. Терминал шлёт CR, а прошивка ждёт LF.
Прошивка должна принимать и то и другое, как консоль в разделе 196.
Не видно, что печатаешь, или каждая буква дважды. Местное эхо выключено при молчащем приборе — или включено при приборе с эхом.
Опрос из 200 вопросов идёт секунду. Каждый обмен стоит такт моста или кадр USB. Спрашивать пачкой, слать телеметрию потоком.
После перепрошивки программа падает с ошибкой порта. Порт USB CDC
исчез вместе с прибором и вернулся, возможно, под другим именем.
Переоткрывать по постоянному имени (read_lines).
«Permission denied» в Linux. Пользователь не в группе dialout
(раздел 194.5).
115б.8 Итоги
- Три пути к одному «порту»: мост USB–UART (CH340
1a86:7523, CP210x10c4:ea60, FT2320403:6001, PL2303067b:2303) со своим драйвером, порт ST-LINK (0483:374A,374B,374C) и USB CDC самого STM32 (0483:5740) — оба черезusbser.sysв Windows. - Имена:
COMnв Windows (сCOM9—\\.\COM9),/dev/ttyUSB*для мостов и/dev/ttyACM*для CDC в Linux. Номер — порядок подключения; искать по VID:PID. На машине автора 7 COM-портов, и все — Bluetooth. - Терминал: Enter уходит как CR, LF или CRLF (у miniterm — CRLF); прошивка «только LF» на CR молчит, прошивка «CR или LF» отвечает всем. Местное эхо и шестнадцатеричный вид — отдельные настройки.
- pyserial при открытии поднимает DTR и RTS; плата со сбросом на них
перезагрузится.
dtr = False,rts = False— доopen. - Каждый обмен «вопрос — ответ» стоит такт: при такте 5 мс 200 обменов — 999,735 мс, 100 вопросов одной записью — 5,007 мс и одно чтение. Виртуальная пара без такта — 0,065 мс на обмен; это не USB.
- USB CDC: скорость не влияет (1000 байт за 0,109 мс при «9600» против
1041,667 мс у UART), но доходит до прибора запросом
SET_LINE_CODING. Порт пропадает при сбросе и может вернуться под другим именем. - В Linux порт может читать кто угодно ещё — байты делятся (841 и 951 из
1800);
exclusive=Trueзащищает только от тех, кто тоже его ставит. В Windows порт монопольный.
Дальше: раздел 115в «Python рядом с прибором: порт, байты, журнал, скрипт-испытатель» — на pyserial пишется испытатель, который гоняет прибор по сценарию и ведёт журнал обмена.