CH32V003.USART

Избитая тема.

USART в основном используется для передачи данных между устройствами — между двумя микроконтроллерами или между микроконтроллером и компом. USART позволяет как передавать, так и принимать данные.

Данные можно передавать непрерывным потоком (словно вода из крана), а можно передавать в виде отдельных пакетов. В последнем случае пакеты удобно снабжать контрольной суммой, которая гарантирует отличие «битых» пакетов, от «не битых». Я в своей практической деятельности использую преимущественно пакетную передачу. (Скорее даже так — я что-то не припомню случая, когда бы слал непрерывный поток байтов.)

Когда вы отправляете пакет по USART, то, разумеется, вы уже знаете его длину. Это обстоятельство тоже даёт определённые преимущества, но о них поговорим чуть ниже.

При приёме данных (приёме пакетов) не всегда бывает так, что все-все-все пакеты имеют одинаковую длину. Одинаковая длина пакетов — это скорее исключение, чем правило. Во всяком случае у меня это так. (Ну сколько есть разработчиков, столько будет и мнений.)

Конечно, длина принимаемого пакета, так или иначе становится известной, после какого-то времени после начала приёма. Я стараюсь указывать длину пакетов в самом начале. Это удобно с точки зрения построения программного обеспечения на модульном принципе.

Ну, есть, допустим, программный модуль (назовём его драйвером) последовательного порта и есть модуль, который получает информацию. Драйвер не вмешивается в содержимое пакета, ему вообще пофигу, что там лежит. В задачу драйвера входит принять пакет, посчитать у него контрольную сумму. И, если сумма сходится, то откинуть заголовок пакета (Это может быть адрес назначения, длина пакета, стартовая последовательность байт и тому подобное), откинуть оконцовку пакета (контрольную сумму), а оставшуюся часть (говорят — «полезная загрузка») отдать модулю, который будет обрабатывать эти данные.

Недавно я познакомился с «разработчиками», которые забили на это дело. У них в проекте тоже происходит передача данных с помощью пакетов. Но как таковой длины пакетов у них нет. Начало пакетов они определяют по уникальной последовательности их трёх байт. Ага, ага! Уникальная. Они думают, что такой последовательности не встретится в полезной загрузке. (А там передаются данные в двоичном виде.) Конец пакета они определяют по паузе…

Ну, почему — по паузе? Да просто потому, что для передачи и приёма пакетов у них используется DMA.

Почему DMA? — Да потому, что проект пишется на HAL-е. А HAL сильно ограничивает возможности.

Нет, на HAL-e, конечно, можно писать программы. Но всегда будет получаться что-то типа аля-Ардуино. Всегда дубово, кондово и глюкаво. Но что не сделаешь, лишь бы не читать документацию, не изучать регистры и биты!

Короче, под HAL-ом есть три способа передачи данных по USART:

блокирующий,
по прерываниям,
с использованием DMA.

Первый способ (блокирующий) не проходит по тому, что для отправки или приёма пакетов микроконтроллер ничего более делать уже не сможет. То есть задача вообще никак не решается.

Второй способ (по прерываниям) — он, в общем-то, не плохой. Процессорного времени занимает мало. Но по причине уродливости архитектуры HAL-a при приёме и при передаче происходит столько вложенных вызовов функций (так называемых — колбэков (callback)), то те, кто сам ковырялся в этих кишках (а не прослушал «краткий курс программиста микроконтроллеров» по книжку Кармина Новиэлло), и кто имеет альтернативный опыт написания программ для микроконтроллеров, у того волосы встают дыбом даже на голове. Ну его нахрен, эту Ардуину!

Третий способ (DMA) — он ещё более весёлый.

Во первых, Этот способ вообще не вписывается в концепцию HAL.

Ну, как не вписывается? Концепция HAL — это абстрагирование уровня, на котором должны создаваться программы, от специфики железа. Мне много раз тыкали догмами, что благодаря HAL-у можно легко перенести проект с одного типа микроконтроллера на другой. (Разумеется, всё в рамках STM32.) Ну, допустим.

Но ведь семейства микроконтроллеров очень разные. У них разное построение периферийных устройств. Ну, ладно, допустим, порты (GPIO) STM32F1xx и STM32F0xx ещё как-то можно подбить под общую схему работы с ними. А что делать с тем же USART?

В STM32F1xx и STM32F3xx модули USART-ов разные. Не сильно, но разные. Для традиционных задач уровня аля-Ардуино, их (USART-ы) можно как-то объединить под общее начало. А более изощрённых задач?

