К вопросу о стандартных библиотеках

в 15:26, , рубрики: Программирование, программирование микроконтроллеров

Этот рассказ мы с загадки начнем,
Даже Алиса ответит едва ли,
Что остается от сказки потом,
После того, как ее рассказали?

Данное эссе будет посвящено различным темам, среди которых найдется место и ответу на вопрос, вынесенный в подзаголовок, а развиваться повествование будет в основном вокруг да около проблем, связанных с инициализацией периферии современного МК.
Итак, обозначим основные проблемы, связанные с настройкой аппаратной части МК: необходимость задания значительного количества параметров, из которых бОльшая часть не задается в каждом конкретном случае, но, тем не менее, не может быть оставлена произвольной, а должна принимать некоторые пред-определенные значения. Если Вы верите, что такая простая постановка задачи способна вызвать поток сознания и привести к некоторым не вполне очевидным решениям, то

Рассмотрим пример, связанный с настройкой набившего оскомину интерфейса, а именно ИРПС (Интерфейс Радиальный ПоСледовательный- именно под таким именем в девичестве выступал, ныне известный, как UART персонаж). Сразу отвечу на вопрос, почему русская аббревиатура- дело в том, что пишу я заметки частично в дороге, а переключение раскладок я бы лично удобной частью Андроид клавиатуры не назвал (кстати, если кто знает удобную клавиатуру со стрелками управления курсором и не тормозную, бросьте в личку, а то так неудобно тыкать в экран).
Конечно, поднятый круг вопросов не ограничивается только ИРПС, необходимо настраивать и прочую аппаратуру МК, но его я взял просто для примера. Так вот, для настройки работы ИРПС мы должны, как минимум, задать формат посылки, который определяется следующими параметрами — скорость передачи, количество передаваемых бит данных, наличие и тип бита четности, количество стоповых битов. И это только начало, на самом деле есть еще и расширенная конфигурация и она гораздо длиннее, но сейчас речь не об этом. Нужно также учесть, что в 70 процентов случаев при использовании ИРПС будет задана стандартная конфигурация 9600-8-0-1, еще в 25 процентах будет меняться только скорость передачи, и все остальные конфигурации поделят оставшиеся 5 процентов, но именно для них и должна существовать возможность настройки.

Прежде, чем мы начнем рассматривать различные варианты решения данной задачи, нам следует определиться с критериями оценки удачности того, либо иного варианта, иначе выбор более подходящего из них превратится в обсуждение вкусовых предпочтений. Я в своем посте исхожу в первую очередь из критерия надежности, поскольку считаю определяющими следующие обстоятельства: 1.человеку свойственно ошибаться, 2. задача компилятора состоит (в том числе) в том, чтобы указать на эти ошибки как можно раньше, а в идеале предотвратить их появление. Именно с таких позиций я буду рассматривать приемлемость того либо иного решения, если же Вы не согласны с хотя бы одним из вышеприведенных двух постулатов, то, скорее всего, данный пост Вам не очень понравится и Вам следует прекратить его чтение, хотя не возбраняется данное занятие продолжить и (обязательно аргументированно) изложить свою позицию в комментариях.

Для начала поймем, почему такая задача возникла, ведь в МК предыдущих поколений (PIC, AVR, 51) мы прекрасно справлялись с настройкой аппаратуры путем прямых записей в соответствующие регистры? Ну, прежде всего, для этого надо знать регистры, или, как принято нынче выражаться, курить мануалы, а там много букв и, хотя лично мне это занятие не представляется особо напрягающим, тем не менее, особенно учитывая качество нынешних мануалов, для значительной части сообщества такой подход может действительно представлять проблему.

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

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

