Стандартное соответствие AVR 8 бит, C относительно доступа к битам SFR
Один из моих коллег столкнулся с некоторыми странными проблемами с программированием ATMega, связанными с доступом к портам ввода-вывода.
Наблюдая за проблемой после некоторых исследований, я пришел к выводу, что нам следует избегать доступа к SFR, используя операции, которые могут компилироваться в инструкции SBI
или CBI
, если мы стремимся к безопасному программному обеспечению, совместимому со стандартом C. Я ищу, было ли это решение праведенным или нет, поэтому, если мои опасения здесь действительны.
Техническое описание процессора Atmel здесь, это ATMega16. Я буду ссылаться на некоторые страницы этого документа ниже.
Я буду ссылаться на стандарт C, используя версию, найденную на этом сайте по ссылке WG14 N1256.
Инструкции SBI
и CBI
процессора работают на уровне битов, получая доступ только к рассматриваемому биту. Поэтому они не являются истинными инструкциями Read-Modify-Write (R-M-W), поскольку они, как я понимаю, не выполняют чтение (целевого 8-битного SFR).
На стр. 50 вышеприведенного описания первое предложение начинается, как и все порты AVR имеют истинную функцию Read-Modify-Write..., но при этом она указывает, что это относится только к обращению с инструкциями SBI
и CBI
которые технически не являются RMW. В таблице данных не указано, какие чтения, например, должны возвращаться регистры PORTx
(однако это означает, что они доступны для чтения). Поэтому я предположил, что чтение этих SFR составляет undefined (они могут вернуть последнюю написанную на них или текущее состояние ввода или что-то еще).
На странице 70 перечислены некоторые внешние флаги прерываний, это интересно, потому что здесь важны инструкции SBI
и CBI
. Флаги устанавливаются, когда произошло прерывание, и их можно очистить, записав их в один. Поэтому, если SBI
был истинным инструкцией R-M-W, он очистил бы все три флага независимо от бита, указанного в коде операции.
А теперь давайте перейдем к вопросам C.
Сам компилятор действительно не имеет значения, единственный важный факт в том, что он может использовать команды CBI
и SBI
в определенных ситуациях, которые, по моему мнению, делают его несовместимым.
В вышеприведенном стандарте C99 раздел 5.1.2.3. Выполнение программы, пункты 2 и 3 относится к этому (на странице 13) и 6.7.3. Отборочные категории, пункт 6 (на странице 109). В последнем упоминается, что То, что представляет собой доступ к объекту, который имеет нестабильный тип, определяется реализацией, однако несколько фраз перед ним требуют, чтобы любое выражение, относящееся к такому объекту, оценивалось строго в соответствии с правилами абстрактной машины.
Также обратите внимание, что аппаратные порты, такие как используемые в этом примере, объявляются volatile
в соответствующих заголовках.
Пример:
PORTA |= 1U << 6;
Это, как известно, переводит на SBI
. Это означает, что на изменчивый (PORTA
) объект происходит только доступ на запись. Однако, если вы напишете:
var = 6;
...
PORTA |= 1U << var;
Это не будет переведено на SBI
, хотя он будет по-прежнему устанавливать только один бит (поскольку SBI
имеет бит для установки кодировки в коде операции). Таким образом, это будет расширяться до истинной последовательности R-M-W с потенциально другим результатом, чем выше (в случае PORTA
это поведение undefined, насколько я мог бы вычесть из таблицы данных).
По стандарту C это поведение может быть или не разрешено. В этом термина также бесполезно, что здесь происходят две вещи, которые смешиваются. Во-первых, тем очевиднее отсутствие доступа к чтению в одном из случаев. Другой, менее очевидный способ выполнения записи.
Если скомпилированный код пропускает чтение, он может не запускать аппаратное поведение, привязанное к такому доступу. Однако AVR, насколько я знаю, не имеет такого механизма, поэтому он может пройти по стандарту.
Запись более интересна, однако она также берет в Чтение.
Опускание чтения в случае использования SBI
подразумевает, что затронутый SFR должен работать как защелки (или любой бит, который не работает так, либо привязан к 0 или 1), поэтому компилятор может быть уверен в том, что он будет читать от них, если он действительно сделал доступ. Если это не так, то компилятор, по крайней мере, будет ошибкой. Кстати, это также противоречит тому, что техническое описание не определило, что читается из регистров PORTx
.
Как выполняется запись, также является источником несогласованности: результат различается в зависимости от того, как компилятор компилирует его (a CBI
или SBI
, затрагивающий только один бит, запись байта, затрагивающая все биты). Таким образом, написание кода для очистки/установки одного бита может быть "работать" (как в случае "случайных" сбросов флагов прерывания), или нет, если компилятор создает истинную последовательность R-M-W вместо этого.
Возможно, это технически разрешено стандартом C (как "поведение, определенное реализацией", а компилятор вычитает эти случаи, когда доступ чтения не требуется для изменчивого объекта), но по крайней мере я бы счел это ошибкой или непоследовательным реализация.
Другой пример:
PORTA = PORTA | (1U << 6);
Хорошо видно, что, как правило, должно соответствовать стандарту "Чтение", а затем должна выполняться запись PORTA
. Хотя в соответствии с поведением SBI
у него не будет доступа к чтению, хотя, как указано выше, это может пройти для сочетания определенного поведения реализации, а компилятор вычитает, что здесь нет необходимости в Read. (Или мое предположение было неправильным? Предполагается, что a |= b
идентичен a = a | b
?)
Поэтому на основе этих данных я решил, что мы должны избегать этих типов кода, как есть (или может быть в будущем), неясно, как они могут себя вести в зависимости от того, будет ли компилятор использовать SBI
или CBI
, или истинная последовательность RMW.
Честно говоря, я в основном занимался различными сообщениями на форуме и т.д., разрешая это, не анализируя фактический вывод компилятора. Не мой проект в конце концов (и теперь я не на работе). Я воспринял это, прочитав AVRFreaks, например, что AVR-GCC выдаст эти инструкции в вышеупомянутых ситуациях, которые сами по себе могут представлять проблему, даже если с фактической версией, которую мы использовали мы не заметили бы этого. (Тем не менее, я думаю, что в этом случае это было мое предложение о внедрении доступа к портам с использованием теневых рабочих переменных, которые фиксировали проблемы, которые наблюдал мой коллега)
Примечание. Я редактировал середину на основе некоторых исследований по стандарту C (C99).
Изменить: Чтение Часто задаваемых вопросов о AVR Libc Я снова нашел что-то, что противоречит автоматическому использованию SBI
или CBI
. Это последний вопрос и ответ, в котором он конкретно заявляет, что, поскольку порты объявлены volatile
, компилятор не может оптимизировать доступ для чтения в соответствии с правилами языка C (как его фразы).
Я также понимаю, что очень маловероятно, что это конкретное поведение (то есть использование SBI
или CBI
) будет непосредственно вводить ошибки, но маскируя "ошибки", может показаться очень неприятным в долгосрочной перспективе, если кто-то случайно обобщает на основе этого поведения, не понимая AVR на уровне сборки.
Ответы
Ответ 1
Вероятно, вам следует перестать пытаться применить модель памяти C к регистрам ввода-вывода. Это не просто память. В случае регистров PORTn на самом деле не имеет значения, является ли это однократной записью или операцией R-M-W, если вы не смешиваете прерывания. Если вы выполняете чтение-изменение-запись, прерывание может изменять состояние между ними, вызывая состояние гонки; но это будет точно такой же проблемой для памяти. Преимущество инструкций ВОО /CBI состоит в том, что они являются атомарными.
Регистры PORTn читаются, а также управляют выходными буферами. Это не разные функции чтения и записи (как на PIC), а обычный регистр. Новые PIC также имеют выходные регистры, читаемые на адресах LAT, именно так вам не понадобится теневая переменная. Другие SFR, такие как PINn или флаги прерываний, имеют более сложное поведение. На недавних AVR запись в PINn вместо этого переключает биты в PORTn, что снова полезно для быстрой и атомной работы. Запись 1s для прерывания регистров флага очищает их, чтобы предотвратить условия гонки.
Дело в том, что эти функции созданы для правильного поведения программ с программным обеспечением, даже если некоторые из них выглядят странно в коде C (т.е. используя reg=_BV(2);
вместо reg&=~_BV(2);
). Точное соответствие стандарту C является нецелесообразной целью, когда код по своей природе специфичен для аппаратного обеспечения (хотя семантическое сходство помогает, что приводит к потере флага прерывания). Обертка нечетных конструкций в встроенных функциях или макросах с именами, которые объясняют, что они на самом деле делают, вероятно, хорошая идея или, по крайней мере, комментирование эффектов. Набор таких подпрограмм ввода-вывода также мог бы стать основой уровня абстракции аппаратного обеспечения, который может помочь вам использовать код порта.
Попытка интерпретировать спецификацию C строго здесь также довольно запутанна, так как она не допускает битов адресации (что является тем, что делают SBI и CBI), и, копаясь в моей старой (1992) копии, обнаруживает, что могут возникать изменчивые обращения в нескольких реализациях, определенных поведением, включая возможность отсутствия доступа вообще.