Pull to refresh

Сделаем код чище: работа с 64-битными регистрами оборудования в Linux

Reading time 3 min
Views 9.3K
Нередко у программистов, пишущих драйверы, возникают некоторые трудности с обменом данными в 64-битном формате. Давайте разберём некоторые ситуации.

Вместо вступления


На самом деле под ширмой работы с 64-битными регистрами скрыто несколько проблем.

Во-первых, обслуживание оборудования, в котором применяются 64-битные регистры, должно быть доступно как на 64-битных ядрах, так и на 32-битных.

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

В-третьих, иногда приходится сравнивать 64-битное число как два 32-битных, снова же в связи с установленными стандартами и протоколами.

Посмотрим, как избежать изобретения велосипеда, и сделать код чище, лаконичнее и красивее.

Обмен 64-битными данными


Конечно же многие сталкивались с известными командами записи и чтения регистров оборудования, такими как writel(), readl().

Они охватывают традиционные байтовые, двухбайтовые и четырёхбайтовые операции с регистрами. Что же делать, когда необходимо записать или считать сразу восемь байт?

На некоторых 64-битных архитектурах на помощь приходят команды writeq() и readq().

Традиционно программист для охватывания 32-битных и 64-битных платформ сочиняет нечто подобное в своём коде:
static inline void writeq(u64 val, void __iomem *addr)
{       
        writel(val, addr);
        writel(val >> 32, addr + 4);
}

И соответственно для чтения.
static inline u64 readq(void __iomem *addr)
{       
        u32 low, high;

        low = readl(addr);
        high = readl(addr + 4);

        return low + ((u64)high << 32);
}


И вот представьте, что таких копий десятки, если не сотни. Во избежание изобретений велосипеда в ядро внесли специальные файлы include/linux/io-64-nonatomic-hi-lo.h и include/linux/io-64-nonatomic-lo-hi.h.

Какие особенности у этих файлов:
  1. Определённые функции выполняют обращения не атомарно.
  2. В связи с вышесказанным в ядре два файла: для записи младший-старший и наоборот — старший-младший.
  3. Оба файла объявляют writeq() и readq() по принципу «кто первый встал, того и тапки». Вначале проверяется не определены ли они уже? Если нет, тогда переопределяем. Соответственно порядок включения заголовочных файлов имеет значение.
  4. Обращения, что очевидно, выполнены по формуле 8 = 4 + 4.

Соответственно, если у нас оборудование, которое понимает только обращения вида 4 + 4, то мы применяем lo_hi_writeq() и lo_hi_readq() или hi_lo_writeq() и hi_lo_readq(). При идеальном случае просто writeq() и readq().

Сравнение 64-битных чисел


В некоторых случаях возникает необходимость сравнить 64-битное число в варианте 8 с числом в варианте 4 + 4.

Решение прямо в лоб:
u32 hi = Y, lo = Z;
u64 value = X, tmp;

tmp = (Y << 32) | X;
return value == tmp;

Ну вы поняли, как много будет кода на 32-битной архитектуре.

Можно разбить это дело на такие два сравнения:
return (value >> 32) == hi && (u32)value == lo;

Вроде легче, но… есть одна проблема. В ядре встречаются типы, которые непосредственно зависят от конкретной платформы, а именно: phys_addr_t, resource_size_t, dma_addr_t и им подобные.

Как вы думаете, что будет, если мы напишем такой код:
u32 hi = Y, lo = Z;
resource_size_t value = X;

return (value >> 32) == hi && (u32)value == lo;

На 64-битной архитектуре, понятное дело, всё будет прекрасно. А вот на 32-битной нам пожалуется компилятор на сдвиг value >> 32.

Для того, чтобы и компилятор был счастлив, и пользователь особо себя не утруждал, в ядро добавили следующие макросы: lower_32_bits() и
upper_32_bits() для соответственно младших и старших 4 байт.

В результате сравнение будет выглядеть так:
return upper_32_bits(value) == hi && lower_32_bits(value) == lo;


Не забывайте, что данные операции не атомарны!
Tags:
Hubs:
+10
Comments 1
Comments Comments 1

Articles