Жизнь настолько коротка, что ее едва хватает на то, чтобы совершить необходимое количество ошибок, а уж повторять их — недопустимая роскошь.
В данном посте речь пойдет о том, чтобы не повторять чужих ошибок, что тоже является непроизводительной растратой столь ценного ресурса, как время. И вроде бы ошибка не столь фатальна и есть масса примеров, где она исключена и можно было бы давно научиться ее избегать, но почему то с упорством, достойным лучшего применения, она встречается вновь и вновь в исходных кодах программ для МК (может быть и для больших систем тоже, но я ими не занимаюсь), причем авторы данных программ не то чтоб новички во встроенном программировании, но тем не менее мы видим то, что видим. Искренне надеюсь, что после того, как данный пост будет Вами прочитан (при попытке ввести сочетание «после прочтения» в строго определенном месте текста у меня 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 в нашу пользу, то есть второй способ лучше по всем параметрам.
Если кто-нибудь может привести аргументы в пользу первого варианта, прошу в комментарии, я ничего придумать не могу. На единственный вопрос – почему фирма выбрала именно этот способ – есть ответ в стиле известного фильма «Может, это потому, что ты …», который, конечно, «многое объясняет», но все-таки не представляется мне правильным, скорее подходит другой ответ из того же фильма «Потому что».
Комментарии (24)
VaalKIA
22.09.2016 00:47встроенном программировании
Наверное, всё-таки, программирование встроенно в человека, а тут речь о железках.
Народ подскажет верный термин, пока что получается «программирование встроенного» или «программирование встраиваемых систем». Можно ещё выделить или заключить в скобки. что бы было понятно, что это термин или придумать какой-нибудь жаргонизм, типа встрой-программинг.GarryC
22.09.2016 12:34Ну я сделал такую кальку с сочетания «embedded programming», иногда еще употребляют «fimware» в смысле «прошивка», наверное такой жаргонизм был бы правильнее.
tip62
22.09.2016 12:20Объясните пожалуйста, что даёт const в передаваемом функции параметре? Почему использование static в конкретном примере — правильное применение?
Может я конечно неправильно понял суть проблемы, но нельзя ли тут попользовать прерывания, если не хотим в while ожидать флага? Кстати boolean в чистом си нет, или это от wiring сюда попало?GarryC
22.09.2016 12:28const говорит компилятору, что попытка изменить значение параметра будет ошибкой и должна пресекаться. В данном случае, когда мы передаем по значению, это неважно, но нужно приобретать полезный навык и все параметры, не подразумевающие возврат, отмечать как константные, так что это решение правильное, но в данном примере и необязательное.
static вообще полезен, чтобы не загромождать глобальное пространство имен и не вызвать по ошибке функцию, которую вызывать не собирались — это в общем. Но многие компиляторы (например IAR) inline без static просто не разрешат, чтобы не было соблазна вызвать его из другой единицы компиляции.
И Вы правы, прерывание будет правильным, речь шла исключительно о разных вариантах ожидания флага.
boolean этот мой enum (False=0,True=1), я о нем в предыдущих постах говорил, вот и использую.tip62
22.09.2016 20:15Я в программировании недавно, но мне кажется, что именно здесь const не удобен, и возможно даже помешает. Возможно ведь, что мне потребуется передать не какой-нибудь константный объект, а например полученный из датчика массив байтов или еще что-то подобное. Обычно такие функции, лично я использую, чтобы потом их попользовать уже например в более серьезной функции передачи того же массива. Тут кстати и static уже не грех применить, типа такой инкапсуляции чтоли) Вобщем думаю еслиб указатель передавался, то const бы мог уже на этапе компиляции, поругать, если мы захотим его изменить. А так один фиг передача по значению, как вы правильно заметили.
Я пишу в AtmelStudio чаще всего, там inline без static работает(точнее компилятор не ругается), но честно говоря не проверял, реально ли inline там работает, или все же происходит именно вызов функции.GarryC
22.09.2016 22:09Вы не совсем правы, const никак не может помешать, если мы не собираемся модифицировать передаваемые параметры, поэтому применяем его где только можно.
tip62
22.09.2016 23:56да, проверил никак не влияет… я думал если параметр функции помечен как const, то и получать функция должна const. Над матчасть повторять периодически)))
Alexeyslav
22.09.2016 16:41Собственно, вариант 1 гарантирует что с объектом можно работать сразу после выхода из процедуры. Иногда эта функция важна — передать байт, выключить модуль передачи и уснуть до следующего приёма.
Вставлять проверки и до и после тоже не вариант, Часто нужно сделать что-то и не ждать когда оно выполнится — заняться другой работой, в т.ч. подготовкой следующей порции данных пока передаётся очередная.
Учитывая что в контроллерах достаточно строгий контроль когда и что происходит, применяются оба варианта не по принципу правильно/неправильно а исходя из необходимого поведения в конкретном месте. Часто бывает такое что применяют и первый и второй варианты, просто надо знать что нужен строгий контроль над тем что и когда происходит и может произойти с объектами в программе.
Gryphon88
23.09.2016 11:56Подскажите, пожалуйста, а зачем в первой реализации nop?
GarryC
23.09.2016 12:08А я и сам не знаю, в фирменных примерах его нет, может быть, автор боялся что флаг готовности после инициализации не упадет мгновенно и может произойти вторая подряд запись. Где то в памяти сидит, что у кого-то когда-то подобные устройства были, но конкретно вспомнить не могу.
boolivar
24.09.2016 23:58Я люблю когда мои программы простые, поэтому я хочу чтобы мой код выглядел примерно так:
chipSelect(); writeSPI(0x0a); chipUnselect();
однако, если использовать второй вариант, мне придется явно добавлять ожидание, в лучшем случае так:
chipSelect(); writeSPI(0x0a); waitReadySpi(); chipUnselect();
GarryC
26.09.2016 09:40Все верно, но это только в том случае, когда Вы делаете ровно одну запись, во всех остальных случаях я рассмотрел преимущества второго способа.
boolivar
26.09.2016 20:10Что значит ровно одну? Даже одна работать не будет:
chipSelect(); writeSPI(0x0a); writeSPI(0x0b); writeSPI(0x0c); chipUnselect();
При использовании второй реализации, 0x0c в устройство записан не будет — кого винить?Alexeyslav
26.09.2016 22:40Вестимо функцию chipUnselect() которая не убедилась что все данные записаны прежде чем убрать разрешающий сигнал.
poznawatel
26.09.2016 09:37-1Не все программят на Сях. Даже и МК. Для охвата аудитории полезно было бы алгоритм описывать не на СИ-коде.
GarryC
26.09.2016 09:38Да в общем то код настолько прост, что транслируется в асм практически один в один, хотя, может Вы и правы.
poznawatel
26.09.2016 10:28Я, конечно, чувствую себя недоумком, но всё таки скажу: есть большущее подмножество промышленных MEK-контроллеров, типа Овен, которые программятся на пятёрке МЭК-языков (три из которых — визуальные), пользователи которых могут не знать ни АСМ-а ни Сей, а пользоваться, например, Питоном, как я. Прочли такие пользователи статью и ничего не поняли — китайская грамота. Возможно, есть и другие не Си-шные контроллеры.
grossws
26.09.2016 12:55То, что программируется на МЭКоских языках обычно называется PLC/ПЛК (программируемый логический контроллер). Это не микроконтроллеры, про которые речь в статье. Внутри PLC содержит тот же микроконтроллер, на котором крутится программа, почти всегда написанная на C или C++. Это просто совершенно разные уровни.
Если вы сталкиваетесь с микроконтроллерами, то стоит не просто знать синтаксис Си, а понимать его. Также как уметь читать datasheet'ы, всякие временные диаграммы, простые логические схемы (используются, например, при описании мультиплексируемых входов и выходов микроконтроллера) и т. п.
В случае PLC у вас не возникнет проблемы контроля за состоянием бита занятости в контрольном регистре какого-нибудь SPI перед или после записи байта в data register. Вы просто запишете значение в глобальную переменную или подадите на вход специального атома/блока.
Си — lingua franca для системщины и прочего низкоуровнего. Его не избежать, если хочется с пониманием делать что-то на микроконтоллерах. Всякие заходы python, js, lua на контроллерах забавны, но абстракции там текут очень сильно, так что рано или поздно может понадобится заглянуть в потроха интерпретатора, а там опять же Си.
boolivar
26.09.2016 23:27На самом деле, во всей статье упущена одна важная вещь: spi это дуплексный интерфейс — взамен отправленного байта вы получаете байт на чтение. Отсюда вопрос: когда читать данные если использовать второй вариант?
grossws
27.09.2016 11:46При использовании busy-wait'а, как в статье, я обычно пишу функцию, которая пишет указанный байт, а считанный возвращает и две обёртки для записи (которая игнорирует считанный байт) и для чтения (которая пишет dummy байт). В рамках базовой функции делается синхронизация на возможность записи в DR в начале, и на возможность чтения перед возвратом результата. дерганье CS/nCS происходит в более высокоуровневых функциях. Описываемые функции являются по возможности
static inline
.
GarryC
28.09.2016 09:35Если Вас в данный момент интересует только запись (а часто так оно и есть), то обычно перед инициализацией операции чтения либо нулируется указатель чтения (если это возможно), либо вычитывается буфер в мусорку.
MichaelBorisov
Программирование МК — это не та область, где существуют «правильные» и «неправильные» решения так же явно, как они существуют на обычных компьютерах.
Вы правы в вашей статье в принципе во всем. Но иногда все же первый вариант может оказаться предпочтительнее. Например: если какое-либо действие необходимо выполнить после того, как байт был фактически передан по аппаратному интерфейсу.
Вы скажете, что для этих целей можно реализовать отдельную функцию ожидания завершения работы интерфейса, а код вывода следует оставить как есть. И будете правы. Во всем, кроме одного маленького момента. Иногда на этих крохах можно сэкономить критически необходимые такты быстродействия или байтики ПЗУ. Когда на счету каждый такт или каждый байт — то не до жиру в плане красоты программ и их абстрактной надежности. А единственное, что играет роль — это характеристики программы и ее фактическая надежность в тех условиях, в которых она будет работать, без учета фантазий, а с учетом только реальных обстоятельств.
GarryC
Да, вы правы, единственное возможное объяснение такой реализации — необходимость удостовериться, что передача завершена и это единственное оправдание. Но в данном случае этот аспект не использовался и, если это действительно необходимо, следовало бы в комментариях отметить, почему применяется неудачное решение.
А общий смысл поста был именно в том, что есть более удачное по всем параметрам решение и следует привыкать использовать именно его, хотя истина всегда конкретна.