📜 ⬆️ ⬇️

Making the code cleaner: working with 64-bit hardware registers in Linux

Often, programmers writing drivers have some difficulties with exchanging data in 64-bit format. Let's look at some situations.

Instead of intro


In fact, under the cover of working with 64-bit registers there are several problems hidden.

First , the maintenance of equipment in which 64-bit registers are applied must be available on both 64-bit and 32-bit kernels.
')
Secondly , not every 64-bit registers can be written for one command, since this is connected with the implementation nuances in hardware.

Thirdly , sometimes you have to compare a 64-bit number as two 32-bit ones, again in connection with established standards and protocols.

Let's see how to avoid the invention of a bicycle, and make the code cleaner, more concise and more beautiful.

Exchange of 64-bit data


Of course, many have come across well-known commands for writing and reading hardware registers, such as writel() , readl() .

They cover traditional byte, double-byte, and four-byte register operations. What to do when you need to write or read eight bytes at once?

On some 64-bit architectures, the writeq() and readq() commands come to the rescue.

Traditionally, a programmer for embracing 32-bit and 64-bit platforms writes something similar in his code:
 static inline void writeq(u64 val, void __iomem *addr) { writel(val, addr); writel(val >> 32, addr + 4); } 

And accordingly for reading.
 static inline u64 readq(void __iomem *addr) { u32 low, high; low = readl(addr); high = readl(addr + 4); return low + ((u64)high << 32); } 


And imagine that there are dozens, if not hundreds of such copies. In order to avoid the inventions of the bike, special files include / linux / io-64-nonatomic-hi-lo.h and include / linux / io-64-nonatomic-lo-hi.h were added to the kernel .

What are the features of these files:
  1. Certain functions perform appeals not atomically.
  2. In connection with the above, there are two files in the kernel: for writing junior-senior and vice versa - senior-junior.
  3. Both files declare writeq() and readq() on the principle of "who first got up, and sneakers." First, is it checked whether they are already defined? If not, then redefine. Accordingly, the order of inclusion of header files is important.
  4. Appeals, obviously, are made by the formula 8 = 4 + 4.

Accordingly, if we have equipment that understands only 4 + 4 type calls, then we use lo_hi_writeq() and lo_hi_readq() or hi_lo_writeq() and hi_lo_readq() . In the ideal case, just writeq() and readq() .

Comparison of 64-bit numbers


In some cases, it is necessary to compare a 64-bit number in version 8 with a number in version 4 + 4.

The solution to the head:
 u32 hi = Y, lo = Z; u64 value = X, tmp; tmp = (Y << 32) | X; return value == tmp; 

Well, you understand how much there will be code on a 32-bit architecture.

You can break this thing down into two comparisons:
 return (value >> 32) == hi && (u32)value == lo; 

It seems easier, but ... there is one problem. In the kernel there are types that are directly dependent on a specific platform, namely: phys_addr_t , resource_size_t , dma_addr_t, and the like.

What do you think will happen if we write this code:
 u32 hi = Y, lo = Z; resource_size_t value = X; return (value >> 32) == hi && (u32)value == lo; 

On the 64-bit architecture, of course, everything will be fine. But on a 32-bit one, the compiler will complain about the value >> 32 shift.

So that the compiler was happy and the user didn’t bother too much, the following macros were added to the kernel: lower_32_bits() and
upper_32_bits() for respectively younger and older 4 bytes.

As a result, the comparison will look like this:
 return upper_32_bits(value) == hi && lower_32_bits(value) == lo; 


Do not forget that these operations are not atomic!

Source: https://habr.com/ru/post/271433/


All Articles