Жизнь настолько коротка, что ее едва хватает на то, чтобы совершить необходимое количество ошибок, а уж повторять их — недопустимая роскошь.
В данном посте речь пойдет о том, чтобы не повторять чужих ошибок, что тоже является непроизводительной растратой столь ценного ресурса, как время. И вроде бы ошибка не столь фатальна и есть масса примеров, где она исключена и можно было бы давно научиться ее избегать, но почему то с упорством, достойным лучшего применения, она встречается вновь и вновь в исходных кодах программ для МК (может быть и для больших систем тоже, но я ими не занимаюсь), причем авторы данных программ не то чтоб новички во встроенном программировании, но тем не менее мы видим то, что видим. Искренне надеюсь, что после того, как данный пост будет Вами прочитан (при попытке ввести сочетание «после прочтения» в строго определенном месте текста у меня 6 раз падал Word To Go — впервые за 2 года использования, так что я смирился и написал чуть по другому — это к вопросу об ошибках, хотя данное поведение вряд ли проистекает именно из за той, о которой я пишу, иначе это было бы особенно пикантно). Вы навсегда поймете недопустимость подобной ошибочной конструкции и не наступите именно на эти грабли, ведь вокруг лежит такое количество других, ожидающих своей очереди.
Сформулируем задачу – нам необходимо взаимодействовать с некоторым ресурсом (аппаратным, но не будем ограничивать себя), который в силу внутренних особенностей не всегда готов к работе (требует времени для выполнения текущей операции, но опять таки предпочтем расширенную формулировку). Для того, чтобы определить готовность ресурса, существует некая процедура проверки состояния и собственно взаимодействие следует осуществлять при достижении определенного значения этого состояния. Если перейти на язык конкретики, то в примере, который мне попался на глаза, рассматривался процесс передачи данных по интерфейсу SPI в МК фирмы ATMEL (ну в общем, опять речь об Ардуино, дальше можно не читать).
Так вот, задача состоит в организации подобного взаимодействия с учетом вышеуказанных особенностей, нам потребуется проверка готовности и инициализация операции и вопрос в том, в каком порядке их применять. Поскольку мы имеем только две сущности, расположить их можно только двумя способами – первая впереди либо вторая. Если перевести на язык конкретной задачи, вопрос состоит в том, следует ли проверять готовность устройства до передачи, ожидая таковую, либо можно сначала осуществить передачу, а потом дождаться ее завершения.
Давайте посмотрим на код, вот первый (ошибочный) вариант, взятый из рассматриваемой реализации:
static inline void writeSPI(const byte b) {
SPDR = b;
asm ("nop");
while ( ! (SPSR & bit(SPIF)) );
};
И поскольку он (кроме третьей строки) совпадает с рекомендованным фирмой изготовителем в описании микросхемы и примерах применений, то будем критиковать далее решения от фирмы ATMEL. А вот и второй (правильный) вариант, предлагаемый как альтернатива в тех же обозначениях:
static inline void writeSPI(const byte b) {
while ( (SPSR & bit(SPIF)) == 0 );
SPDR = b;
}
Кстати не могу не отметить правильное применение ключевого слова static и const в сигнатуре функции, inline рассмотрим как нейтральное решение.
В принципе, можно было бы и завершить на этом обсуждение, просто применяйте правильный вариант и все, но, поскольку неправильный встречается снова и снова, придется доказать, что второе решение лучше первого как в данном конкретном случае, так и в общем. Для человека, который получал образование в 80е годы прошлого века совершенно естественно, что коды 105737 177564 100375 должны быть расположены перед кодами 112737 000060 177566, именно так и надо делать и это даже не обсуждается (привет, коллеги), а для всех остальных продолжим.
ИНВАРИАНТЫ
Первый вариант требует, чтобы 1)при входе в функцию устройство было готово к обслуживанию, более того, подразумевается, 2)что однажды сформированная готовность не может быть отменена иначе, чем инициализацией операции. Если второе утверждение для данного конкретного устройства верно, то первое отнюдь не гарантируется при начале его использования. Второй вариант никаких ограничений не накладывает (вернее накладывает, но они исчезающе малы по сравнению с первым), что несомненно является плюсом, поскольку расширяет область применения. Конечно, они (здесь и далее по тексту «они» — это те кто использует первый вариант, в частности программисты фирмы ATMEL) скажут, что в данном конкретном случае все инварианты выполняются и будут правы, но мы ведь предпочтем универсальный вариант – 0:1 в нашу пользу.
ПОСЛЕДОВАТЕЛЬНОСТЬ
В любой процедуре использования ресурса можно выделить три фазы – начало работы, повторения, конец работы. Первый вариант неплох в первой фазе (если инварианты выполнены), хорош во второй, но в третьей выполняет излишнюю работу – дожидается готовности ресурса, который нам больше не понадобится. Второй вариант неплох в первой фазе (выполняет только немного лишней работы в случае выполнения инварианта), хорош во второй, и хорош в третьей – вовремя прекращает работу. Мы понимаем, что такой подход противоречит «принципу бойскаута», которого придерживается первый вариант, но в реальной жизни скауты не всегда являются примером для подражания. Конечно, они скажут, что лишней работы на так много и она в пользу ближнего своего, и будут правы, но все таки – 0:1 в нашу пользу.
СОВМЕСТИМОСТЬ
Первый вариант будет иметь проблемы при взаимодействии со вторым, второй будет прекрасно себя чувствовать, даже если кто то придерживается первой стратегии. Конечно, они скажут, что не следует смешивать стратегии, и будут правы, но мы в реальной жизни, а тут все бывает -0:1 в нашу пользу.
БЕЗОПАСНОСТЬ
Вот с этого момента поподробнее. Допустим, у нас есть конкуренция за ресурс, тогда мы должны рассмотреть интервал уязвимости – чем он короче, тем в большей безопасности мы находимся. В первом варианте это интервал от начала операции до получения готовности устройства, во втором – от получения готовности устройства до начала операции, и это интервал в разу (а то на порядки) меньше первого. Соответственно, второй вариант почти безопасен, но как говорят, нельзя быть немножко беременной, будем улучшать ситуацию.
Естественной защитой ресурса в рассматриваемом случае является использование критической секции в той или иной форме и что мы видим при такой реализации (я придерживаюсь мнения, что чем короче критическая секция, тем лучше, у Вас может быть другое). Первый вариант:
static inline void writeSPI(const byte b) {
CriticalStart();
SPDR = b;
while ( ! (SPSR & bit(SPIF)) );
CriticalStop();
};
И мы сделали критическую секцию значительного времени. Реализацию в аналогичном стиле для второго варианта мы даже не будем рассматривать, а сразу укажем правильную защиту:
static inline void writeSPI(const byte b) {
bооlean IsOk = False;
while (IsOk == False) {
if ( (SPSR & bit(SPIF)) != 0 );
СriticalStart();
If ( (SPSR & bit (SPIF)) != 0 ) {
SPDR = b;
IsOk=True;
};
CriticalStop();
};
}
Да, это намного сложнее, да, необходимы две проверки и нам следует создать макрос или inline функцию, следуя принципу DRY (я этого не сделал специально, чтобы подчеркнуть такую необходимость), да, встраиваемость функции тут под очень большим вопросом, но зато коротенькая (по времени) критическая секция, которая в 99.999% случаев будет выполняться за одну попытку, но останется безопасной и в 0.001% оставшихся.
Отметим еще одно важное обстоятельство — в общем случае, если у нас есть атомарная операция проверки и замены, либо обмена с флагом, то мы вообще во втором варианте можем обойтись без критической секции (в рассматриваемом конкретном варианте такой операции нет), а вот для первого варианта такой операции не существует в принципе. Конечно, они скажут, что совместное использование ресурса редко применяется в МК, что реализовывать такой вариант следует через обработчик, и будут правы, но в реальной жизни бывает все и лишняя безопасность наверное бывает, но крайне редко встречается – 0:1 в нашу пользу.
ЭФФЕКТИВНОСТЬ. Last but Not Least
В принципе, этого пункта было бы достаточно, но тогда пост был бы намного короче. Первый вариант по затратам времени можно описать так – инициируем операцию и ждем пока она выполнится (при этом ничего делать не можем), если необходимо, то проводим какие то дополнительные операции (готовим очередной символ к передаче) и повторяем цикл. Второй вариант – ждем, пока предыдущая операция закончится, инициируем очередную операцию и выходим из функции, то есть можем проводить дополнительные операции, при этом выполнение операции на ресурсе идет параллельно с ними, при необходимости повторяем цикл. Очевидно, что мы выигрываем минимальное из времени дополнительных операций либо времени выполнения на ресурсе, причем ни одно ни второе нулем не являются. В конкретной ситуации выигрыш может быть весьма значителен и увеличивать быстродействие системы до двух раз (в случае совпадения двух указанных времен). Конечно, они скажут, что предельно возможное быстродействие не всегда требуется, что для его достижения нужны специальные меры, что выигрыш не всегда будет столь значителен и будут правы, но тем не менее 0:1 в нашу пользу.
ПОНЯТНОСТЬ
Если у кого то есть аргументы в пользу одного из вариантов, прошу в комментарии, у меня их нет — 0:0 (за исключением безопасной реализации второго варианта, которая очевидно сложнее всех остальных).
ВЫВОДЫ
Второй способ обеспечивает более широкое применение, экономнее в работе, лучше совместим, более безопасен, однозначно эффективнее и не менее понятен, чем первый.
Общий счет 0:5 в нашу пользу, то есть второй способ лучше по всем параметрам.
Если кто-нибудь может привести аргументы в пользу первого варианта, прошу в комментарии, я ничего придумать не могу. На единственный вопрос – почему фирма выбрала именно этот способ – есть ответ в стиле известного фильма «Может, это потому, что ты …», который, конечно, «многое объясняет», но все-таки не представляется мне правильным, скорее подходит другой ответ из того же фильма «Потому что».
Автор: GarryC