Небольшая реминисценция на тему документации. Изучал недавно описание на один кристалл (фирмы Intel, между прочим) и там имеется вход с говорящим названием PE_RST_N, который описан как +3.3Vdc вход, который, будучи утвержден (asserted), сигнализирует о наличии питания и тактовой частоты. Как именно вход утверждается — высоким или низким уровнем, не указано, временных диаграмм для этого сигнала не приведено (хотя на временной диаграмме есть сигнал PERST# и в диаграмме состояний прибора он упоминается именно с таким названием), далее по тексту есть фраза о том, что значение 0b индицирует активность сброса, в таблице режимов работы при наличии сброса стоит галочка в графе значение, что как бы намекает. В общем, по совокупности косвенных признаков можно сделать вывод, что активный уровень на этой ноге, приводящий к сбросу прибора, таки да, низкий, но почему я должен догадываться, вместо того, чтобы просто прочитать соответствующее ясное, четкое и понятное описание в документации?
Почему меня заставляют вступать на зыбкую почву догадок и предположений? Наверное, лично для меня это наказание за плохую карму в прошлых жизнях (ну и в этой я ангелом не был), но остальные разработчики в чем провинились? Один мой коллега высказал интересную гипотезу, что такую документацию делают специально, чтобы затруднить нам поднятие с колен, но зачем тогда она написана на английском языке? Или же она сделана специально для нас, а на Западе пользуются настоящей, правильной документацией? Хотя в данном случае как нельзя лучше подходит фраза «Не следует объяснять злым умыслом то, что можно объяснить простой ленью»(не буду обижать создателей такой документации дословным цитированием).

Но настоящий шедевр в моей личной коллекции — это описание одного, в общем-то неплохого МК производства отечественной фирмы, в документации на который был описан бит управления подключением подтягивающих резисторов следующим образом: «0- подтягивающие резисторы выпадают», 1-… угадайте, как там написано? — «подтягивающие резисторы не выпадают» — неплохая попытка, но не угадали … барабанная дробь в студию … «значение, противоположное 0». Все аплодируют, занавес.

Но все вышеперечисленное (это я про неважное качество документации, если кто уже забыл) только часть проблемы, вторая ее составляющая
заключается в том, что современные МК действительно сложнее своих предшественников. У меня есть своя точка на то, почему происходит усложнение аппаратуры и «все более полное удовлетворение непрерывно растущих потребностей разработчиков» не стоит на первом месте в списке причин данного явления, но ситуацию в целом мои размышления на этот счет не меняют — современные МК действительно сложнее своих предшественников, в них намного больше аппаратных блоков и сами блоки стали намного сложнее, реализуют дополнительные функции, которые разработчики могут (и должны) использовать с пользой для дела. Я тут замыслил пост на тему некоторых фич (действительно полезных) в UART семейства STM, когда закончу этот, обязательно напишу, как раз при реализации использования этих особенностей я и задумался над проблемами, вызвавшими к жизни данный пост.

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

#define device xxxxx

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

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

UARTInit(9600,8,0,1);

для вышеприведенного стандартного случая (здесь и далее оставим за скобками вопрос о выборе одного настраиваемого канала аппаратуры из существующих в МК). Вроде бы тут все нормально и ничего сверхъестественного нам писать не приходится, но попробуем найти недостатки в данном варианте и (разумеется, иначе зачем искать) устранить их.

Прежде всего, надо определиться, что мы вообще хотим от процедуры настройки аппаратной части и какие требования к ней предъявляем. На мой взгляд, программа должна быть прежде всего надежной, безопасной, и понятной. Требования эффективности не столь значимы, поскольку инициализации, как правило, проводится один раз и может быть не слишком быстра, но если она будет компактна по памяти и нетребовательна по скорости, то это дополнительный плюс при оценке. Кстати, по меньшей мере одна технология, а именно Чарлиплекинг, требует оперативной смены режима работы ножек МК, поэтому совсем уж пренебрегать быстродействием не стоит.

Какие же недостатки мы видим в данной технологии (а мы их видим, иначе о чем писать дальше) — прежде всего, это необходимость перечислять большое количество параметров, причем в строго определенном порядке, и если мы где-то ошибемся, то результат будет совсем не тот, на который мы рассчитывали. Вопрос с порядком параметров можно несколько ослабить, если определить пользовательские типы данных для каждого параметра, тогда получим:

typedef enum {…UARTSpeed9600…} UARTSpeedT;
void UARTInit(UARTSpeesT UartSpeed,…};

, и при попытке ошибиться мы получим предупреждение от компилятора. Данные подход не стоит ничего на этапе исполнения и совсем немного на этапе компиляции, поэтому я могу его настоятельно рекомендовать и одновременно выразить тягостное недоумение по поводу того, что авторы (в том числе фирменных) библиотек таким простым и в то же время эффективным способом пренебрегают.

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

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

Если мы опять таки работаем с продвинутым языком типа С++, то у нас есть еще один метод — перегрузка оператора присвоения, хотя лично меня повергает в уныние перспектива написания (4+4*3+4+1=21) функций присвоения с жестким порядком аргументов и еще более невообразимого числа функций присвоения с произвольным порядком. Тем не менее, такая возможность существует, и правила приличия требуют ее упомянуть, хотя не обязывают использовать.

Если бы у нас был хороший препроцессор, который действительно предоставлял бы нам возможности макроязыка, то мы могли бы написать вызов макрофункции с переменным числом типизированных аргументов и получить сгенерированный препроцессором вызов собственно функции, но у нас его нет (ну нет у нас в С хорошего препроцессора, у нас то и среднего нет). Если кто то из читателей этого поста считает стандартный препроцессор к языку С действительно макроязыком, то я настоятельно рекомендую ознакомиться с описанием макропроцессора языка ассемблер для машин фирмы DEC, и потом можно будет подискутировать на эту тему более предметно. Но только в таком порядке, причем я не исключаю и знакомства с другими развитыми средствами генерации кода. Тем не менее, нам такой подход недоступен в силу ограниченности выразительных средств, а писать свой собственный препроцессор мы не будем, хотя иногда и хочется. На этой пессимистичной ноте мы завершим рассмотрение вариантов с использованием функции инициализации с прямой передачей параметров.

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

Разумеется, если мы напишем функцию установки значений по умолчанию и вызовем ее для управляющей структуры, планируемой к использованию, то никаких проблем не будет. А если мы забудем это сделать? В лучшем случае мы получим немного не то, что мы ожидали, в случае средней тяжести получим ну совсем не то, что мы ожидали, в весьма плохом случае мы получим настолько не то, что повредим аппаратуру, а вот в самом плохом случае мы получим все вышеперечисленное внезапно, то есть в некоторых редких и плохо повторяемых ситуациях. Это я все к тому, что задача отнюдь не надумана.

Опять таки, если мы используем С++, то конструктор является естественным ответом на наши чаяния, а если еще и прикрутить умные указатели, то решение близко к идеальному, но мы то по прежнему остаемся в рамках чистого С, так как это путь самурая (путь постоянной готовности к Segmentation Fault).

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

А что мое поведение есть девиация и именно отклонение от мэйнстрима, подтверждается изучением исходных текстов известных программных пакетов, в том числе от STM и TI. В силу непонятных соображений авторы данных пакетов считают, что все должны знать все обо всех, что достигается использованием вложенных включений заголовочных файлов и защитой от повторного включения. То есть, если вдруг модуль работы с ИРПС не сможет узнать распределение битов в регистре управления USB хостом, то он будет «страдать, чахнуть и даже … эээ … умретъ».

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

Так что они (STM и TI) как хотят, а я буду продолжать придерживаться принципа «Чем меньше знаешь, тем лучше спишь», или, в другой формулировке «То, о чем Вы не знаете, не может вызвать у Вас беспокойства». Поэтому я считаю, что пользователю библиотеки ни на фиг не сдалась информация о ее внутреннем устройстве, хотя в рамках С мы обязаны ее предоставить (а как все классно было в Turbo Pascal с его концепцией Unit, но что мечтать о несбыточном, нам уже сказали, что их не будет и в С++17). Так что у нас где-то будет выражение типа

Typedef struct {
	UARTSpeedT UARTSpeed;
…
} UARTConfigT;

и ее появление так же неизбежно, как победа коммунистического труда. Но никто нас не заставляет сделать еще один шаг по дороге в трясину и задавать значения управляющей информации в стиле

UARTConfigT UARTConfig;
UARTConfig.UARTSpeed=UARTSpeed9600;

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

void UARTSetSpeed(UARTConfigT *UARTConfig, UARTSpeedT UARTSpeed);
UARTSetSpeed(&UartConfig, UARTSpeed9600);

Прошу прощения за насколько тяжеловесный стили именования функций, но если лишние 20 символов в названии позволили Вам сэкономить полчаса отладки, то Вы выиграли.
Данный подход, помимо того, что следует принципам ООП, имеет и утилитарное значение — если мы используем С++ (но мы его не используем, не забыли?), то мы можем написать одну перегруженную функцию и использовать ее для задания различных параметров в стиле

void UARTSetParam(UARTConfigT *UARTConfig, UARTSpeedT UartSpeed);
void UARTSetParam(UARTConfigT *UARTConfig, UARTParityT UartParity);

и так далее, что позволяет нам снизить нагрузку на мозг пользователя за счет снижения количества необходимых для запоминания имен функций.
Конечно, мы должны понимать, что «ДарЗаНеБы» (отдельный привет тем, кто ценит Хайнлайна) и обращение к сеттеру потребует большего времени при исполнении и большего объема памяти для хранения кода по сравнению с прямым присвоением значения полю (хотя для inline функций данное утверждение и небесспорно), но, с моей точки зрения, плюсы перевешивают.

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

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

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

Какое же решение я считаю приемлемым, после того, как смешал с пищей для воробьев все остальные? Это решение комплексное, то есть оно позволяет и обеспечить значения по умолчанию, и дает возможности выборочного изменения параметров, и исключает ошибки пользователя и снижает уровень холестерина в крови и делает еще кучу полезных и нужных вещей, как и свойственно по настоящему комплексному решению. И еще одно его немаловажное свойство оно написано на чистом С, то есть путь бусидо не потерпел урона.

Ведь можно сколько угодно рассказывать о плохих свойствах японской стали и, связанной с этим, невозможности классического фехтования клинок в клинок, но это очень красиво решить схватку одним ударом, начинающимся с выхватывания клинка из ножен и завершающимся возвращением его обратно одним слитным движением со стряхиванием капель крови неудачливого оппонента по дороге. А что касается критиков, недавно прочитал замечательную фразу «критика импотентами Дон Жуана может быть объективно справедливой, но все равно имеет неприятный оттенок».
Честно говоря, испытываю сильнейшее искушение на этом моменте пост прервать и оставить читателя в сильнейшем недоумении и разочаровании, но все-таки подвергнем его дальнейшим испытаниям и позволим получить разочарование другого вида от того, что так красиво описанное решение оказалось неуклюжим и неудобным.

Итак, вот оно.

UARTConfigT UARTConfigInit(void) {
	UARTConfigT UARTConfig;
	UARTConfig.UARTSpeed=UARTSpeed9600;
	return UARTConfig; 
};

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

UARTConfigT UARTConfigSpeed(UARTSpeedT UARTSpeed,UARTConfigT UARTConfig) {
	UARTConfig.UARTSpeed=UARTSpeed;
	return UARTConfig; 
};

Обратим внимание на порядок задания параметров, преимущества такого решения мы видим в строке

UARTConfigSpeed(UARTSpeed4800,UARTConfigParity(UARTParityEven,UARTConfigInit()));

где значение параметра следует сразу после имени функции, что более обозримо по сравнению со следующим выражением

UARTConfigSpeed(UARTConfigParity(UARTConfigInit(),UARTParityEven),UARTSpeed4800));

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