То есть получается так, что для того чтобы концепция HAL заработала (единое начало для всех микроконтроллеров), придётся пожертвовать самым «вкусным», что имеется в новых семействах микроконтроллеров.

Ну, давайте конкретно! В STM32F303 есть возможность настроить его (то есть встроенный в USART) таймер на длительность паузы в приёме данных. Конфетка, ведь! Было бы самое то, для той команды разработчиков, о которой я говорю. Но HAL это предусматривает… Ну и чём тут можно говорить?

Поскольку USART-ы довольно-таки разные, пришлось сломать красоту концепции HAL. Чтобы окучивать периферийные устройства с разными возможностями, ST Microelectronics сделала театральную физиономию и отошла от своей концепции. ST добавила в свое ПО «расширенные» файлы и функции. Вы их легко узнаете по добавочным двум буквам («Ex») в их названиях. ST сказала, что поскольку этот зоопарк невозможно без ущерба объединить под одним началом, то, вот, вам расширенные функции, которые позволят работать с уникальными возможностями периферии, НО! Но мы, типа, не гарантируем переносимость созданного вами ПО на другие семейства (STM32), ибо у других семейств не факт, что будут примерно такие же расширенные функции и файлы. А у старых семейств их точно не будет. Ну, зашибись, чо!

Ведь, если ты лидер, то ведь не важно в каком направлении ты бежишь. Толпа баранов бежит не с какой-то свой целью (спасаются от волка, бегут в направлении полянки с сочной травой). Толпа баранов тупо бежит за лидером и ни о чём не думает. Потому что бараны. Поэтому им пофигу, куда их ведёт козёл-провокатор. Главное — не думать и чётко следовать за лидером.

Ну, ладно. Проехали! Но самое-то печальное, что даже при наличие расширенных файлов и функций, в частности, для STM32F303 нет возможности в HAL-е использовать этот таймер. Есть единственный способ пользоваться возможностями таймера. Для этого нужно выйти за рамки HAL-а, и напрямую общаться с регистрами. Но тогда возникает вопрос — а в чём тогда прикол необходимости использовать HAL?

Во вторых (это всё ещё про третий способ передачи данных, с помощью DMA), если передать по DMA пакет данных ещё можно (так как длина передаваемого пакета так или иначе известна), то как вы будет принимать пакеты с заранее неизвестной длиной?

Какого размер (длину пакета) нужно указать при вызове функции?

Предположим, что мы ожидаем получать пакеты, размер которых составляет от 10 до 100 байт. (Цифры взяты примерно.) Тогда можно создать буфер с максимальный пакет и этот размер указать при вызове функции. И будет счастье?

Хрен там!

Счастья не будет. С HAL-ом может быть только счастье уровня аля-Ардуино.

При приёме небольших пакетов от 10 до 50 байт всё будет более-менее хорошо. Ну, разве что будут не очень большие проблемы со временем обнаружения, что пакет принят. Когда пакеты поступают относительно редко, это не проблема. Но если пакеты сыплются как градины — ну-у-у… продолжайте дальше грызть кактус. Кто ж вам доктор!

А вот если начал поступать большой пакет (от 50 до 100), то… я уже не могу сдержать своё ядовитое хи-хикание. Система, построенная на HAL-е сообщит вам, что она приняла пакет в 50 байт…

Э-э… что?

А где остальное?

А остальное — оно тоже будет получено, но только нужно немного подождать. HAL настраивает DMA, чтобы тот реагировал не на заполнение буфера целиком, а на заполнение ровно половины.

А-а? Что?

Это не глюк. Это особенность. И это прописано в кишках HAL-а. Если хотите изменить поведение DMA, вам нужно самостоятельно найти этот файл, найти это место и справить.

Ха! Тоже мне — проблема!

А вот и проблема! Проблема, для тех, кто ардуинщик, но взялся за серьёзный проект, не за проект уровня Ардуино. Ну, кто ж им доктор-то. Судьба у них такое.

Ладно. а мы с вами идем дальше.

Так вот. Я вернусь к своему видению, как следует передавать и принимать пакеты данных. Как я уже говорил, в самом начале, передавать данные можно как с использованием прерывания, так и с использованием DMA.

Оба способа не забирают много процессорного времени и очень хорошо вписываются в парадигму FSM (Finit State Machine, машина конечных состояний) и много поточных программ, потоки которых организованы программным способом (то есть в пользовательском пространстве, без использования RTOS).

