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


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

Рассмотрим пример, связанный с настройкой набившего оскомину интерфейса, а именно ИРПС (Интерфейс Радиальный ПоСледовательный- именно под таким именем в девичестве выступал, ныне известный, как 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-выражению. Я, в общем то, понимаю, что это означает, то так и не понял, за что они со мной так. жестоки.

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

Комментарии (35)


  1. CodeRush
    15.06.2016 18:47
    +1

    Плюс поставил, хоть и стена текста. Одно только покоробило: PE_RST_N и PERST# — выделенное сразу дает понять, что активный уровень этого сигнала — низкий, и это не недостаток качества документации, а недостаток знаний принятых в индустрии обозначений.


  1. GarryC
    15.06.2016 19:02

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


    1. CodeRush
      15.06.2016 19:19
      +2

      Автор схемы, скорее всего, под PE_RST_N имел в виду вывод микроконтролера, т.к. именно так он назван в даташите на него. PERST# — имя сигнала (или сети) PCIe reset, к которому нога PE_RST_N подключена. Две разные сущности — два разных имени, ничего необычного.


  1. Mirn
    15.06.2016 21:06

    я в SPL от STM'a (стиль: заполнил структуру и вызвал функцию)
    делал существенно проще:

    задача: сделать загрузчик 8..16к по GPRS из готовых наработок GPRS которые после компиляции занимают почти 30к.

    проблема: SPL занимает чуть больше 50% кода.

    причина проблемы: в SPL используются функции и они работают со всеми видами переферии, т.е. внутри куча switch-case для всех случаев жизни которые в моём проэкте не используются

    решение: все используемые функции у SPL копируем в инклудники и подписываем к ним static inline, и добавив к имени функций суффикс _inline, благо они используются почти все только один раз. И чуток дорабатываю свой код добавив _inline суффикс

    результат: компилятор собирает проект аж целых 10 секунд, но зато использует только актуальные куски кода и не шараборится с структурами.

    Итог: код стал 17к, после я ещё помелочи не куроча логики работы убрал лишнее и свистелки и перделки типа подробных логов с выводом таймингов (а так же всякие анархизмы типа impure_data and ..._ptr) и код стал 11к. При этом вся логика не изменила своего вида за исключением суффикса SPL функций.

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

    но оно того стоит?
    по-моему на собственно саму логику проекта уходит 90% всех сил
    и нужно быть просто внимательным и аккуратным.


    1. rrrav
      15.06.2016 22:14

      Судьба SPL весьма туманна после появления HAL. Генератор кода STM32CubeMX тоже весьма приятная штучка, заметно облегчает порог входа. А сгенерированный код можно потом допиливать путем удаления лишнего.


      1. Mirn
        15.06.2016 22:21
        +1

        Это микроконтроллеры — а значит кристаллы не изменятся и никто не заставляет переходить на хал.
        Вот новые типа М7 и выше, да проблема.


        1. rrrav
          16.06.2016 10:52

          Возможно в HAL тоже можно применить ваш способ облегчения кода (если без HAL не обойтись). Там, правда, универсализация кода пошла еще дальше, в сравнении с SPL.


          1. Mirn
            16.06.2016 11:18
            +1

            не думаю что можно.
            SPL хорош тем что он на входе имел структуру и немного параметров, а на выходе изменял состояние переферии и почти не содержал глобальных переменных, глобально статических и тд, а так же не занимался обработкой прерываний.
            SPL по сути набор макросов близкий к идеальной функции без сайд-эффектов. Некий перекидыватель данных стркутур в регистры переферии.
            Одна функция — одно действие над одним блоком, нет такого чтоб можно было активировать одновременно UART and DMA.

            HAL же пошёл дальше — он работает более высокоуровнево: в нём есть например функция которая настраивает и уарт и дма, их связывает и в связке посылает данные обрабатывая прерываний. Я бы не рискнул ТАКОЕ инлайнить — явно там есть и обработчик прерывания, и внутренние состояния и циклы ожидания и прочие «прелести».

            В добавок у HAL есть большие проблемы с поддержкой errata от той же СТМ — он её игнорирует в многих вопросах, при этом все действия скрыты в нём и вмешаться в них сложно не залазя в код хала. Что очень жирный минус.

            Вот например я сейчас мучаюсь с USB CDC. Добился скорости выше 400кбайт/сек для приёма и передачи, но не одновременно. При одновременной работе всё рушится — просто железяка перестаёт разом давать прерывания и на приём и на передачу войдя в аппаратный дедлок. А править кубовый усб это АД из сплошных нарушений всех правил данной статьи (чего стоит многократно вложенные указатели на фукнции которые возвращают структуры с указателями на функции). А причина всех бед наверное в этом пункте ерраты: http://i.prntscr.com/cac94bb65ca642caab59656105118e82.png. Так же у хала прочие пункты ерраты не учтены, например проблемы с каналом А0 ацп в сотой серии.


            1. rrrav
              16.06.2016 11:37

              А править кубовый усб это АД из сплошных нарушений...

              — это точно, пробовал завести USB HID на CubeMX — заработать вроде заработал, но с глюками. А код такой, что лучше и не видеть. В итоге взял кусок из прежних наработок.


      1. GarryC
        16.06.2016 10:47

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


        1. rrrav
          16.06.2016 11:17

          Ну есть и определенные преимущества. Очень здорово, на мой взгляд, сделана в CubeMX страница Clock Configuration.
          Еще можно легко протестировать различные варианты конфигурации периферии, даже если нет желания использовать сгенерированный код (и вправду тяжеловесный «макаронный») в конечной реализации.
          В определенный момент проект можно оторвать от CubeMX (правда, вернуться уже не получится) и уйти в свободное плавание.


          1. GarryC
            16.06.2016 13:30

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


            1. rrrav
              16.06.2016 14:51

              Ну так есть же такое — Wizard в Кейле. Как раз в комментах и сидит, вот так примерно:

              //*** <<< Use Configuration Wizard in Context Menu >>> ***
              
              
              /*
              // <h> USB Configuration
              //   <o0> USB Power
              //        <i> Default Power Setting
              //        <0=> Bus-powered
              //        <1=> Self-powered
              //   <o1> Max Number of Interfaces <1-256>
              //   <o2> Max Number of Endpoints  <1-16>
              //      <i> Number of Bidirectional Endpoints
              //   <o3> Max Endpoint 0 Packet Size
              //        <8=> 8 Bytes <16=> 16 Bytes <32=> 32 Bytes <64=> 64 Bytes
              //   <o4> Bytes in output/input report <1-64>
              //   <h> Double Buffer Setup
              //      <i>Double Buffer not yet supported
              //<i>     <i> Enable Double Buffer Mode for selected Bulk Endpoints
              //<i>     <o5.1>  Endpoint 1
              //<i>     <o5.2>  Endpoint 2
              //<i>     <o5.3>  Endpoint 3
              //<i>     <o5.4>  Endpoint 4
              ................
              


  1. fki
    15.06.2016 22:39

    «в С мы можем возвращать любой тип, за исключением массива»
    я бы переформулировал, указатель на массив мы возвратить можем

    char * returnArray()
    {
    static char a[3] = { 0, 0, 200 };
    return a;
    }

    int main(int argc, char ** argv)
    {
    return returnArray()[2];
    }


    1. GarryC
      16.06.2016 11:01

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


  1. Amareis
    16.06.2016 13:34

    Меня одно интересует, вы это всё с телефона написали? Такое упорство заслуживает одобрения уже само по себе )


    1. GarryC
      16.06.2016 13:52

      Ну, конечно, не с телефона, а с планшета, у меня Perfeo 9716 (их больше не делают), но в основном да — по пути на работу и обратно. Кстати, увлекательнейшее занятие, время поездки пролетает просто незаметно, даже лучше, чем когда что-либо читаешь. На ПК делал только окончательное оформление.


  1. john-brown
    16.06.2016 15:27

    Очень хорошо написано и интересно читать. Спасибо. Единственное, что меня напрягло: если Вы считаете, что программист должен отслеживать структуру модулей в продукте, то почему бы ему не перестать забывать сбросить поля структуры настройки перед конфигурированием?

    PS. Видел интересную идею вроде задания
    #define UARTConfigDefault {1,2,3}
    и последующей инициализации структуры вроде
    UARTConfigT UARTConfig = UARTConfigDefault;


    1. GarryC
      16.06.2016 15:30

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


      1. john-brown
        16.06.2016 15:34

        Да, вы правы. Совсем забыл, что вопрос рассматривается с точки зрения создателя библиотеки. Спасибо.


  1. Amomum
    16.06.2016 16:21

    Я (как, вероятно, каждый второй) для STM писал свою обертку над UART'ом и я для инициализации использовал вот такую сигнатуру:

    void init( Pins pins, uint32_t baudrate, GPIO_TypeDef * rs485Port = 0, uint16_t rs485Pin = 0 );
    


    При этом Pins — это строгий enum (аналог enum class средствами С++03):
        STRONG_ENUM( Pins, UART1_PA9_PA10,  UART1_PB6_PB7,
                           UART2_PA2_PA3,   UART2_PD5_PD6,
                           UART3_PB10_PB11, UART3_PD8_PD9, UART3_PC10_PC11,
                           UART4_PC10_PC11,  
                           UART5_PC12_PD2 );
    


    Получается довольно удобно. Последние два опциональных параметра задают порт и ногу для переключения преобразователя UART-RS485 (который приходится использовать довольно часто).

    Бодрейт может быть практически какой угодно, поэтому для него enum заводить смысла нет.

    Единственный минус — соответствие моего объекта uart'a и значения Pins проверяется только во время выполнения, но как это красиво проверять при компиляции я не придумал.


    1. GarryC
      17.06.2016 12:30

      Ваш вариант выходит за путь самурая, о его недостатке я чуть сказал в тексте, но самое главное — посыл поста был в том, как ЗАСТАВИТЬ пользователя вызвать функцию инициализации, а тут мы его всего лишь уговариваем.


      1. Amomum
        17.06.2016 12:47

        В таком случае, я, видимо, не до конца понял ваше решение. Как вы можете заставить пользователя вызвать вашу функцию? Это ведь не конструктор.


        1. GarryC
          22.06.2016 21:09

          Если Вы не вызовете функцию инициализации, то не получите указателя. который дальше пропихивается через функции изменения в функцию использования.


          1. Amomum
            23.06.2016 11:14

            Ну ведь никто не запретит пользователю просто создать указатель такого типа? Ну, если пользователь совсем дуб.
            А если не совсем, то он и так понимает, что UART инициализировать нужно, т.е. и особого принуждения не требуется.


  1. mark_ablov
    16.06.2016 18:23

    Угу. хотелось бы писать как-то так:

    UARTCreate()->setParity(XXX)->setBandwidth(XXX)->init();
    


    1. GarryC
      17.06.2016 12:27

      Если Вы можете позволить себе С++, то, конечно, так можно и, наверное, даже нужно, единственный вопрос, как гарантировать последний вызов?
      Гарантия первого у Вас есть. Но гарантия последнего вызова и у меня делается нестандартными средствами, что не есть хорошо.


  1. LennyB
    16.06.2016 21:04

    Вариант без статической структуры, о которой не надо знать пользователю.

    Либа:
    typedef struct UARTConfigT
    {
    UARTSpeedT speed;
    UARTStopT stop;
    UARTParityT parity;
    } UARTConfigT;

    int UARTConfigUse(UARTConfigT config)
    {
    return DoSomething();
    }

    // инициализация со значениями по умолчанию
    #define UARTConfig(...) UARTConfigUse((UARTConfigT){.speed = UARTSpeed9600, .stop = UARTStop1, .parity = UARTParityNone, __VA_ARGS__})

    Инициализация с нужными параметрами:
    UARTConfig(.speed = UARTSpeed115200, .parity = UARTParityEven);


    1. GarryC
      17.06.2016 12:37

      А так можно?
      Вариант интересный, сейчас попробую.


    1. GarryC
      17.06.2016 13:10

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


      1. LennyB
        17.06.2016 13:39

        Мне как раз нравится из-за названий, по-моему, очень читабельно, особенно если параметры не такие очевидные, как UARTSpeed115200. И узнал я о таком способе весьма недавно из книги «21st Century C» (Ben Klemens).


  1. LynXzp
    16.06.2016 23:24

    UARTConfigSpeed(UARTSpeed4800,UARTConfigParity(UARTParityEven,UARTConfigInit()));
    А если что-то сложнее? (Библиотеки инициализации должны быть в одном стиле)
    Я обычно шучу: первый оператор в выражении снимает с предохранителя, второй подает патрон в патронник, третий направляет в ногу, четвертый…
    По-моему это очевидный минус такого подхода. И не знаю что лучше/хуже так или обьявление структуры с возможностью забыть инициализацию.

    P.S. Неужели структура после Вашей строки остается в памяти?


    1. GarryC
      17.06.2016 12:39

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


      1. LynXzp
        17.06.2016 13:08

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


        1. GarryC
          17.06.2016 13:15

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