Дню знаний посвящается...
Данный пост посвящен тому, с чем сталкиваются все пользователи Ардуино (далее по тексту А, имейте в виду что под этой буквой будет прятаться как сам кристалл, так и среда разработки программ), а именно с работой с портами ввода/вывода.
Вы спросите, а что, собственно, тут рассматривать? Функции работы с портами прописаны ясно, есть большое количество примеров, поэтому использование портов не представляет никакой сложности. Начальная программа мигания светодиодом использует эти функции и прекрасно работает, о чем речь?
Все это верно, но лишь до того момента, когда вам потребуется подключать к А что-нибудь пошустрее светодиода (у меня нет претензий к этим замечательным приборам, но обычно в силу специфики использования особого быстродействия от них не требуется), и тут то Вам понадобится эффективная работа с пинами (будем так для краткости именовать порты ввода/вывода), и тогда на форумах возникают вопросы «почему у меня так медленно работает программа», на которые молодые гуру мгновенно отвечают «работай напрямую с регистрами и будет тебе счастье» и показывают, как именно, по их мнению, следует это делать.
Несмотря на то, что в их ответе есть толика истины, тем не менее такой ответ далеко не полон, в чем то не совсем верен (посмотрите, как именно они рекомендуют работать с регистрами), не вполне оптимален, и не слишком понятен. Заполнить пространство между стандартными скетчами и подобным ответом и предназначен настоящий пост.
Поскольку я вижу перед собой читателя с различным уровнем подготовки, то постараюсь ориентироваться на различный диапазон знаний в области МК (микроконтроллеров), поэтому, если Вам что то покажется хорошо известным (а будет немало таких мест) смело пропускайте этот фрагмент, а вот если что то будет не вполне понятно, то комментарии и существуют для того, чтобы там задавать вопросы, я их читаю и, по мере своих сил, пытаюсь ответить.
Для начала, немного теории. Пины, как следует из их определения, предназначены для взаимодействия созданной Вами программы с внешним миром (при этом внутренним миром считается только сам МК). Для того, чтобы использовать пины, Вы должны знать об их существовании и, поскольку их более одного, уметь эти пины указывать (это называется именованием или адресацией). Поэтому каждый доступный Вам пин (а бывают и недоступные, но они нам неинтересны с практической точки зрения) должен иметь уникальный параметр, его характеризующий. В А приняты в качестве такого параметра номера пинов и, хотя это не единственный возможный способ, он по-своему неплох. Далее все, что Вам нужно знать, это о наличии связи пина, имеющего конкретный номер, с конкретным выводом МК и, соответственно, контакте на гребенке, через которую подключаются к Вашей плате (конечно, это плата А, но, раз Вы ее купили, она Ваша) различные внешние устройства, и соответственно, о влиянии конкретного пина на конкретное внешнее устройство.
Например, если Вы хотите, чтобы расположенный на плате А светодиод засветился, то Вы должны на пин 13 подать уровень «Low», а для продвижения данных через микросхему сдвигового регистра сформировать на пине 6 переход из низкого уровня в высокий и так далее. Причем для первого случая — управления светодиодом дальнейшие рассуждения не имеют особого смысла, поскольку приемником информации является человеческий глаз, а его возможности по части быстродействия не превышают десятков Герц, то для управления сдвиговым регистром время изменения уровня на ножке весьма существенно, так как для вывода информации на внешнее устройство (например, семи-сегментный индикатор) Вам потребуется множество изменений и их совокупное время может оказаться неприемлемым для конкретного случая (будет блокировать работу остальных частей программы, критичных по периоду опроса, типа обработки энкодера, что нарушит их работу). Ну а если Вы управляете пином виртуального SPI, то получившееся быстродействие SD карточки Вас весьма неприятно удивит, так что задача ускорения работы с пинами вполне практическая.
Для управления пинами в А существуют предопределенные функции, главной из которых является DigitalWrite, которой Вы должны сообщить номер пина для модификации и значение на нем после выполнения функции. Однако, если у Вас после написания команды DigitalWrite(13,Low) проблемы закончились (при условии, что Вы не забыли где то раньше команду настройки режима пина), то у исполняющей системы они только начинаются. Дело в том, что существуют архитектуры МК, в которых каждый пин действительно имеет уникальный адрес, чем обеспечивается легкое отображения Вашей команды на систему команд МК, чем и занимается исполняющая система (связка компилятора и системной библиотеки), но фирма Atmel в те времена, когда создавалась А, своих поклонников подобными изысками не баловала (это не совсем верно, но в первом приближении так). В микроконтроллерах семейства Мега, на которые платформа А исторически базировалась, принята несколько иная схема работы с пинами. Здесь работа с внешним миром осуществляется не через уникальные пины, а через порты ввода/вывода, которые представляют собой совокупность пинов (в данном случае не более 8) и, соответственно, каждый пин имеет в физическом представлении 2 параметра — имя порта (представляется буквой от А до Е в разных представителях семейства МК) и номером бита внутри порта (цифра от 0 до 7). Так, например, пин 13 может иметь физический адрес РB.5 в одном МК, и РC.0 в другом.
Поэтому первая задача исполняющей система — преобразовать номер пина в физическое представление для последующей работы с ними. Решать эту задачу можно различными способами и, на мой взгляд, в А это сделано не лучшим способом, но, к счастью, система является открытой, и мы можем внести необходимые изменения и исправления в нее.
Прежде всего, заметим, что в исполняющую систему входят две компоненты — компилятор (на самом деле под этим собирательным именем прячутся отнюдь не одна функция, а, как минимум, препроцессор, собственно компилятор, ассемблер, компоновщик и библиотекарь, а также исполняющая подсистема, представленная библиотечными модулями (скетчами). Так вот, задача преобразования должна быть решена либо на этапе компиляции, либо на этапе исполнения, либо каким то образом распределена между этими этапами. И предпочтительнее сделать как можно больше работы на первом этапе, поскольку затраты времени на нем пренебрежимы по сравнению со временем собственно написания кода, а вот любые затраты (памяти и времени) на этапе исполнения требуют расходования ограниченных (по сравнению с платформой разработки) ресурсов МК. К сожалению, данное предложение не всегда реализуемо, но в условиях, когда известен конкретный состав системы исполнения и наличествуют все исходные файлы, может быть весьма полезно. Но это я слегка забежал вперед, чуть приостановимся и посмотрим реализацию функции работы с пином в А. Вот исходный код функции
void digitalWrite(uint8_t pin, uint8_t val) {
uint8_t timer = digitalPinToTimer(pin);
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *out;
if (port == NOT_A_PIN) return;
// If the pin that support PWM output, we need to turn it off before doing a digital write.
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
out = portOutputRegister(port);
uint8_t oldSREG = SREG;
cli();
if (val == LOW) { *out &= ~bit; } else { *out |= bit; }
SREG = oldSREG; }
Сразу же отмечу, что данный код взят отсюда, как и последующие исходные коды, но он не похож на фэйковый, да и в других источниках мне попадался именно такой код, так что будем считать его действительно кодом для А.
Прежде, чем мы двинемся по тексту, для начала выскажу неудовольствие сигнатурой функции, и это неудовольствие вполне обоснованно. Оба параметра совершенно очевидно не являются целыми числами и принимают ограниченный набор значений, так что они должны быть определены, как экземпляры перечислимых типов, что позволит нам проконтролировать правильность передаваемых в них фактических параметров, по крайней мере в отношении константных выражений, что и сделано в моей реализации. Ну а теперь можно перейти и к рассмотрению кода.
Что мы тут видим — прежде всего преобразование номера пина в физический адрес, осуществленное путем извлечения информации из таблицы констант. Рассмотрим эту операцию более подробно, для чего пойдем по исходному тексту, смотрим код и видим операцию получения битовой маски:
#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )
И обнаруживаем, что это макрос-расширение, который передает номер пина другой функции и добавляет имя таблицы, где хранятся номера битов. Далее выясняем, что:
#define pgm_read_byte(address_short) pgm_read_byte_near(address_short)
Это тоже макрос-обертка, передающий свои аргументы следующей функции, сразу же выясняется, что:
#define pgm_read_byte_near(address_short) __LPM((uint16_t)(address_short))
Это… да, верно, макрос-обертка, Вы начинаете улавливать принцип, передающий свои аргументы в функцию:
#define __LPM(addr) __LPM_enhanced__(addr)
Которая является (кто бы мог подумать) макросом-оберткой для функции, которая является… а вот тут не угадали, должно же было все закончиться рано или поздно, настоящим макросом-подстановкой ассемлерной вставки. Мне не слишком понятно наличие четырех оберток, но, поскольку мы договорились (вернее я это утверждал, а Вы не спорили, или спорили, но я этого не заметил), что время компилятора не стоит ничего, то не будем акцентировать внимание на данном аспекте, а просто удивимся вслух, зафиксируем свое удивление на бумаге и пойдем дальше смотреть код.
#define __LPM_enhanced__(addr) (__extension__({
uint16_t __addr16 = (uint16_t)(addr);
uint8_t __result;
__asm__ ( "lpm %0, Z" "\n\t" : "=r" (__result) : "z" (__addr16) ); __result; }))
Рассмотрим текст функции извлечения данных из таблицы более внимательно, тут есть ряд интересных моментов. Поскольку нам дана ассемблерная вставка, то следует учитывать особенности реализации архитектуры МК, для нас важно, что он 8-разрядный и аккумуляторный.
Обратим внимание на первые две строчки, где фактический параметр извлекается не из текста подстановки макроса, а из промежуточной переменной __addr16, и поставим вопрос — почему сделано именно так, это же лишние пересылки. Придуманный мною ответ — может быть, это действительно было необходимо, если мы желаем в качестве номера пина применять статически невычислимые выражения, то есть строку символов, значение которой невозможно определить на этапе компиляции с точностью до одной пересылки. Тогда при раскрытии макроса будут сгенерированы команды, вычисляющие данное выражение «на лету», результат передан в промежуточную переменную, а дальше использован по назначению. То есть в данном случае мы имеем учет особенностей препроцессора, а не архитектуры МК, за которые приходится платить быстродействием. Вы уже догадались, что я не в восторге от подобной практики, когда за возможность применять статически неопределенные выражения должны платить все вызовы функции, в том числе и с константными параметрами, которых будет подавляющее большинство.
Сразу же подумаем об альтернативах — во-первых, это может быть вариантная реализация, основывающаяся на определении типа параметра, если это возможно в данном препроцессоре (я его не настолько хорошо знаю, чтобы привести данное решение), в-вторых, это может быть прямой запрет использования статически неопределенных выражений, когда конструкция
DigitalWrite(BasePinNumber+6, Low)
приведет к ошибке компилятора и Вам придется превратить ее в int PinNumber=BasePinNumber+6; DigitalWrite(PinNumber,Low)
, что мне представляется приемлемой ценой за увеличение быстродействия в остальных случаях.(Позднее примечание. Просмотр реального кода показал, что эта строчка не порождает дополнительного кода в рассматриваемом компиляторе, ее удаление и прямая передача ассемблеру аргумента макроса не изменяет код ни насколько, так что видимо это наследие проклятого прошлого, так и оставим, раз на максимальную скорость не влияет, а текст удалять не хочется, идея то была неплохая).
Необходимое пояснение — хотя я и рассуждаю о недостатках реализации А функции, тем не менее должен признаться что самой платы А у меня нет, то есть конечно есть, но это так называемая А совместимая плата Intel Edisson, которая хоть и может программироваться из А среды, тем не менее никоим образом на является заменой платы собственно А. Следующий грех, в коем я должен признаться — я не пользуюсь А средой разработки, на что есть множество причин…
Есть великолепный анекдот на эту тему. После одной из битв Наполеон спрашивает у маршала артиллерии, почему артиллерия не стреляла. Маршал отвечает: «На то было множество причин, Сир. Во-первых, у нас не было снарядов. Во-вторых ...». Наполеон прерывает его: «Достаточно»
… из которых в данный момент определяющей является то, что в ней весьма неудобно смотреть промежуточные файлы, в том числе код на языке ассемблер. Поэтому дальнейшие рассматриваемые результаты относятся к коду, получаемому в онлайн компиляторе gcc.godbolt.org в режиме AVR gcc 4.5.2 при включенной оптимизации -O. Сразу хочу заверить читателя, что данный компилятор порождает весьма эффективный код, если бы я писал его руками, то получилось бы не намного лучше (хотя все таки и чуть лучше), я думаю, что в А компиляторе результаты лучше, чем полученные этим способом, точно не будут.
Далее (вернее, чуть ранее) мы видим извлечение дополнительной информации из вспомогательной таблицы, которая сообщает нам, не является ли данный пин выходным портом таймера. Я не очень понимаю, как именно этот факт может повлиять на нежелание исполняющей системы работать с таким пином без отключения таймера (мне кажется, что в данном случае она на себя много берет), но то, что такая проверка требует дополнительного времени, для меня несомненно. Возможно, это наследие проклятого прошлого, истинный смысл подобного решения недоступен для непосвященных в сокровенные тайны А. Что мы можем сделать для ускорения работы? Ну можно совместить данную проверку с получением индекса порта, введя специальное значение, тем более, что такая подобная проверка индекса порта на допустимость осуществляется несколькими строками позже. Далее, мы можем определить условную компиляцию, предоставив пользователю возможность определить, а готов ли он платить временем исполнения (если бы речь шла о времени компиляции, я был бы только рад) за дополнительную проверку. Ну и как вишенка на торте, само условие проверки с использованием двойного отрицания мне лично представляется несколько вычурным, простое условие
if (timer == IS_ON_TIMER)
ничем не уступает оригинальному условию, но понятнее при прочтении. Обратим еще внимание на то, что получаем это значение в одном месте, а используем (причем один раз) намного позже, что тоже не есть красиво, что неизбежно для языка С, но у нас то С++, и можно сделать правильнее, хоть и не быстрее.Еще один интересный момент, связанный с проверкой на таймер. Тут присутствует несомненная ошибка, если Вы посмотрите на порождаемый код в функции выключения шима (посмотрите этот код самостоятельно) turnOffPWM(), то увидите возможность нарушения работы других модулей. Конечно, она будет проявляться крайне редко, может быть, вообще никогда не проявится (но не забывайте о законах Мерфи), но она есть и должна быть безусловно исправлена, ведь иначе ЭТО могут увидеть дети, и решить, «а я и не знал, что ТАК можно».
Далее следует проверка индекса порта на допустимость, поскольку далеко не все пины могут иметь физическое представление в конкретном МК. И опять тут не все сделано хорошо. Во первых, нарушается правило, что каждая функция должна иметь одну точку выхода, ну тут достаточно слегка подправить текст. Правда, возрастет цикломатическая сложность программы, но тут приходится выбирать, какое из взаимоисключающих правил поддерживать. Во вторых, мы опять должны платить за паранойю разработчиков временем исполнения, причем нас не спрашивают, нужна ли нам такая забота. Есть два варианта увеличения быстродействия этого фрагмента — первый это условная компиляция, а вот второй похитрее — указание в элементах таблицы, соответствующих отсутствующим пинам, адреса порта и номера бита, изменение которого нейтрально по отношению к внешним выводам (и внутренним регистрам МК, что даже более важно). Проще всего указать нулевую битовую маску для реального порта, но возможны варианты. Да, в этом случае мы проделаем бесполезную работу в случае неправильного номера пина (отсутствующего в данном МК), но не потратим время при правильном номере.
Вот в чем с разработчиками А нельзя не согласиться, так с необходимостью не допустить модификацию внутренних битов регистров МК при задании неверного номера пина, чем указанный фрагмент кода и занимается, но тогда почему, черт подери, такой проверки нет в самом начале функции, ведь если мы попросим изменить состояние пина 137, то получим совершенно непредсказуемое поведение программы, и это разработчиков А абсолютно не волнует — налицо забавное сочетание паранойи и пофигизма, я думал, последнее свойственно только нам, славянам. Мы можем вставить такую проверку в текст функции, но намного лучше делать, как я советовал в самом начале — создать пользовательский тип и компилятор сделает нужные проверки сам и на фазу исполнения ничего не останется. Опять таки, Вы легко можете данную проверку обойти и выстрелить, куда только захотите, но Вам придется ясно и четко компилятор о своем намерении предупредить и потом не жалуйтесь.
Смотрим дальше и замечаем, что номер бита извлекается в одну стадию, а адрес порта — в две стадии, сначала по номеру пина получаем индекс порта — число от 0 до количества портов, а затем на основании индекса извлекаем собственно адрес порта. Зачем так сделано, ведь это очевидно дольше, чем сразу получить нужный нам адрес — можно придумать два объяснения. Во-первых, такая методика дает бОльшую гибкость — честно говоря, притянутое за уши объяснение. Вторая возможная причина — экономия размера ПЗУ, которая составит в байтах количество пинов минус размер дополнительного кода, то есть байтов 6-8, что мне представляется явно недостаточной компенсацией за существенное уменьшение быстродействия. Более того, тот же результат может быть достигнут и при помощи указания в первой таблице не индекса, а смещения с последующим превращением его в адрес менее затратным путем сложения с базой или даже комбинирования, как продемонстрировано в моей реализации. Да, этот способ не столь переносим, как оригинальный, но насколько я помню, директивы условной компиляции никто не отменял. Вообще то, у меня складывается впечатление, что многие компоненты библиотек А делались на скорую руку из уже существующих универсальных (кто такой универсал — человек, который умеет делать множество дел одинаково плохо) заготовок, а дальше действовали по принципу «Эта штука работает? — Да. — Не трогай ее.» К сожалению, намного более быстрый способ обращения к портам через команды in и out в данном случае неприемлем, поскольку указать номер порта в качестве аргумента команды в общем случае невозможно.
Продолжим рассмотрение кода. Мы получили адрес порта и маску бита и можем приступить к собственно выполнению операции, то есть тут мы видим именно то, что нам рекомендуют юные гуру от А. Еще раз, как я уже неоднократно делал в своих постах, умоляю Вас так не делать. То есть другого пути изменить содержание бита в порте нет (а вот я Вас и обманул, есть, но об этом чуть позже), но совершенно необязательно оформлять этот путь в виде прямой операции. Обязательно оберните обращение к регистрам в макрос или инлайновую функцию, это Вам может сэкономить немало времени при отладке. Конечно, если Вы принадлежите к числу счастливчиков, которые никогда в своей жизни не забывали символ ~ перед маской, то обертка Вам не нужна (но тогда зачем Вы вообще это читаете, мой пост не для полубогов от программирования), но вреда она не принесет точно, а вот для нормальных людей, которым свойственно ошибаться, она весьма полезна. Причем авторы А о такой необходимости знают, посмотрите реализацию выключения ШИМ, там как раз стоит макрос на сброс бита, но в этом конкретном месте они такой возможностью высокомерно пренебрегают.
И еще обратим внимание на то, что собственно работа с регистром, а именно чтение-модификация-запись обрамлены дополнительными строками, назначение которых непонятно для неофитов. Мы то с Вами понимаем, что это защита от совместного использования ресурса, выполненная в классическом стиле критической секции, где бит разрешения прерывания используется в качестве семафора, но она с неизбежностью требует времени для исполнения, даже если она в данной конкретной программе (надеюсь, никто не обиделся, что я так назвал скетч) не нужна. Привлекая на помощь условную компиляцию, мы можем еще немного улучшить скорость работы функции. Рекомендую обратить внимание на то, что все вновь вводимые условия компиляции изначально поставлены таким образом, чтобы работа функции в режиме по умолчанию нисколько не изменилась, дабы сохранить преемственность, вдруг в каком то скетче учитывалось, что время работы функции именно такое, и это важное обстоятельство не должно изменяться.
Кстати, одно интересное наблюдение. При отсутствии защиты от совместного использования регистра может быть потеряна работа по изменению состояния битов порта, произведенная той частью программы, которая прервала работу другой части, и никогда не наоборот. Видимо, таким образом реализует себя мировая справедливость, выраженная в народной мудрости «кто мешает, того бьют».
И еще один маленький камешек в огород А — из текста функции нетрудно видеть, что описание работы функции не вполне верно — если значение второго параметра равно LOW, то бит будет сброшен, в противном случае (а не если параметр будет равен HIGH, как в описании) бит будет установлен. Если этот параметр принимает только указанные значения, то данное уточнение не имеет смысла, но в оригинальной функции его значения ничем не ограничены, кроме доброй воли программиста.
Для того, чтобы Вам были понятны следующие вычисления, под спойлером лежит получившийся при компиляции код с комментариями:
digitalWrite(unsigned char, unsigned char):
преамбула 9 тактов
push r15
push r16
push r17
mov r16,r22
mov r18,r24
ldi r19,lo8(0)
uint8_t timer = digitalPinToTimer(pin); 7 тактов
mov r30,r18
mov r31,r19
subi r30,lo8(-(digital_pin_to_timer_PGM))
sbci r31,hi8(-(digital_pin_to_timer_PGM))
lpm r24, Z
uint8_t bit = digitalPinToBitMask(pin); 7 тактов
mov r30,r18
mov r31,r19
subi r30,lo8(-(digital_pin_to_bit_mask_PGM))
sbci r31,hi8(-(digital_pin_to_bit_mask_PGM))
lpm r17, Z
uint8_t port = digitalPinToPort(pin); 7 тактов
subi r18,lo8(-(digital_pin_to_port_PGM))
sbci r19,hi8(-(digital_pin_to_port_PGM))
mov r30,r18
mov r31,r19
lpm r15, Z
if (port == NOT_A_PIN) return; 2 такта
tst r15
breq .L12
if (timer != NOT_ON_TIMER) turnOffPWM(timer); 3 такта в случае невызыва функции, в случае вызова лучше не считать
cpse r24,__zero_reg__
rcall turnOffPWM(unsigned char)
out = portOutputRegister(port); 14 тактов
mov r30,r15
ldi r31,lo8(0)
lsl r30
rol r31
subi r30,lo8(-(port_to_output_PGM))
sbci r31,hi8(-(port_to_output_PGM))
lpm r24, Z+
lpm r25, Z
mov r30,r24
mov r31,r25
uint8_t oldSREG = SREG; 1 такт
in r24,__SREG__
cli(); 1 такт
cli
if (val == LOW) { *out &= ~bit; } else { *out |= bit; } 10/8 для LOW/HIGH
tst r16
brne .L15
ld r25,Z
com r17
and r17,r25
st Z,r17
rjmp .L16
.L15:
ld r25,Z
or r17,r25
st Z,r17
.L16:
SREG = oldSREG; 1 такт
out __SREG__,r24
.L12:
постамбула 10 тактов
pop r17
pop r16
pop r15
ret
main:
....
собственно вызов 4 такта
ldi r24,lo8(1)
ldi r22,lo8(0)
rcall digitalWrite(unsigned char, unsigned char)
..... ret
Теперь можно подвести итоги. Вызов+преамбула+постамбула — 4+9+10 = 23, защита ресурса — 3, защита таймера — 7+3 = 10, защита пина — 2 получение номера бита — 7, получение адреса порта — 7+14 = 21, модификация значения — 10/8, что дает нам время исполнения функции 76/74 такта, или при тактовой частоте МК 16 МГц составит 4.75/4.625 мксек — результат вполне ожидаемый для того, кто видел исходный код и знаком с архитектурой AVR. В разных источниках я видел разные цифры времени исполнения функции digitalWrite, но они были только больше полученных в данном случае.
Интересное наблюдение — время установки и сброса бита различаются, что не есть слишком хорошо. Данный недостаток легко исправить, заменив условие на обратное, тогда мы получаем 9/9 — выравнивание приводит к увеличению одного из времен, но зато они выравниваются — мелочь, а приятно.
register char tmp;
tmp=*out;
tmp |= bit;
bit = ~ bit;
if (val == HIGH) tmp &= bit;
*out=tmp;
получить следующую минимально возможную программу ld r18,Z
or r18,r17
com r17
cpse r16,one_reg
and r18,r17
st Z,r25
Неожиданно пришло в голову следующее соображение — стандартная реализация временного отключения прерывания, рекомендованная фирмой Atmel в многочисленных примерах, небезопасна. Она уязвима в точке от чтения текущего значения регистра состояния и до запрета прерываний, так что результаты работы функции, прервавшей программу в этой точке, по изменению бита разрешения прерывания будут потеряны в момент восстановления регистра состояния сохраненным значением. Конечно, переход 0-1 просто невозможен, но переход 1-0 вполне себе представим. Как то мне не по себе стало от такого открытия, либо я чего то недопонимаю, либо одно из двух. Единственное, что могло бы спасти в подобной ситуации — внутренний запрет на прерывания на 1 такт после чтения регистра состояния, но в документации я такого примечания не нашел. Если кто в курсе — прошу в комменты.
Ну а теперь посмотрим, к чему мы пришли в результате реализации вышеперечисленных изменений при оптимизации на скорость. Вот исходный код:
#define I_NEED_TIMER_CHECKING 0
#define I_NEED_PORT_CHEKING 0
#define I_NEED_OLD_PORT 0
#define I_NEED_OLD_DATA 0
#define I_NEED_INTERRUPTS 0
void digitalWrite(uint8_t pin, uint8_t val)
{
#if ( I_NEED_TIMER_CHECKING == 1)
uint8_t timer = digitalPinToTimer(pin);
#endif
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port;
#if I_NEED_OLD_PORT == 1
port = digitalPinToPort(pin);
#else
port = digitalPinToPortNew(pin);
#endif
#if I_NEED_PORT_CHEKING == 1
if (port == NOT_A_PIN) return;
#endif
#if ( I_NEED_TIMER_CHECKING == 1)
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
#endif
uint8_t *out;
#if I_NEED_OLD_PORT == 1
out = (uint8_t *) portOutputRegister(port);
#else
out = (uint8_t *) ( port + BASEPORT );
#endif
#if I_NEED_INTERRUPTS ==1
uint8_t oldSREG = SREG;
cli();
#endif
#if I_NEED_OLD_DATA == 0
if (val == LOW) {
*out &= ~bit;
} else {
*out |= bit;
}
#else
if (val != LOW) {
*out |= bit;
} else {
*out &= ~bit;
}
#endif
#if I_NEED_INTERRUPTS ==1
SREG = oldSREG;
#endif
};
digitalWrite(unsigned char, unsigned char):
преамбула 1 такт
ldi r25,lo8(0)
uint8_t bit = digitalPinToBitMask(pin); 7 тактов
mov r30,r24
mov r31,r25
subi r30,lo8(-(digital_pin_to_bit_mask_PGM))
sbci r31,hi8(-(digital_pin_to_bit_mask_PGM))
lpm r18, Z
uint8_t port = digitalPinToPortNew(pin); 7 тактов
subi r24,lo8(-(digital_pin_to_port_new_PGM))
sbci r25,hi8(-(digital_pin_to_port_new_PGM))
mov r30,r24
mov r31,r25
lpm r24, Z
uint8_t *out = (uint8_t *) ( port + BASEPORT ); 2 такта
mov r26,r24
ldi r27,lo8(0)
if (val == LOW) { *out &= ~bit;} else { *out |= bit; } 8/8 тактов
tst r22
brne .L13
com r18
ld r30,X
and r18,r30
st X,r18
ret
.L13:
ld r30,X
or r18,r30
st X,r18
постамбула 4 такта
ret
main:
....
собственно вызов 4 такта
ldi r24,lo8(1)
ldi r22,lo8(0)
rcall digitalWrite(unsigned char, unsigned char)
....
Вызов+преамбула+постамбула — 4+1+4 = 9, защита ресурса — 0, защита таймера — 0, защита пина — 0, получение номера бита — 7, получение адреса порта — 7+2 = 9, модификация значения — 8/8, что дает нам время исполнения функции 9+7+9+8/8 = 33/33 такта, или при тактовой частоте МК 16 МГц составит 2.062/2.062 мксек, совсем неплохо, ускорение в 2 раза, но можно и лучше, для чего есть два пути.
Первый из них заключается в смене парадигмы использования пина, где в существующем подходе при каждом использовании функции заново происходят все вычисления. Альтернативой данному подходу будет разбиение работы с пином на две части — отдельно преобразование номера пина в физические параметры со всеми необходимыми проверками и отдельно использование полученных результатов для собственно модификации содержимого бита, соответствующего физическому адресу. Очевидно, что само по себе такое разбиение при однократном вызове функции не может привести к увеличению быстродействия, но мы крайне редко изменяем значение пина один раз в программе, а вот в противном случае выигрыш может быть значителен. Недостатком такого метода является необходимость выделения памяти для хранения промежуточных результатов, но за все в этом мире надо платить, ДарЗаНеБы. Получаем следующий код, иллюстрирующий данную возможность:
PinAdr Pin13=TransferPin(PIN13);
DigitalPut(&Pin13,LOW);
И мы видим, что наши усилия увенчались успехом — время модификации пина составляет всего лишь 13+3+9+8/8 = 33 такта при включенной защите от прерываний и 30 тактов при выключенной (остальные режимы не влияют на данную функцию, поскольку остались в фазе преобразования, а Вы догадались не ставить ее перед каждым вызовом фазы исполнения), при этом время вычисления адреса увеличилось незначительно, чего и следовало ожидать. Но почему прирост не слишком большой, всего лишь 10%, ведь мы убрали из фазы выполнения работу с таблицами? Все дело в извлечении параметров функции из памяти при таком подходе, обратите внимание на то, как возросла длительность преамбулы и длительность выборки (9) и поглотила значительную часть выигрыша от операции.
Для улучшения ситуации используем другой способ передачи параметров, а именно
DigitalPut2(Pin13.Port,Pin13.Mask,LOW);
и получаем куда больший выигрыш 15+8/8 = 24 такта с отключенной защитой (27 с защитой), что почти на 30% лучше быстрого варианта в 30 тактов и в 3 раза быстрее оригинального варианта:typedef struct { uint8_t *Port; uint8_t Mask;} PinAdr;
PinAdr TransferPin(uint8_t Pin) {
PinAdr PinAdrTmp;
uint8_t port;
#if I_NEED_OLD_PORT == 1
port = digitalPinToPort(Pin);
#else
port = digitalPinToPortNew(Pin);
#endif
#if I_NEED_PORT_CHEKING == 1
if (port == NOT_A_PIN) PinAdrTmp.Mask=0; else
#endif
PinAdrTmp.Mask = digitalPinToBitMask(Pin);
#if ( I_NEED_TIMER_CHECKING == 1)
uint8_t timer = digitalPinToTimer(Pin);
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
#endif
#if I_NEED_OLD_PORT == 1
PinAdrTmp.Port = (uint8_t *) portOutputRegister(port);
#else
PinAdrTmp.Port = (uint8_t *) ( port + BASEPORT );
#endif
return PinAdrTmp;
}
void DigitalPut(PinAdr &Pin, uint8_t val) {
#if I_NEED_INTERRUPTS ==1
uint8_t oldSREG = SREG;
cli();
#endif
if (val != LOW) {
*(Pin.Port) |= Pin.Mask;
} else {
*(Pin.Port) &= ~ Pin.Mask;
}
#if I_NEED_INTERRUPTS ==1
SREG = oldSREG;
#endif
};
void DigitalPut2(uint8_t *Port, uint8_t Mask, uint8_t val) {
#if I_NEED_INTERRUPTS ==1
uint8_t oldSREG = SREG;
cli();
#endif
if (val != LOW) {
*Port |= Mask;
} else {
*Port &= ~ Mask;
}
#if I_NEED_INTERRUPTS ==1
SREG = oldSREG;
#endif
};
DigitalPut(PinAdr&, unsigned char):
mov r30,r24
mov r31,r25
tst r22
breq .L14
ld r26,Z
ldd r27,Z+1
ld r25,X
ldd r24,Z+2
or r24,r25
st X,r24
ret
.L14:
ld r26,Z
ldd r27,Z+1
ldd r24,Z+2
com r24
ld r25,X
and r24,r25
st X,r24
ret
DigitalPut2(unsigned char*, unsigned char, unsigned char):
mov r30,r24
mov r31,r25
tst r20
breq .L17
ld r24,Z
or r22,r24
st Z,r22
ret
.L17:
com r22
ld r24,Z
and r22,r24
st Z,r22
ret
main:
.....
mov r24,r28
mov r25,r29
adiw r24,1
ldi r22,lo8(0)
rcall DigitalPut(PinAdr&, unsigned char)
ldd r24,Y+1
ldd r25,Y+2
ldd r22,Y+3
ldi r20,lo8(0)
rcall DigitalPut2(unsigned char*, unsigned char, unsigned char)
Неплохо. но мы то рассчитывали на большее. Конечно, даже маленькая рыбка лучше большого таракана, но мы то хотим поймать большую рыбку, можно ли сделать это в нашем пруду?
Разумеется, можно, иначе бы я этот вопрос не задавал. Еще большее увеличение быстродействия связано со вторым путем, а именно учетом особенностей архитектуры МК. Да, этот путь тернист, такой подход требует изучения документации на конкретный МК (но я это сделал за Вас и Вы можете просто пожинать плоды), он практически не переносим на другой МК (но нам это и не нужно сейчас), он требует учета особенностей компилятора и ассемблера, в общем «Нельзя просто так взять и ...», но этот путь приводит нас к необычайно хорошему результату и мы на него встанем (то есть я встану, а Вам придется либо последовать за мной либо бросить чтение данного опуса).
Дело в том, что когда я заявлял о невозможности другого способа изменения состояния бита в регистре, я намекнул, что он таки есть. И этот путь связан с использованием специальных команд работы с битами, а именно команд с мнемониками CBI и SBI. Почему мы оставили эту возможность напоследок — потому что это зависимое решение и оно плохо реализуемое (вообще никак не реализуемое) в рамках языка С, но то же самое можно сказать и про чтение данных из таблиц, привлечение ассемблера неизбежно.
К сожалению, обе эти команды не принимают никаких параметров, что не позволяет указать номер пина для модификации и вид модификации, а эта информация есть часть самой команды и мы имеем чисто теоретически 32 различных команды для 32 различных портов. Учтем возможность наличия у каждого порта 8 бит и получим 256 различных команд. Поскольку вид модификации пина, а именно установка либо сброс бита, тоже есть часть команды, всего получается 512 различных команд.
Немаленькое такое количество, в архитектуре 51 всех команд было не более 256, а тут такая роскошь, но 16-разрядная система команд может себе это позволить. Конечно, далеко не все из этих 512 команд будут производить осмысленные действия на конкретном МК, но некоторые будут и мы должны иметь возможность максимально быстро одну конкретную команду из этого набора выполнить.
У читателя сразу возникнет вопрос — а зачем нам такие проблемы с привлечением ассемблера и прочих сложностей, и я сразу же на него отвечаю — эти команды атомарны (не-прерываемы) что позволяет нам существенно сэкономить на времени исполнения, не отключая защиту от совместного использования ресурса, поскольку для атомарных операция она излишня. Итак, мы можем реализовать обращение к конкретной нужной нам команде классическим способом, создав таблицу команд и получив доступ к ней при вычислении индекса в таблице «на лету», как показано в следующем фрагменте кода:
typedef void func (void);
void fnull(void) {};
void fres1(void) {(__extension__({__asm__("cbi PORTD,0""\n\t");}));};
void fset1(void) {(__extension__({__asm__("sbi PORTD,0""\n\t");}));};
void fres2(void) {(__extension__({__asm__("cbi PORTD,1""\n\t");}));};
void fset2(void) {(__extension__({__asm__("sbi PORTD,1""\n\t");}));};
func *funcAdr_PGM[] PROGMEM = {
fnull,fnull,
fres1,fset1,
fres2,fset2,
};
#define funcOfPin(P) ( (func *)(pgm_read_word( funcAdr_PGM + (P))) )
void digitalWriteF(uint8_t Pin, uint8_t val) {
#if I_NEED_PORT_CHEKING == 1
uint8_t port;
#if I_NEED_OLD_PORT == 1
port = digitalPinToPort(Pin);
#else
port = digitalPinToPortNew(Pin);
#endif
if (port == NOT_A_PIN) return;
#endif
#if ( I_NEED_TIMER_CHECKING == 1)
uint8_t timer = digitalPinToTimer(Pin);
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
#endif
Pin=Pin*2;
if (val!=LOW) Pin++;
funcOfPin(Pin)();
};
main:
digitalWriteF(13,LOW);};
digitalWriteF(unsigned char, unsigned char):
Pin=Pin*2;
if (val!=LOW) Pin++;
lsl r24
cpse r22,__zero_reg__
subi r24,lo8(-(1))
func *p=funcOfPin(Pin);
mov r30,r24
ldi r31,lo8(0)
lsl r30
rol r31
subi r30,lo8(-(funcAdr_PGM))
sbci r31,hi8(-(funcAdr_PGM))
lpm r24, Z+
lpm r25, Z
p();
mov r30,r24
mov r31,r25
icall
ret
main:
ldi r24,lo8(13)
ldi r22,lo8(0)
rcall digitalWriteF(unsigned char, unsigned char)
Вызов+преамбула+постамбула — 4+0+4 = 8, защита ресурса — 0 (но она есть), учет значения — 3, получение точки входа — 6, получение адреса функции — 3+3+2 = 8, выполнение операции 3+2+4 = 9, что дает нам время исполнения функции 8+3+6+8+9 = 34/34 такта. Пока не очень ясно, зачем мы вообще все это затеяли, но вот как раз наступил момент, когда мы можем (и должны) слегка улучшить код, порожденный компилятором, и заменить вызов функции p() на asm («ijmp \n\t») (ну не совсем так, но смысл передает), что позволяет нам сэкономить 4 такта на постамбуле за счет совмещения операций, итого 30 тактов с защитой.
При этом нам потребуется 3*П (максимальное количество пинов) слов памяти программ по 2 байта каждый — 1 слово для собственно кода команды, 1 слово для кода возврата после выполнения каждой команды и 1 слово для хранения адреса команды в таблице адресации. Сразу же отметим, что эти вычисления дают верхнюю границу, поскольку для несуществующих пинов точка входа в пустую функцию одна и мы должны выделить 2*П1 (количество реально используемых пинов + 1)+П слов памяти программ, тем не менее требуемое количество памяти неплохо бы уменьшить.
Пробуем применить принцип разделения фаз вместе с модификацией таблиц и получаем:
void funcAll(void) {
asm volatile ("nop \n \t ret \n\t");
asm volatile ("nop \n \t ret \n\t");
asm volatile ("cbi PORTD,0 \n \t ret \n\t");
asm volatile ("sbi PORTD,0 \n \t ret \n\t");
asm volatile ("cbi PORTD,1 \n \t ret \n\t");
asm volatile ("sbi PORTD,1 \n \t ret \n\t");
};
typedef func *PinAdr;
PinAdr TransferPin(uint8_t Pin) {
#if I_NEED_PORT_CHEKING == 1
uint8_t port;
#if I_NEED_OLD_PORT == 1
port = digitalPinToPort(Pin);
#else
port = digitalPinToPortNew(Pin);
#endif
if (port == NOT_A_PIN) return;
#endif
#if ( I_NEED_TIMER_CHECKING == 1)
uint8_t timer = digitalPinToTimer(Pin);
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
#endif
Pin=Pin*4;
PinAdr PinTmp = (PinAdr) ((int)funcAll + Pin);
return PinTmp;
};
void DigitalPut(func *Pin, uint8_t val) {
if (val != LOW) {
Pin = (PinAdr) ((int)(Pin)+2);
};
Pin();
main:
digitalPut(Pin13,LOW);
};
DigitalPut(void (*)(), unsigned char):
cpse r22,__zero_reg__
adiw r24,2
mov r30,r24
mov r31,r25
icall ( ijmp )
ret
main:
lds r24,main::Pin13
lds r25,main::Pin13+1
ldi r22,lo8(1)
rcall DigitalPut(void (*)(), unsigned char)
Вызов+преамбула+постамбула — 7+0+4 = 11, защита ресурса — 0 (но она есть), учет значения — 3, получение точки входа — 0, получение адреса функции — 0, выполнение операции 2+3+2+4 = 11(7), что дает нам время исполнения функции 11+3+11(7) = 25(21) тактов, что лучше, чем для варианта с портами на целых 6 тактов. Заметим, что для ослабления требований к памяти, мы расположили таблицу команд несколько иным образом, тогда нам потребуется ровно 2*П слов в памяти программ. Вам решать, какая реализация предпочтительнее, быстродействие их близко.
Если мы аккуратно перепишем на ассемблере вызов функции и саму функцию работы с пином (оставляю это упражнение на долю пытливого читателя), то получим 11+3+(3+2)5=19 тактов, и это, похоже, предел совершенства.
Что мы получили в результате такой резкой смены парадигмы — существенное увеличение быстродействия в варианте с защитой от совместного использования ресурса, но не слишком значительный прирост в сравнении с вариантом, когда для порта защита отключена. На первый взгляд, несколько странный результат, ведь если раньше основная работа производилась четырьмя командами, то теперь только одной и мы должны были бы получить кратный прирост скорости. Дело в том, что накладные расходы на выполнение собственно модификации состояния пина не изменились и мы по прежнему должны потратить 2 такта на вызов функции и 4 на выход из нее (а еще определенное количество тактов на передачу фактических параметров, но будем считать, что нам повезло и эта часть оптимизирована компилятором до полного отсутствия), раньше было 4+4=8 тактов на обработку, теперь получается 1+4=5 тактов и прирост производительности даже не в 2 раза, а если учесть еще и время на подготовку — пересылку параметров в регистры, то и того меньше. А заплатить за такое не слишком значительное увеличение быстродействия нам пришлось вполне конкретной памятью программ и данных для хранения адреса функции.
Недоумение читателя вполне понятно, но, во первых, это все равно увеличение быстродействия, и если нам очень нужна скорость, то мы можем себе позволить пожертвовать памятью (не бывает оптимизации без целевой функции с конкретными весами параметров), а во вторых такой подход с одной командой для выполнения работы позволяет нам в определенных случаях достичь максимально возможного быстродействия, то есть одной команды на операцию модификации пина.
Что это за случаи и почему они не всегда присутствуют, хотя и довольно таки часто? Речь идет о ситуации, когда выполнены два условия: первое — нам доступны исходные коды программных модулей, для которых необходима быстрая работа с пинами, и второе — номера пинов должны быть константными, то есть известны на этапе компиляции (именно константными, а не статически определенными, это важно). Тогда мы можем создать макро-подстановку для работы с пинами следующего вида:
#define digitalWriteC(pin,val) if (pin == 0) { if (val == LOW) asm volatile ("cbi PORTD, 0 \n\t"); else asm volatile ("sbi PORTD, 0 \n\t"); }; if (pin == 1) { if (val == LOW) asm volatile ("cbi PORTD, 1 \n\t"); else asm volatile ("sbi PORTD, 1 \n\t"); }; if (pin == 2) { if (val == LOW) asm volatile ("cbi PORTD, 2 \n\t"); else asm volatile ("sbi PORTD, 2 \n\t"); };
main:
digitalWriteC(13,HIGH);
И вместо чудовищной конструкции, которую мы написали, в сгенерированный машинный код войдет одна единственная команда правильного изменения правильного бита, поскольку современные компиляторы настолько хороши в плане оптимизации, но следует учесть, что при попытке подставить не-константный аргумент вызова мы получим в результирующем коде множество сравнений (причем без всякого предупреждения), что вряд ли приведет к ожидаемому ускорению работы функции. Чтобы избежать подобного, в начале макроса вставлена защита от не-типизированного аргумента макроса, которая естественно не помешает Вам выстрелить себе в ногу, применив типизированную переменную, но кто я такой, дабы ограничивать Вашу свободу стрелять куда захочется. Обратим особое внимание на тот факт, что при подобном подходе мы экономим не только на времени исполнения, но и не проигрываем (вообще то точно выигрываем, но я предпочитаю обтекаемые формулировки) в размере кода (естественно, имеются в виду машинные коды, которые только и есть смысл учитывать), поскольку команда вызова подпрограммы занимает не меньше слова. В общем идеальный вариант, если выполняются два вышеуказанных условия.
Еще раз обращу Ваше внимание, что, например, для реализации интерфейса типа I2C или SPI данный способ представляется весьма привлекательным, обеспечивая действительно необходимое быстродействие, а вот реализация сканирования клавиатуры будет не слишком удобна, потребуется оператор выбора для включения конкретного пина, а максимально возможное быстродействие здесь не особо нужно, но решать Вам, как мы и договорились, я только даю Вам веревку, а способ нацеливания ее в нужное место остается в Вашей компетенции.
Другой идеальный вариант в том же самом случае выглядел бы как макроподстановка, генерящая код команды из своих константных аргументов, что нибудь вроде:
#define DigitalPut(Pin,Data) ( asm ( "DW 0x0123+((Pin / 8) << 4)+(Pin % 8) + (Data << 8))
Но мне почему то построить подобную конструкцию не удалось, если кто знает, как это сделать, прошу в комментарии.
Если бы у нас был нормальный макро-язык в препроцессоре, то можно было бы написать обобщенную подстановку, которая генерировала бы оптимальный вариант для каждого случая, не заставляя программиста задумываться о том, какую конструкцию вызвать и какую парадигму использовать, но это из области беспочвенных мечтаний, поскольку стандартный препроцессор С таких возможностей не предоставляет (по крайней мере мне не предоставляет, если Вы с ним в более дружеских отношениях, поделитесь секретами).
Вот такое получилось эссе на тему быстрой работы с портами в система А, может быть, кто то станет лучше понимать происходящее в МК, для кого нибудь станет отправной точкой для своих собственных экспериментов с платформой. Кстати, у меня давно лежит недописанный пост на тему собственно платформы А как таковой (конечно, это не рассказ о том, как установить и настроить среду разработки и помигать светодиодом, я обычно парю с других эмпиреях), не нахожу в себе сил закончить и опубликовать, когда начинал, думал, что тема будет интересна широкому кругу читателей, а потом засомневался и решил сделать более практически применимый пост, который Вы и прочитали до конца, я надеюсь. Замотивируйте меня в опросе на его завершение и публикацию, ведь обратная связь — важная часть любого рода деятельности, в особенности публичной, каковой и является написание постов.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (56)
Shtucer
01.09.2016 14:19Ардуино (далее по тексту А, имейте в виду что под этой буквой будет прятаться как сам кристалл, так и среда разработки программ)
Что за «кристалл», простите?ploop
01.09.2016 14:38По тексту понятно, что имеется ввиду AVR8.
Да и посыл автора верный, но скучный. Зачем пытаться понятьукуренныемысли разработчиков библиотек Arduino, когда можно просто её не использовать? Достаточно чистого Си и тонн кода в сети для тысяч периферийных устройств, чтобы не изобретать велосипедов. Либо забить на всё это и писать скетчами.
Архитектура очень проста, и не требует массы библиотек для старта, как, например, с ARM.Shtucer
01.09.2016 15:12Мысли разработчиков библиотек Arduino понять как раз несложно: библиотека универсальна для некоторого количества типов «кристаллов», в том числе и ARM. И оптимизация в ней далеко не приоритет. Но это не даёт повода мешать все «кристаллы», которые устанавливаются на Ардуины, в одну кучу. Интересней было бы почитать разбор libavrc, например. Хотя, думаю, там пищи для разоблачений поменьше было бы, но я могу ошибаться.
ploop
01.09.2016 15:17И оптимизация в ней далеко не приоритет
Собственно, я об этом. Если не устраивает такой подход, зачем «переделывать мир», когда можно просто пойти по другому пути?
GarryC
02.09.2016 12:15Вы имели в виду AVR Libс? Надо бы посмотреть, но не могу найти официальный код, ссылкой не поделитесь?
Shtucer
02.09.2016 12:20http://www.nongnu.org/avr-libc/
Исходники можно найти в глубинах Arduino IDE: hardware\tools\avr
Или в комплекте с авэровским тулчейном.
А, нет, вру, там одни хидеры и статические библиотеки. Значит, остаётся первая ссылка.GarryC
02.09.2016 12:51Спасибо, скачал, но почему ее на самом сайте Atmel нет, хотя документация на сайте лежит — старанно как то, или они на ASF переключились?
Shtucer
02.09.2016 13:00Документация лежит, потому что их тулчейн был построен вокруг gcc и libavr.
Про ASF пока ничего не знаю. Но раз уж он уже доступен, заодно и в его сторону посмотреть. Но я не уверен на счёт доступности его исходников.
ToSHiC
01.09.2016 15:52Там же C++, можно наспециализировать шаблонов, да ещё и предложить компилятору делать инлайн — вот вам и будет выбор адреса и команды во время компиляции.
GarryC
01.09.2016 18:04Я это и сделал, только через дефайн. Просто через шаблона лучше не получалось, я пробовал, но смысл становился менее понятен, все таки отнести шаблоны к интуитивно понятным частям С++ я не могу.
Alexeyslav
01.09.2016 16:46В той же ардуинке «Интел эдисон» данный подход просто невозможен и выигрыша не даст.
Но… даже 34 такта на переключение порта это дикость, даже с проверками в то время как сам контроллер выполняет это за пару тактов.
Там кстати с портами засада полнейшая, если мы берём простой AVR-контроллер вроде ATMEGA8 то проблем нет — там все наличные порты укладываются в 5 бит и адресуются командами IN/OUT но берём контроллер с более широкой периферией и у него адресное пространство регистров значительно шире, и до некоторых портов можно дотянутся только командами LDS/STS и к несчастью регистры большинства портов ввода-вывода находятся именно там.
И мне изначально непонятно зачем нужен динамический доступ к пинам? Обычно в скетче жестко указана функция пина и дальше не меняется т.е. они по факту используются как константы. А если нужен будет динамический доступ, то организовать специальную обёртку, которая будет медленной и неповоротливой как ныне существующая.
Похоже, что это писал x86 программист с учётом того что порты работают изначально медленно и все эти проверки выполнятся быстрее чем будет осуществлён непосредственно акт ввода-вывода из-за медленной шины. Это частично справедливо и для STM32 контроллеров — у них тоже изначально периферия работает значительно медленнее чем ядро выполняет инструкции, и элементарная операция ввода-вывода(даже на ассемблере) запросто может затянуться на 20...50 тактов ядра на ровном месте(одна инструкция).lorc
01.09.2016 18:08Мне кажется что подход в корне неверен. Почему-то ни в статье, ни в комментариях я не увидел главной аббревиатуры — GPIO. Процессорное ядро не управляет напрямую пинами. Оно обращается к контроллеру GPIO, который как раз отвественнен за ногодрыжество. Из этого следует несколько интересных выводов:
1. У контроллера GPIO есть своё быстродействие, которое может быть ниже быстродействия процессора. Просто потому что переключать силовые транзисторы долго. То что процессор записал единичку в какой-то регистр — ещё не значит что эта единичка тут же появится на пине/пэде.
2. GPIO — это general purpose input/output. А значит он нужен для неспешного подергивания пинами. Если возникает задача управлять пинами с частотой даже в сотню килогерц — значит при проектировании что-то пошло сильно не так.
Поэтому проблема быстрого управления GPIO должна рассматриваться как чисто теоретическое упражнение.GarryC
01.09.2016 18:20В принципе Вы правы, но дьявол кроется в деталях. Периферию А чипов нельзя назвать очень богатой и иногда приходится через GPIO реализовывать стандартные интерфейсы типа I2S или SPI. А когда речь идет, например о последовательных шинах типа WS2811, то применение стандартного интерфейса хоть и возможно, но требует особого внимания в реализации, и пины здесь — весьма удобное подспорье.
У меня есть пост о SPI как универсальном интерфейсе, но их не так много на чипе.
armature_current
02.09.2016 09:40контроллер GPIO? Наверно не стоит небольшой набор секвенциальной синхронной логики называть контроллером. Мне кажется, что в таком стиле можно и таймерный модуль назвать контроллером, хотя по сути там не больше десятка регистров и мультиплексоров. Но это вопрос конечно же терминологии и конкретной реализации.
По статье имею следующий комментарий:
Попытки программно управлять ногами контроллера с максимальной скоростью очевидно имеют предел, значительно меньший чем позволяют заточенные под это периферийные устройства. Тот же модуль выходного сравнения с привязкой на DMA очевидно даст точность, не хуже периода тактирования таймера. На входную последовательность в том же таймере есть модуль захвата. Обвязываем его с DMA и получаем высокую производительность.
Использование стандартных библиотек в контроллере для высокоскоростного программного управления ногами этой микросхемы, на мой взгляд, разработчиками этих библиотек и не предполагалось. А зачем, ведь есть аппаратная периферия, которая все это сделает, а CPU при этом может, например, кофе попить.lorc
02.09.2016 14:31Ну на AVRках его может и тяжело назвать контроллером. Но на всяких ARM-based микроконтроллерах (и тем более SoCах) он бывает довольно продвинутым. Правда, всё равно, подорзеваю что там большую часть логики занимает сопряжение с шиной (включая реализацию регистрового банка).
Попытки программно управлять ногами контроллера с максимальной скоростью очевидно имеют предел, значительно меньший чем позволяют заточенные под это периферийные устройства.
Собственно, мой комментарий был о том же. Незачем использовать GPIO для высокоскоростного обмена. Всегда можно найти более подходящую периферию для такой задачи.Alexeyslav
05.09.2016 08:46Основная идея ускорения работы с пинами — это высвободить время для выполнения другой задачи а не тратить много тактов только на переключение пина. В идеале это должна быть одна команда в 1 такт, а там уже пусть себе переключается хоть вечность.
monah_tuk
06.09.2016 13:56Если возникает задача управлять пинами с частотой даже в сотню килогерц — значит при проектировании что-то пошло сильно не так.
ваши бы слова, да богу в уши. Cypress FX3, при конфигурировании их интерфейса GPIF II в 32 бит режим начисто отпадает блок SPI. А он нужен. Пришлось эмуляцию делать на GPIO. И пришлось мириться, что обновление прошивки происходит меееееееедленно. А она всего ~2.5 Мб (FX3 + FPGA). На последней борде получилось сменить FPGA и изменить схемотехнику, теперь FPGA сам умеет вычитывать свой код из SPI — теперь хотя бы старт платы происходит очень быстро. Но прошивка всё такая же неспешная.
lorc
06.09.2016 21:16Так я же и говорю — при проектировании что-то пошло сильно не так. На этот самый FX3 должна существовать errata которая описывает вашу ситуацию (и возможные решеиня). Если вы при этом всё равно выбрали этот чип — значит это ваше осознанное решение и значит вы готовы выжимать всё возможное из GPIO.
Но это — исключение из правил. Обычно всё-таки стараются выбрать такой чип, в котором работает вся ключавая периферия в нужных режимах.monah_tuk
07.09.2016 02:14А вы найдите ещё адекватные реализации Device USB 3.0 (собственно это и есть его основное предназначение). Errata нет, на форуме советуют bit-bang, что собственно и сделано.
GarryC
01.09.2016 18:11Конечно же, последнее решение со специфическими командами — это реализация исключительно для AVR, я об этом сразу сказал.
А вот первый мой вариант с условной компиляцией будет жить где угодно, хотя на Edissone даст очень немного выигрыша (но все равно даст).
boolivar
01.09.2016 17:05Все безопасно в отключении прерываний: между чтением sreg и вызовом cli вполне может произойти прерывание, но прерывания должны оставлять sreg неизменным перед возвратом, хоть этим и должен озаботиться программист.
GarryC
01.09.2016 18:08А вот с этого момента поподробнее — почему это программист должен оставлять бит неизменным, конечно, управление конкретным прерыванием через глобальное нельзя отнести к хорошему стилю, но ведь это и не запрещено. Если бы в примерах от Atmel это было ясно прописано, то еще как бы сошло, но ведь такого нигде нет. По крайней мере, мне не встречались подобные ограничения, если Вы где то в примерах такое ограничение видели явно прописанным, бросьте ссылку.
boolivar
01.09.2016 19:08Вы о каком бите, простите? Я про весь регистр sreg целиком говорю, иначе просто ничего работать не будет.
GarryC
01.09.2016 22:27В том то и дело, что в AVR программа обработки прерываний должна сохранять на стек sreg сама и сама его восстанавливать при выходе. Но в процессе обработки она может модифицировать бит глобального разрешения и она имеет право это делать. Она также может восстановить sreg со стека, запретить прерывания (не влияя на флаги, кроме IE) и возвратить управление командой ret, чтобы не выставить его снова. И если она это сделает, то тем не менее не сможет прекратить обработку прерываний, поскольку мы восстановим сохраненный нами sreg с взведенным IE.
boolivar
01.09.2016 23:46Для выхода из прерывания используется reti. Иначе вы выйдете из прерывания с sreg отличным от того, каким он был до прерывания.
ploop
02.09.2016 00:00RETI просто взводит флаг IE при выходе из прерывания (sei + ret), так как при входе он сбрасывается. Собственно, SREG будет отличаться только этим флагом, и когда мы сохраняем его при входе в прерывание — он уже изменён. Это к комменту выше:
И если она это сделает, то тем не менее не сможет прекратить обработку прерываний, поскольку мы восстановим сохраненный нами sreg с взведенным IE.
GarryC
02.09.2016 09:41Еще раз утверждаю, и документация на микросхему со мной согласна, что sreg надо сохранять и восстанавлисать ручками.
ploop
02.09.2016 10:01Благо, если писать на Си, всё это сделает компилятор. Если не использовать всяких финтов типа вложенных прерываний.
GarryC
02.09.2016 11:27Посмотрите на код, который породил компилятор, я его привел во вкладках, и, Бога ради, покажите мне строки, порожденные компилятором для сохранения и восстановления sreg. Вы хотите спорить о вкусе бананов с человеком, который их ест в значительных количествах?
ploop
02.09.2016 11:47Либо плохо смотрел, либо не увидел ни одного обработчика прерываний. Приведите, пожалуйста, цитату по тексту.
В ваших примерах защищаются только атомарные операции.
Вы хотите спорить о вкусе бананов с человеком, который их ест в значительных количествах?
Поверьте, вы не единственный на этом свете пробовали бананы :)GarryC
02.09.2016 11:51+1Ну да, согласен, с прерываниями погорячился, их тут нет. Значит, действительно многие бананы употребляют.
boolivar
02.09.2016 11:10О чем тогда спор то? Оставляете sreg таким каким он был до прерывания и проблем нет. Содержимое восстанавливаете вручную, а I флаг восстановит инcтрукция reti. Начинаете выходить из прерываний инструкцией ret — получите проблемы.
GarryC
02.09.2016 11:38Вижу, до Вас не вполне дошел смысл вопроса. Речь идет о том, что подпрограмма обработки прерывания может принять решение об запрете прерываний вообще и принять соответствующие меры для его выключения, в том числе гасить бит и выходить инструкцией ret. Но вся эта работа пропадет, посокльку Ваша программа восстановит старое значение sreg с включенным битом.
boolivar
02.09.2016 12:21А, простите, я не хотел ввязываться в спор как решить проблемы которые будут если выходить из прерываний инструкцией ret. Внутри прерывания может быть сколько угодно инструкций ret/cli/sei, это допустимо, окончанием обработки прерывания является команда reti и она устанавливает флаг I в sreg, таким образом восстанавливает контекст, который был до прерывания, это распространяется и на вложенные прерывания: раз мы попали в прерывание — значит они были включены, не важно где мы были до этого, в контексте приложения или в контексте прерывания.
Пока вы не вызвали reti вы все ещё в прерывании.
ploop
02.09.2016 12:33+1Ваша программа восстановит старое значение sreg с включенным битом.
Вот этот момент хотелбы обсудить:
1) Вход в прервыние — флагI
сброршен аппаратно.
2) Сохранение SREG (флагI
сброршен)
3) Тело прерывания
4) Восстановление SREG (флагI
сброршен)
То есть, если выходить через RET, мы погасим прерывания. Такой финт тоже можно применить, если прерывание должно отработать разово, но я считаю это дурным тоном. Разбирающийся в таком коде должен проследить логику.
gorbln
01.09.2016 23:22Автор странный. Было влом пройтись по тексту автозаменой и поменять «А» на «ардуино»? Что за мода на сокращения в стиле совковых НИИ?
Далее. «Не читал, но осуждаю». Автор пользуется перепечаткой исходников на левом сайте. А в исходники самой ардуино заглянуть почему не захотелось? Причём, даже на сайте написано, где исходник лежит.
The digitalWrite() is defined in hardware/arduino/cores/arduino/wiring_digital.c as below.
И там (в исходнике), к слову, написано, почему отказались от инлайновых функций в пользу свича:
// Forcing this inline keeps the callers from having to push their own stuff
// on the stack. It is a good performance win and only takes 1 more byte per
// user than calling. (It will take more bytes on the 168.)
//
// But shouldn't this be moved into pinMode? Seems silly to check and do on
// each digitalread or write.
//
// Mark Sproul:
// — Removed inline. Save 170 bytes on atmega1280
// — changed to a switch statment; added 32 bytes but much easier to read and maintain.
// — Added more #ifdefs, now compiles for atmega645
//
//static inline void turnOffPWM(uint8_t timer) __attribute__ ((always_inline));
//static inline void turnOffPWM(uint8_t timer)
Дальше. То, что ардуино тормозная и неэффективная по использованию памяти — достаточно широко известно. Но, ардуинохейтеры, она не для того, чтобы бить рекорды по быстродействию. Это конструктор, и относиться к нему надо так же. Позволяет по-быстрому прикинуть конструкцию, что-то затестить. Не надо писать на ардуинке систему управления ядерным реактором. Так же как не надо систему управления ядерным реактором доверять писать пьяному студенту-первокурснику. Под каждую задачу должно быть своё средство.
Я так толком и не понял, за ассемблерным жонглированием, была ли предложена альтернативная функция DigitalWrite, работающая абсолютно с любым ардуино-совместимым железом (включая ESP8266, всякие Edison-ы), и при этом меньше жрущая памяти и процессорного времени?
P.S. Спасибо хоть, без указателей обошлось.
А вообще, ИМХО, проблема надумана. Кому важно получить выполнение функции не за 70 тактов, а за 25 — ну погуглите, как это сделать.
А ещё можно каждый пин выставлять отдельно, тратя по 70 таков на это. А можно порт писать…GarryC
02.09.2016 10:09Автор такой, какой есть. Да, Вы правы, именно в стиле советских НИИ, и я Вас уверяю, там были и высококлассные специалисты, так что прошу Вас поосторожнее с эпитетами. Стиль сокращений произошел в то время в том числе и потому, что функции автозамены на пишущих машинках не было.
Далее по комментарию — там такой же исходный текст? Если да (а так оно и есть) то в чем пафос замечания? То есть я разбирал действительно исходный текст, взятый с другого ресурса и это что то изменило?
И я совершенно не спрашивал, почему у них функция вместо инлайна, мне как то по барабану, как именно реализована совершенно излишняя функция, бесполезно отнимающая время и память. Я задавался вопросом, зачем она вообще включена в исходный текст, и в приведенном Вами комментарии я ответа на этот вопрос не вижу.
Я ни в коем случае не отношусь к ненавистикам А, и решения, предложенные в моем посте, направлены на улучшение быстродействия именно этой платформы. И если Вы можете улучшить работы своих скетчей в плане быстродействия, то я не понимаю, почему это не сделать.
Данная функция была именно предложена, жаль, что Вы ее не заметили, причем в двух вполне себе рабочих вариантах для любой архитектуры и еще в двух для конктерно AVR, причем два последних легко портировать на другие архитектуры, если почитать соответствующую документацию.
Если ассеблер представляет для Вас проблему, Вы могли не открывать соответствующие спойлеры, которые вставлены именно для тех, что в ассембелер разбирается, чтобы они могли проверить мои утверждения о быстродействии разных вариантов, а не принимать их на веру.
Не очень понял, чем именно Вам не угодили указатели, как таковые, может, Вы просто не умеете их готовить, просто на данной архитектуре они не очень хорошо реализуются, иначе бы я точно не обошелся.
И, простите великодушно, рекомендация писать порт — это как раз из разряда юнных гуру, о которых я упоминал в самом начале поста, со всеми недостатками, которые я пречислил.
Еще раз, без обид, но меня, честно признаюсь, задела фраза о совковых НИИ, что и вызвало, может быть, излишне резкий ответ.
mmMike
02.09.2016 09:11+1Много букв… не смог дочитать до конца.
Раздел «I/O Ports» в Atmel документации на 8 битные процы короче и понятнее. Да и другие спеки на микроконтроллеры лаконичны, полноценны и понятны.
Ардуино — зло. Когда описание библиотек превосходит по объему описание контроллера…GarryC
02.09.2016 10:12+1А — не зло, а инструмент, злом может быть только бездумное его использование.
И я не очень понял, что именно Вы хотите — уменьшить описание библиотек (категорически не согласен с таким подходом) или увеличить описание МК (берите STM, они реализовали Вашу мечту на 300+ страницах).mmMike
02.09.2016 10:39А — не зло, а инструмент, злом может быть только бездумное его использование.
Когда описание инструмента (обертка вокруг фактических вызовов) и время затраченное на его изучение начинает превышать время затраченное на изучение исходной документации… Это становится странным.
Да еще использование этой обертки не дает использовать многие фичи контроллеров!
Аа… впрочем, если только дергать ножками..
Как то пытался найти реализацию для работы в ультразвуковым датчиком HC-SR04 на STM32Fx. Лениво самому было писать.
И везде дурной "ардуиновский" (уже можно слово сделать нарицательным) подход. Цикл по опросу и дерганье ножек с задержками, реализованными циклам. В лучшем случае таймер с опросом регистра CNT в цикл для формирования задержки.
Пришлось по быстрому свою реализацию делать "по честному" с нормальным использованием возможностей таймера.
берите STM, они реализовали Вашу мечту на 300+ страницах
Кстати, весьма понятная и приятная документация. Больше возможностей контроллера — больше объем.
И глава "8 General-purpose and alternate-function I/Os (GPIOs and AFIOs)" занимает всего 35 страниц где 60% это картинки и таблицы.
Ну не бывает "универсальной" библиотеки оболочки на все контроллеры. Даже в пределах одной линейки полно тонких фич, которые нужно учитывать
Опять же, если задача стоить не светодиод зажечь/погасить. Да и в этом случае использовать "универсальную программную прослойку" при скудных ресурсах контроллера — это сомнительное "преимущество ардуино".
Объем прикладного кода на "подергать ножку" что в через "библиотеку" ардуино, что напрямую — практически одинаков.Alexeyslav
05.09.2016 09:02Объём кода одинаков, но объём ньюансов, которые необходимо учесть на конкретной платформе — не сопоставим. Например те же обновлённые меги — с индексами P и PA сильно отличаются в ньюансах некоторых мелочей, старые не умели к примеру одной командой аппаратно проинвертировать пин.
VBKesha
Честно скажу ещё не прочитал до конца. Но слишком много текста для чтения за один присест. Либо разбить на несколько частей, ну или хотя бы картинками украсить.
Boomburum
Написано же в начале, «Дню знаний посвящается» — первоклашкам сегодня тоже непросто :)
GarryC
Так и писал я этот пост долго, конечно, с перерывами, но недели две. А насчет картинок — во первых, не умею, а во вторых, я инженер старой закалки и позволяю себе только схемы и временные диаграммы, може, последние тут и были бы не лишние, но сильно понимание не облегчили бы, а просто для привлечения внимения неохота их ставить.
Bombus
Простите великодушно за подколку, но для кого писалась эта статья, для инженеров старой закалки?
GarryC
Ах добрые старые времена, когда мы соревновались в написании библиотек BCD арифметики на 580ВМ80 и хвастались друг перед другом уменьшением размера кода на 3 байта и увеличением скорости работы на 4 такта. В этом была своя романтика, которая, видимо, уже никогда не будет доступна современным инженерам, конечно, без обид.
Victor_Grigoryev
а) Вы не в твиттере
б) тема затрагивается больная, о ней нельзя коротко: уход от обфускации языка C в Arduino IDE к нормальному человеческому ассемблеру, пусть и не самым близким путём
soshnikov
Не-не-не. Как раз самое оно.
Взята строго одна тема и полностью рассмотрена одним блоком, а не размазана на несколько кусков.
Автору огромное спасибо, прочитал одним махом.