UARTConfigSpeed(UARTSpeed4800,
 UARTConfigParity(UARTParityEven,
  UARTConfigInit()
 )
);

но это уже вопрос вкуса и обсуждению не подлежит по определению — о вкусах не спорят.

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

int UARTConfigUse(UARTConfigT UARTConfig) {
 return DO_something(); // собственно настройка с возможной ошибкой 
};

и наш пример в итоговом виде будет выглядеть, как

UARTConfigUse(
 UARTConfigSpeed(UARTSpeed4800,
  UARTConfigParity(UARTParityEven,
   UARTConfigInit()
  )
 )
);

В качестве вишенки на торте покажем, что можно контролировать и возможность последней ошибки — пропуска использования сформированной структуры, к сожалению, эта возможность не относится к стандартным языковым средствам и наличествует только в GCC семействе — предупреждение об игнорировании возвращаемого значения, для чего функции инициализации и настройки должны быть описаны, как __attribute__((warn_unused_result)). Сработает ли этот метод конкретно у Вас, зависит от разработчиков компилятора, например, в KEIL сработало, а в IAR нет (ни в коем случае не в умаление IAR, я к нему хорошо отношусь и использую, но никуда не денешься от факта). Существуют и другие решения данной проблемы, основанные на макросах и некоторой промежуточной переменной времени компиляции, но они, к сожалению, не гарантируют результат.