Приведу конкретные цифры, полученные на практике. Так, например, для передачи пакета из 10 байт на скорости 115200 Бод, на саму передачу тратится 868 мкс. Если бы мы использовали блокирующую передачу, то на это время микроконтроллер бы ничего не смог делать. Одна миллисекунда — это довольно-таки большое количество времени.

Если программу строить на передачу пакета по-байтно в прерываниях, то затраты процессорного времени будут состоять из времени инициализации передачи пакета и времени работы процессора в прерывании для загрузки очередного байта из внутреннего буфера драйвера в регистр данных UASRT.

В моей тестовой программе функция usart_send() сначала проверяет свободен ли передатчик, затем оценивает размер передаваемого пакета (поместится ли он во внутренний буфер драйвера), изменяет некоторые флаги и переменные в драйвере и наконец поднимает нужный бит в регистре управления USART чтобы передача началась. На всё про всё уходит 11 мкс. Это на микроконтроллере CH32V003, тактовая частота 24 МГц.

Количество прерываний будет равно количеству передаваемых байт (которые загружаются из буфера в регистр данных), и плюс ещё одно завершающее прерывание, которое останавливает USART.

Итого:

11 мкс + 10 (байт) * 1.8 мкс + 1.6 мкс = 30.6 мкс

Это примерно 3.5% времени передачи всего пакета.

Если же передавать пакет с помощью DMA, то процессорное время будет затрачено только дважды. Первый раз при инициализации передачи, и второй раз по окончании. Функция инициализации передачи usart_send() занимается примерно тем же самым, но только ещё дёргает DMA. И, как это ни странно, в этом случае функция работает всего 8 мкс. То есть даже ещё меньше, чем в первом случае.

По окончании посылки пакета DMA вызывает прерывание. Длительность прерывания составляет 0.8 мкс.

Всего затрат времени получается 8.8 мкс. Это примерно 1 % времени от передачи пакета.

Но есть нюансы!

Во первых, я должен отметить, что программы не совсем идентичны в плане определения времени об окончании передачи. В первой программе последнее прерывание происходит в момент полного окончания передачи последнего байта пакета.

Во второй программе, прерывание от DMA, возникает в момент исчерпания байтов во внутреннем буфере драйвера. Но при этом в самом USART-е остаются ещё два байта. Один байт находится в последовательном регистре и бит за битом выталкивается наружу. А второй байт находится в буфере данных, и как только последовательный буфер освободится, этот байт тут же в него провалится и тоже будет бит за битом выводится наружу. В общем получается так, что после возникновения прерывания USART ещё примерно 176 мкс передаёт эти два байта.

В принципе это не страшно, после возникновения прерывания от DMA можно почти сразу же отправлять следующий пакет. Но если хотите безупречной аккуратности кода, то нужно активировать прерывание от USART по завершению передачи, как в пером случае. С точки зрения затрат времени процессора это ничего не меняет.

Теперь скажу ещё несколько слов по приёму пакетов.

Пакеты, с моей точки зрения следует принимать по-байтно и с помощью прерываний. Способ с использованием DMA здесь не годится. Как я уже говорил, пакеты могут иметь разную длину, а для правильно работы DMA нам сразу нужно задать сколько байт принимать.

Поэтому принимает по-байтно. В одном из первых байтов пакета я обычно указываю длину пакета. Таким образом в процессе приёма следующих байтов драйвер уже знает, сколько ещё нужно принимать. По завершению набора байтов пакета приёмник USART отключается, а набранный в приёмном буфере (драйвера последовательного порта) пакет подвергается анализу.

Первое, что надо сделать — это оценить размер пакета. Пакеты а принципе не могут быть меньше какой-то определённой длины, и не могут быть больше размера буфера. Значения вы должны задавать сами на основании своих предположения. Если пакет не укладывается в заданные рамки, то пакет однозначно «битый» или мы поймали помеху на линии.

Далее следует определить целостность пакета. Это осуществляется с помощью подсчёта контрольной суммы. Сумма не сходится — пакет «битый». Битые пакеты безжалостно откидываем, как будто их и не было вовсе.

Теперь скажу ещё за байт-стаффинг. Это, в общем, такая хрень, которая беспардонно увеличивает размер пакета, но зато однозначно снимает проблему «слипшихся» во времени пакетов. Пакеты могут «слипаться», когда обмен происходит с компом.

