#assembly #gcc #avr
#сборка #gcc #avr
Вопрос:
У меня есть короткий фрагмент сборки из проекта AVR:
uint8_t high = _BV(0);
uint8_t low = ~high;
uint8_t port_value = 0;
asm volatile (
"in %0, %1 nt"
"or %0, %3 nt"
"out %1, %0 nt"
T1H_NOOP
"and %0, %2 nt"
"out %1, %0 nt"
T1L_NOOP
: "=r" (port_value)
: "I" (_SFR_IO_ADDR(PORTB)), "r" (low), "r" (high));
Идея этого блока заключается в том, чтобы включить определенный вывод (фактический вывод физического микропроцессора) на короткий промежуток времени (T1H_NOOP), а затем отключить его. Приведенный выше код на самом деле работает безупречно.
Но в приведенном выше коде точный pin-код жестко запрограммирован: PORTB, Pin 0 ( _BV(0)
). Что я хочу, так это передавать адрес, подобный этому:
struct IO_ADDR {
volatile uint8_t *port;
uint8_t pin
}
Пока я остаюсь в коде C, это действительно работает.
struct IO_ADDR addr = { .port = amp;PORTB, .bit = 0 };
latch(amp;addr);
void latch(struct IO_ADDR *addr) {
if (addr->bit >= 8) return;
*(addr->port) amp;= ~(_BV(addr->bit));
_delay_us(50);
}
И когда я говорю это, я имею в виду, что я прогнал это через симулятор и увидел, что выводы срабатывают, как ожидалось, плюс я соединил этот фрагмент со сборкой выше и запустил его на оборудовании. Итак, очевидно, что *(addr->port) amp;= ...
обращается к самому выводу, а не к указателю. Прохладный.
Но когда я делаю это, я получаю ошибку сборки:
asm volatile (
"in %0, %1 nt"
"or %0, %3 nt"
"out %1, %0 nt"
T1H_NOOP
"and %0, %2 nt"
"out %1, %0 nt"
T1L_NOOP
: "=r" (port_value)
: "I" (_SFR_IO_ADDR(*(addr->port))), "r" (low), "r" (high));
Эта ошибка:
/nix/store/j31yaksw2dh82by2lgz1ysgh494cz6j2-src/neopixels.c: In function 'write_value':
/nix/store/j31yaksw2dh82by2lgz1ysgh494cz6j2-src/neopixels.c:29:9: warning: asm operand 1 probably doesn't match constraints
29 | asm volatile (
| ^~~
/nix/store/j31yaksw2dh82by2lgz1ysgh494cz6j2-src/neopixels.c:29:9: error: impossible constraint in 'asm'
Это также происходит, если я заменяю параметр addr-> port на _SFR_IO_ADDR(addr->port)
.
SFR_IO_ADDR(*(addr->port))
предварительные обработки для этого:
: "I" (
# 38 "src/neopixels.c" 3 4
(((uint16_t) amp;(
# 38 "src/neopixels.c"
*(addr->port)
# 38 "src/neopixels.c" 3 4
)) - 0x20)
# 38 "src/neopixels.c"
)
Окончательная сборка должна быть такой в случае PORTB
, адрес 0x24 на этом конкретном аппаратном обеспечении (и игнорирование точных регистров, которые выбрал компилятор):
in r18, 24
or r18, r21
out 24, r18
Что мне нужно сделать, чтобы передать этот конкретный адрес ввода-вывода в мой ассемблерный код?
Комментарии:
1. Ваша основная проблема заключается в том, что вы используете адрес ввода-вывода с инструкцией, которая принимает непосредственный операнд, поэтому должна быть оценена во время компиляции, но адрес в целом может быть определен только во время выполнения. Вам либо нужно сделать адрес ввода-вывода константой, которую всегда можно вычислить во время компиляции, либо использовать инструкцию, которая принимает регистр или операнд памяти для адреса ввода-вывода. Обратите внимание, что адресное пространство инструкций ВВОДА и вывода отличается от того, которое используют указатели C. Вам нужно вычесть 0x20, чтобы преобразовать адрес памяти в адрес ввода-вывода.
2. Обратите внимание, что здесь действительно нет необходимости использовать встроенную сборку. GCC может генерировать инструкции ВВОДА-вывода, когда используемый адрес памяти является константой времени компиляции и отображается в действительный адрес ввода-вывода после преобразования.
3. Действительно, компилятор может сгенерировать гораздо лучший ассемблерный код, чем вы: gcc.godbolt.org/z/rKshaM
4. Если вам нужен такой точный контроль над каждой инструкцией, помимо примера
t1h_noop
функции задержки, который я привел по ссылке выше, тогда вам следует использовать обычную сборку.5. Вы можете посмотреть на сборку, которую GCC генерирует для показанного вами фрагмента кода C, использующего указатели. Тогда вы можете просто написать то же самое в сборке.
Ответ №1:
"I"
Ограничение сборки требует, чтобы его операнд был константой (или, с -O1 / -O2, постоянным выражением), поэтому, к сожалению, вы не сможете передать его в качестве параметра.
Ответ №2:
После еще одного дня исследований я нашел этот ответ:
asm volatile (
"ld %0, %1 nt"
"or %0, %3 nt"
"st %1, %0 nt"
T1H_NOOP
"and %0, %2 nt"
"st %1, %0 nt"
T1L_NOOP
: "=r" (port_value)
: "X" (*(addr->port)), "r" (low), "r" (high));
Здесь были задействованы два ключевых момента. Один из них предоставляет X
ограничение вместо I
constraint, что фактически означает «операндом может быть вообще что угодно». Это неоптимально, но ассемблер не принял некоторые из наиболее очевидных («операнд — это адрес памяти, который нельзя перемещать»).
Кроме того, я переключился с in
и out
инструкций ассемблера, которые, как указывает Росс Ридж в комментариях выше, требуют непосредственного адреса, который должен быть известен во время компиляции, на ld
и st
инструкции, которые принимают адреса памяти.
В качестве последнего замечания мне пришлось изменить количество команд NOP в некоторых макросах T1H_NOOP, T1L_NOOP, T0H_NOOP и T0L_NOOP, чтобы сохранить временные ограничения, которые требует протокол передачи сигналов neopixel.
Все это достаточно болезненно, поэтому я немного почитал о широтно-импульсной модуляции и схемах таймера / синхронизации с прерываниями, поскольку кажется, что это будет генерировать более надежные тайминги, чем мои активные циклы занятости. Однако добавление обработчиков прерываний увеличило бы сложность моего кода способами, с которыми я не готов справиться прямо сейчас.
Комментарии:
1. Если адрес может быть константой, вы могли бы обернуть этот код в
if __builtin_constant_p
. Тогда есть два варианта, один сI
и один сX
. Поскольку это условие разрешается во время компиляции, оно должно предоставить вам лучшее из обоих миров. NB ознакомьтесь с документами , в которых указывается на такие вещи, как то, что встроенные функции не будут должным образом определять константы, если вы не включите оптимизацию (что имеет смысл).2. Я полагаю, что определенно есть несколько случаев, в которых это может быть константой времени компиляции. У меня есть случай, когда этого не происходит. Как вы думаете, эта опция сэкономит какое-либо пространство в программе?
3. Трудно говорить в абсолютных выражениях о том, что могут делать оптимизаторы. Но я ожидал бы, что конструкция, подобная
if __builtin_constant_p do I else do X
, оптимизирует способ, который вы ожидаете. Не было бы причин (в оптимизированных сборках) для включения обоих случаев. Является ли использование «I» «меньшим», чем использование «X»? Не могу сказать, я не настолько хорошо знаю AVR.