Почему приведенный способ настройки лично я не могу считать идеальным, хотя он решает все задачи по выразительности и надежности? Исключительно в силу его невысокой эффективности с точки зрения затрат на исполнение, говоря проще, в силу его исключительной прожорливости по времени.
Пришло время ответить на вопрос, заданный в эпиграфе к настоящему посту, применительно к возврату функцией результата не-примитивного типа. Ведь мы не можем вернуть указатель на нашу внутреннюю переменную, поскольку получим предупреждение компилятора (а если предупреждения отключить, то тогда могли бы, ага).
Как же компилятор решает это задачу? Не знаю, как это должно быть, я не читал стандарт языка С (да, это так, и мне не стыдно в этом признаться), не уверен, что там описаны тонкости реализации, но относительно IAR я просто смотрел сгенерированный код на ассемблере и увидел там следующий механизм. При входе в функцию (в том числе main), которая вызывает функцию, возвращающую не-примитивное тип, выделяется место на стеке, достаточное для хранения переменной данного типа. Далее вызванная функция инициализации осуществляет побитное копирование своей внутренней переменной на стек под свой адрес возврата и после завершения ее работы на верхушке стека лежит значение возвращенного результата. Перед вызовом функции изменения значения параметра это значение копируется на стек и передается ей в качестве аргумента, а результат опять побитно копируется со стека в выделенную область памяти и доступен оттуда для следующей функции изменения значения параметра.
То есть некоторый фрагмент данных (а он может быть весьма значительного размера) постоянно таскают между выделенной областью памяти (на стеке, между прочим) и верхушкой стека, и, разумеется, это не способствует быстрой работе.

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

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

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

С учетом вышеперечисленных соображений, получаем следующую модификацию решения:

typedef UARTConfigT *UARTConfigPT; 
UARTConfigPT UARTConfigInit(void) {
	static UARTConfigT UARTConfig;
	UARTConfig.UARTSpeed=UARTSpeed9600;
	return &UARTConfig; 
};
UARTConfigPT UARTConfigSpeed(UARTSpeedT UARTSpeed,UARTConfigPT UARTConfigP) {
	UARTConfigP->UARTSpeed=UARTSpeed;
	return UARTConfigP; 
};
int UARTConfigUse(UARTConfigPT UARTConfigP) {
 return DO_something(); // собственно настройка с возможной ошибкой 
};

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

Кстати, идеальным решением, быстродействующим и не занимающим памяти было бы получение указателя на возвращаемую функцией инициализации структуру с последующей передачей ее по цепочке вызовово, но компилятор жестоко обломал крылья моей фарнтазии, заявиви, что опереатор & применим только к L-выражению. Я, в общем то, понимаю, что это означает, то так и не понял, за что они со мной так. жестоки.

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

Автор: GarryC

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js