Когда мне предстоит начать работу с новым микроконтроллером, я обычно гляжу, а какое у него быстродействие GPIO. Сколько тактов на одну запись уходит по факту. Такая у меня традиция. Было дело, я так выяснил, что китайские клоны STM32 работают с GPIO чуть быстрее, чем оригинал. Для дешёвых контроллеров обычно ничего более интересного такие проверки не выявляют, но традиция есть традиция. Не изменял я ей и при начале освоения CH32x035 на базе RISC-V. И вот для него картинки получились такими интересными, что я решил поделиться ими с общественностью. Не то, чтобы там было что-то революционное, но от привычных мне они точно отличаются.
А ещё я добавлю к ним немного выводов… И нутром чую, что в комментариях мне объяснят, что я понимаю всё неправильно, а на самом деле… Но я буду только рад обоснованным высказываниям. Вместе мы установим истину.
Немного матчасти
Когда я рассказывал про свои выводы коллегам, каждый считал своим долгом сказать: «Забудь про такты, в современных контроллерах всё настолько сложно, что такты пальцем считать бессмысленно». Они, конечно, правы… Но не совсем. Вот было дело, играл я в Cortex A9 в составе ПЛИС Cyclone V-SoC, вот там был бардак полный. Куча шин. Между ними мосты с буферами на 7 транзакций. На разных шинах свои кэши… Вот там – да. Там от запуска к запуску получить дважды один результат по тактам не получалось. Здесь же система намного проще. Но давайте сначала разберёмся, что же у нас имеется. Чего мы вправе ждать от неё.
Контроллер CH32x035 является идеологическим наследником STM32. Например, когда мне что-то в китайской документации на его периферию непонятно, я открываю документ на фирменный STM32F103 и гляжу рисунки или описания в нём. Понятно, что это уже не клон, в котором просто поменяли процессорное ядро. Но ноги у идеологии растут именно из STM32. А раз идея железа унаследована, то и идея описания — тоже. Так что на него есть пара документов: Reference Manual и Datasheet. Скачать их можно тут: https://www.wch-ic.com/products/CH32X035.html
Как ST-шники не описывают общесистемные вещи в аналогичной паре документов (да хоть SysTick), так и здесь, для общесистемных вещей производители требуют обращаться к документу на ядро. Говорят, мол, ищите QingKeV4_Processor_Manual. Гугль находит его тут: https://www.wch-ic.com/downloads/QingKeV4_Processor_Manual_PDF.html
И вот давайте я сделаю выжимку ключевых моментов. Первое. У нас нет никакого кэша. Сие следует из цепочки фактов, которые разбросаны сразу по двум документам:
И второе – нам обещают наличие предсказателя ветвлений. Как в описании ядра:
Так и в даташите:
Дальше стоит отметить, что у процессора тактовая частота 48 МГц, а шина AHB настроена на работу с коэффициентом деления 1 относительно системной шины. В общем, по умолчанию всё работает на частоте 48 МГц.
И вот теперь, с этими знаниями, поехали!
Тест последовательной записи
Собственно, у меня есть три любимых теста. Первый – это когда программа просто пишет в порты. Смотрим, какое время у обращения к шине на запись. Чаще всего, на запись обращение быстрее, так как программа не должна пользоваться результатом транзакции. Она просто инициировала её и начала исполнение следующей инструкции. Вот и давайте проверим. Возьмём вот такой код:
Этот код пишет то в Bit Set Register, то в Bit Clear Register. Мы то взводим, то роняем бит в порту. На ассемблере получается вот такая цепочка команд:
Смотрим осциллограмму на порту A0. Так как программная поддержка осциллографа наша собственная, я точно знаю, что график соединяет точки, пришедшие из прибора, никаких искажений, вызванных функцией sin(x)/x на высоких частотах… Но тем не менее, что-то тут не так.
Мы видим типичные группы из четырёх единиц и четырёх нулей. Давайте дальше я буду показывать всегда одну группу, подписывая сверху число тактов, соответствующих периоду того или иного её элемента. Итак:
Причина любви к определённому уровню
Мы видим, что система любит определённый уровень. Я долго думал, она больше любит единицы, поэтому работает в этом состоянии быстрее, или нули, поэтому дольше находится в этом состоянии. Каждый может выбрать свой вариант термина, но несимметричность работы очевидна. Почему? К счастью, мне довелось поработать с синтезируемым ядром ZPU, поэтому я знаю наиболее вероятную причину. Дело в том, что у нас ядро имеет в своей формуле буку «C». В оригинале это Compressed. Оно поддерживает команды… Как назвать их по-русски? Сжатые? А кто и как их сжал? Давайте я буду называть их компактными. Их длина – всего 16 бит. Давайте ещё раз посмотрим на ассемблерный код:
Процессор делает выборку сразу 32-битного слова. Поэтому во внутренний регистр попадают сразу две компактные команды. И вторая на декодирование будет подана из этого внутреннего регистра, без дополнительной выборки из памяти.
Давайте проверять гипотезу. Давайте предположим, что выборка идёт, когда ножка находится в состоянии 0. Пока идёт выборка, состояние ножки не меняется. Как там всё бегает по конвейеру – я не знаю, но знаю, что для зануления ножки выборка не нужна. Поэтому в единице мы находимся меньше. Вроде, всё сходится.
Давайте сместим команды в памяти на 16 бит, добавив NOP перед циклом:
Убеждаемся, что ассемблерный код действительно сдвинулся на 2 байта (смотрим на адреса в предыдущем и вот таком листинге):
И получаем предсказуемую осциллограмму. Как переменчива любовь нашего контроллера!
Итак, первый важный вывод: В данном режиме (ниже мы дойдём до других) следует рассматривать не одиночные команды, а пары команд. В статье всё выглядит красиво, а в жизни пришлось перелопатить массу результатов, где я делал замеры для одиночных команд и результаты не складывались в единую картину. Но тут – просто мы везде будем работать с парами команд, и у нас всё будет хорошо (а уж переключив важный режим, мы сможем и про пары забыть).
Тест времени исполнения регистровой команды
Ну что, теперь можно попытаться изучить, насколько большая задержка возникает при зацикливании. Особенно учитывая то, что нам обещали предсказатель ветвлений, а у нас здесь совершенно предсказуемый безусловный переход. Я уже сейчас вижу, что конвейер ломается, но хотелось бы узнать, насколько. Для этого надо понять, а как долго исполняется одиночная команда, не связанная с записью в шину AHB. Чаще всего – один такт, но лучше проверить.
Итак, давайте добавим между нашими записями в порт пару NOP-ов. Для сокращения числа экранов, занимаемых статьёй, далее я буду размещать сишный и ассемблерный код в виде единого рисунка.
Получаем:
Три такта превратились в семь. То есть, пара NOP-ов добавляет четыре такта. Ориентировочно по 2 на команду. Однако!!! Хорошо, а заменим NOP на команду «регистр-регистр». У нас в коде не используется a0, вот будем его модифицировать…
Осциллограмму не привожу, она ничем не отличается от предыдущей. Четыре такта на пару команд. То есть, два на команду!
Отключаем ускорители
Когда я привёл выкладки коллегам, мне начали советовать выяснить, какой же у меня сейчас режим работы системы предвыборки, да и включены ли предсказатели ветвлений. На самом деле, хороший вопрос! Только нигде в документации не описано, как это можно проверить. Но ум – хорошо, а два сапога – пара! Один дотошный коллега стал рассуждать так: Вот у нас есть строчка в Startup коде:
Ну и что, что CSR с адресом 0xbc0 нигде не описан в найденных документах, но как насчёт просторов сети? И описание нашлось!
У нас ядро QingKeV4, а в описании на QingKeV3 этот порт вполне себе документирован! Да, в нём с тех пор появились какие-то новые биты, но всё равно, даже неполное описание лучше, чем его отсутствие! Приведу небольшой фрагмент оттуда… Кому реально интересно, наверняка скачают тот документ и изучат его целиком… А кому не интересно – всё равно и это-то уже по диагонали читают, нервно спрашивая: «Ну когда уже развязка?»…
По той же причине (разросшийся объём) я скажу, что особый интерес для обзорной статьи, представляет только случай, когда отключено всё. И предвыборка, и предсказатели. Тогда осциллограмма для последнего варианта исходного кода выглядит так:
Всё те же 2 такта на команду, но уже не примерно, а точно два. Что на ветвление стало на 1 такт меньше, так просто он «уполз» на единичное состояние в начале периода.
Так что же, все эти ускорители только вредят? Без них всё работает точно так же, но только равномернее?
Правильный ответ таков: На частоте 48 МГц – да! Но давайте вернём все ускорители на место и посмотрим исходный код настройки тактовых частот…
Латентность доступа к флэш-памяти
Вот такие комментарии мы видим при настройке системы на частоту 48 МГц:
Для частоты 24 МГц латентность по окончании настроек уменьшается до одного такта:
Для меньших частот она вообще зануляется, но давайте посмотрим, что будет на частоте 24 МГц при включённых ускорителях:
Собственно, по наносекундам то же самое, с одной оговоркой. Энергопотребление немного снизилось, ведь оно зависит от частоты. А вот по тактам – один такт на команду! Но перезагрузка конвейера в конце цикла добавляет 3 такта. А в документации на ядро как раз сказано, что глубина конвейера равна трём. Другой вопрос, как быть с предсказателями ветвлений, если они не могут предсказать безусловный переход?
Измерение реальной длительности цикла шины AHB
До сих пор мы осуществляли только запись в порт. Это не совсем честный тест. Ну, если выбросить предмет из окна быстро движущегося транспортного средства, тот будет некоторое время лететь с той же скоростью. Потом он станет мусором, так что не надо в реальности выбрасывать… Достаточно согласиться, что место выброса из окна и место падения на землю не совпадут.
Применительно к нашему случаю, та аналогия позволяет понять, почему осциллограф фиксирует перепады уровня порта не в тот момент, когда программа произвела запись. В большинстве современных систем, процессор инициирует транзакцию записи в шину и тут же начинает исполнять следующую команду, ведь ему же не нужны результаты транзакции!
Я уже упоминал многострадальный Cyclone V-SoC. Там ядро Cortex A9 работает на частоте 900 мегагерц! Чтобы запись в медленные шины не тормозила процессорное ядро, там имеется целых 7 буферов записи (грубо можно считать это чем-то типа очереди запросов, в более привычных для программистов терминах - FIFO). Шансы, что программа только и делает, что пишет в ту медленную шину, невелики, поэтому записи для программы почти всегда будут выглядеть моментальными. А вот если начать только и делать, что быстро-быстро писать, то восьмая запись заблокирует процессор до тех пор, пока не закончится самая первая из активных транзакций шины. По крайней мере, я сначала получил результат экспериментально, потом уже нашёл сущность в документации. Насчёт цифры 7 могу путать, давно дело было, всё равно мы не с той системой сейчас работаем, тут важна идея.
А вот при появлении транзакции чтения, всё поменяется. Результат её работы нужен процессору для продолжения исполнения кода! Поэтому он сначала дождётся завершения всех незавершённых транзакций записи, затем – начнёт и дождётся полного завершения транзакции чтения и только после этого перейдёт к исполнению следующей команды.
И вот люблю я пользоваться тем, что данные, попавшие на выход порта, электрически проходят и на вход тоже. Вот фрагмент из документации на GPIO:
Поэтому если при наличии в порту единицы записать в Bit Clear Register содержимое Input Data Register, мы тем самым, произведём сброс текущей ножки, но не при помощи константы из программы, а при помощи той битовой маски, которую считали из порта. Тем самым, мы гарантированно спровоцируем транзакцию чтения. Поехали?
Что за ерунда? Где ещё один перепад? У меня такое ощущение, что транзакция чтения началась до того, как завершилась транзакция записи. Вот и не произошла очистка бита. Отключаем предвыборку (вместо константы 0x1f пишем в CSR 0xbc0 константу 0x1c). В коде ничего не меняем, получаем:
Ура! Не просто всё работает, а чтение производится за столько же тактов, за сколько и запись (2 такта). В STM32 в этом месте аппаратура создаёт охххх, какие тормоза! Но там зато и проблем таких нет. А тут – делаем выводы, что применённые ускорители хороши для вычислений, но не для управления ножками. Что, на самом деле, странно, ведь в контроллерах, в отличие от простых процессоров, важнее предсказуемость. Хорошо, повышаем частоту назад до 48 МГц. Код опять не меняем. Ускорители тоже оставляем выключенными.
Глюков нет, при этом производительность вновь поднялась. Ветвление в наносекундах выполняется столько же, сколько и в случае 24 МГц, но в тактах – дольше.
Заключение
В статье показано, что в общем случае, тактовая частота 48 МГц для контроллера CH32X035 не даёт преимуществ для процессорного ядра относительно тактовой частоты 24 МГц. Она может быть полезна для точного задания частоты UART, для более высокой точности таймера, но не для исполнения команд. Команды будут исполняться на частоте 24 МГц.
Скорее всего, это вызвано латентностью флэш-памяти на максимальной частоте. При этом ускорители предвыборки не могут обеспечить полноценную загрузку конвейера, но создают неравномерность работы. На частоте 24 МГц действие ускорителей уже начинает проявляться, хоть и не факт, что полностью, ведь нулевая латентность, согласно исходным кодам, активируется только на частоте 16 МГц.
Но если есть работа с портами, не разделённая прочими командами, все эти ускорители являются злом. В статье показано, что в одном случае, система успела считать данные из портов до того, как предыдущие успели в них записаться. Для предотвращения таких ситуаций, лучше наоборот установить тактовую частоту 48 МГц, но отключить предвыборку команд (в коде, показанном в статье, в CSR 0xbc0 записывается не рекомендованная производителем константа 0x1f, а константа 0x1c).
Желающие могут продолжить исследования на более низких частотах, где латентность становится равной нулю, но автору низкие частоты процессорного ядра не так интересны. Возможно, именно там начнёт на полную мощность работать предсказатель ветвлений. Но может, для этого надо сделать соотношение частот на системной шине и шине APB, отличным от 1. Но и такие режимы не представляют интерес для автора, поэтому не рассматривались.
Комментарии (10)
Brak0del
14.01.2025 08:45Другой вопрос, как быть с предсказателями ветвлений, если они не могут предсказать безусловный переход?
Подскажите, осциллограммы строились на первой итерации цикла или нет? По идее, на первой итерации у предсказателя должна быть осечка, ведь он ещё не встречался с этим циклом, ещё не обучен.
VelocidadAbsurda
14.01.2025 08:45А вот тут ещё вопрос, что вообще имеется в виду под предсказателем в данной реализации? А то при общей невнятности документации может оказаться простым «все переходы назад - вероятны, вперёд - нет».
Brak0del
14.01.2025 08:45На скрине в статье видны BTB, BHT, RAS -- это механизмы Branch Target Buffer, Branch History Table и Return Address Stack. Конкретно в этом куске кода наверно отыграет branch target buffer -- фактически маленькая ассоциативная память (кэшик), которая по program counter выдаёт сохраненный ранее адрес перехода.
you_been_pwn3d
14.01.2025 08:45Полезная статья. Надо будет свой msp430fr5939 протестировать (да, там не заявлено прямым текстом об "улучшайзерах", но мало ли).
VelocidadAbsurda
По идее (раз скорость режет ожидание flash), «полноценных» 48МГц можно достичь при исполнении из RAM. Применимость в целом так себе, но в каких-то случаях вынести туда критический код может и помочь.
EasyLy Автор
Проверять не хочу, но там может вылезти другая проблема - Гарвардская архитектура. Сейчас шины должны работать в параллель. Флэшка для кода, ОЗУ для данных. Так - будут задержки из-за двойного использования шины ОЗУ для кода/данных.
Но я проверял только те режимы, которыми собираюсь пользоваться, а уж проверку этого дела оставим тем, кто решит использовать этот вариант. Но на Cortex M при исполнении из ОЗУ этот эффект точно возникает.
VelocidadAbsurda
Согласен, и это ещё сужает применимость данного подхода. Посмотрел, никаких TCM, которые помогли бы обойти данную проблему, в этом МК нет.