Ну вот, прикиньте, что комп отправляет два пакета друг за другом. Первый пакет лежит в буфере драйвера последовательного порта (компа) и уже происходит его передача. Комп быстрый. Во всяком случае намного быстрей, чем длится передача пакета по RS232. И вполне может возникнуть ситуация, когда к первому пакету в драйвере добавится второй. И тогда эти оба пакеты будут выплюнуты наружу без паузы между ними.

Почем без паузы?

Да потому, что последовательный порт — это инструмент из арсенала потоковой передачи байтов. В понятия последовательного порта не входят ни начало пакета, ни конец пакета. Последовательный порт — это тупой приёмопередатчик. Если есть что передавать — он будет передавать. Только успевай подсыпать ему! Никаких пауз он не соблюдает. Поэтому два и более пакетов могут, если ничего специально не делать, слипнуться, как дважды два.

Микроконтроллер, разумеется, примет первый из пакетов. Примет, скорее всего, успешно. А вот успеет ли он включить свой приёмник к началу поступления второго — это ещё вопрос. Скорее всего — нет. Поэтому, если программа (которая работает на компе) как-то специально не притормаживается, то она вам «налепит пельменей». Ну либо делайте байт-стаффинг.

При передаче от микроконтроллера в комп, тоже могут происходить странности. Ни Линукс, ни тем более Шиндовс не являются системами реального времени.

Что это значит? Это значит то, что время реакции на внешние события к них непредсказуемое. Другими словами, допустим, мы начали передавать в комп какой-то пакет. А комп возьми, да и отвлёкся на какие-то свои системные дела. Кэш, например, ему срочно понадобилось сбросить на диск или ещё что. Ну, про подновление Винды я уж не буду…

А мы ведь не знаем, что делает комп. Мы отправляем ему следующий пакет. Тем не менее оба пакета будут приняты компом и сохранены в буфере драйвера последовательного порта. Но при этом они окажутся «слипшимися». Ну и как вы будете разлеплять эти «пельмени»?

Я бы наверно мог ещё пофилософствовать на тему передачи данных. Но я уже и без того написал достаточно много. По нынешним временам такие длинные тесты читать довольно-таки затруднительно. Народ (а я тоже народ!) предпочитает короткие тексты. Копать глубоко и читать — нынче моветон. Нынче модно гуглить библиотеки и «склеивать» в своей программе куски чужого кода, не вдаваясь в то, что они внутри себя там делают. И это считается разработкой? Разбудите меня.

4 responses to “CH32V003.USART

  1. Полностью согласен. Протоколы надо стараться делать вида запрос-ответ с CRC16 хотя бы. Флага USART IDLE в stm32/ch32 вполне достаточно для разделения пакетов. Даже промку на нем делают. Например, индикатор Овен СМИ2-М по умолчанию в качестве таймаута Modbus использует IDLE, но можно настроить и на 3,5T. А по поводу, что времени IDLE иногда не хватает, т.к. комп тормозит — все зависит от длины пакета и размера буфера в переходнике usb-uart.

  2. По поводу сочетания 3 байт для начала передачи пакета, если данные в ASCII, например в JSON, то можно вообще одним байтом кодировать начало передачи, а другим конец передачи, используя не текстовые коды, например 0x1 и 0x2, которые не встретятся в самих данных, а целостность пакета можно определить по валидности JSON. Уже видел такой способ передачи данных, он понравился больше всего из тех, с которыми приходится работать.

    А не нравится способ, где нет отметки начала пакета, а сразу передаётся длина пакета, хоть там и используется CRC16, и маловероятно получить битый пакет с правильным CRC, но в условиях помех не понятно где начинают идти сами данные, и в итоге теряем их среди мусора.

    • Не-не! В том то и дело, что там при передаче используется не ASCII-строки, а двоичные данные. Что меня, собственно, и напрягла. Ну да ладно! Криво-косо, но, надо признать, как-то оно работает же. Ну не идеалисты там разработчики, Ну гонят вот такое грязное решение. Ну, может быть когда-нибудь и выстрелит не в том направлении, ну может быть пациент когда и помрэ на аппарате. Да кого это волнует! Главное ж работает! (Это был сарказм. Ну, вот такие у нас специалисты. Поэтому ИВЛ и горят синим пламенем.)

    • >если данные в ASCII … например 0x1 и 0x2

      Не стоит так делать. ASCII передача подразумевает возможность просмотр данных в любой программы, с любого устройства или даже прямой вывод на принтер. Это особенно важно, когда железка начала сбоить где-то в поле за тысячи км, а подключиться может только местный наладчик уровня «ещё учится в колледже»

Оставьте комментарий