6 ms·
Bitfields make for much more readable code when accessing individual fields of hardware registers, although there are some caveats if the registers are poorly-d
by AdamH12113 4y ago
Bitfields make for much more readable code when accessing individual fields of hardware registers, although there are some caveats if the registers are poorly-designed. The main one is that bitfield writes are usually read-modify-writes, so if reading the register or writing back its current value causes something to happen, bitfields are a no-go. But when they work, you get code like:
old_divider = SpiRegs.CONFIG_REG.bit.CLK_DIVIDER;
SpiRegs.CONFIG_REG.bit.CLK_DIVIDER = new_divider;
instead of:
old_divider = (SpiRegs.CONFIG_REG & SPI_CONFIG_CLK_DIVIDER_MASK) >> SPI_CONFIG_CLK_DIVIDER_POS;
SpiRegs.CONFIG_REG = (SpiRegs.CONFIG_REG & ~SPI_CONFIG_CLK_DIVIDER_MASK) | (new_divider << SPI_CONFIG_CLK_DIVIDER_POS);
or the slightly nicer but even longer:
config = SpiRegs.CONFIG_REG;
old_divider = (config & SPI_CONFIG_CLK_DIVIDER_MASK) >> SPI_CONFIG_CLK_DIVIDER_POS;
config &= ~SPI_CONFIG_CLK_DIVIDER_MASK;
config |= new_divider << SPI_CONFIG_CLK_DIVIDER_POS;
SpiRegs.CONFIG_REG = config;
For anything other than hardware registers, I agree that they're not portable enough to rely on.
- pavon 4y agoHelper functions or macros are just as clean as the bitfield syntax. That said, hardware register access is one of those things that is intrinsically tied to a specific platform (and if you target multiple platforms, it will already be behind an abstraction layer), so you can usually know the quirks of how the toolchain for that platform supports bitfields, and use them accordingly. Still more work for people reading the code, though since there are a lot of hidden assumptions behind that deceptively simple "=" than with an explicit mask and shift.
- AdamH12113 4y ago>Helper functions or macros are just as clean as the bitfield syntax. They can be, if done right, but then I have to remember the names of all the helper functions and macros. :-) An IDE can auto-complete bitfield names. >Still more work for people reading the code, though since there are a lot of hidden assumptions behind that deceptively simple "=" than with an explicit mask and shift. Depends on the platform. IIRC on ARM a bitfield access is masking and shifting, only done by the compiler instead of me. With optimized code I often have to look at the disassembly anyway if I want to know what's really going on.
- deaddodo 4y ago> Depends on the platform. IIRC on ARM a bitfield access is masking and shifting, only done by the compiler instead of me. With optimized code I often have to look at the disassembly anyway if I want to know what's really going on. ARM supports native bitfield loading[1], extraction[2] and clearing[3]. 1 - https://developer.arm.com/documentation/ddi0602/2022-03/Base-Instructions/BFI--Bitfield-Insert--an-alias-of-BFM-?lang=en https://developer.arm.com/documentation/ddi0602/2022-03/Base... 2 - https://developer.arm.com/documentation/ddi0602/2022-03/Base-Instructions/UBFX--Unsigned-Bitfield-Extract--an-alias-of-UBFM-?lang=en https://developer.arm.com/documentation/ddi0602/2022-03/Base... 3 - https://developer.arm.com/documentation/ddi0602/2022-03/Base-Instructions/BFC--Bitfield-Clear--an-alias-of-BFM-?lang=en https://developer.arm.com/documentation/ddi0602/2022-03/Base...
- ncmncm 4y agoThere are tens of thousands of different ARM chip types. Many of them get it right.
- raxxorraxor 4y agoAnother trap with macros is that they might introduce extremely hard to detect bugs. You macro for clearing a bit like for example: #define CLEARBIT(a,p) = ((a) &= ~(1 << (p))) might not work correctly if your datatype is something "exotic" like a long. An inlined function might be the better choice. Otherwise you would need to define specific macros again: #define CLEARBITLONG(a,p) = ((a) &= ~(1L << (p))) These bugs can steal hours of your time, especially on embedded systems where debugging is less accessible.
- FrozenVoid 4y ago#define CLEARBIT(a,p) = ((a) &= ~((typeof(a))1 << (p)))
- raxxorraxor 4y agoThat is clearly better provided the compiler supports it. Less portable but it should really be a part of ISO C in my opinion.
- ncmncm 4y agoReadable code that does not necessarily execute what it says does nobody any favors.
- nomel 4y agoThis is why we test our code, or build in runtime checks, before releasing it.
- TillE 4y agoAbsolutely, it makes far more sense to do some basic sanity checks instead of writing painfully awkward code out of fear of a hypothetical. The standard may be flexible, but the behavior of a given compiler on a given platform will be consistent and very unlikely to change in the foreseeable future.
- ncmncm 4y agoWrong.
- TillE 4y agoPerfectionist nerds love to obsess over a million scenarios which only exist in their head, or in obscure edge cases. This is not productive. Perfectly portable, future-proof, CPU-bug-proof code simply does not exist. The only thing to be aware of here is that you shouldn't be trying to transfer data from one platform to another, loading it directly into memory, which is an insane thing to do in any case.
- ncmncm 4y agoYou may ignore field experience freely if you are insulated from all consequences.
- ncmncm 4y agoYou really are failing to get this! Testing formally-correct code can only ever find bugs in the exact compiler release and physical CPU chip you run the test on. The bugs cited show up in various compiler releases and various chip steppings. Unless your code will only ever run on that exact machine, you have not tested it.
- LAC-Tech 4y agoI remember being surprised that setting an individual bit on an AVR hardware register in assembly was so much shorter than doing all that C bit masking stuff.
- dataflow 4y agoIt feels weird to see arguments like this when you could just use a language (C++ being the elephant in the room here) that lets you define methods, then call those methods instead.
- nomel 4y agoA method that does will often (depending on the architecture) have much more overhead than a struct lookup. If you're doing hardware stuff, you often care about performance.
- dataflow 4y agoInlining?
- Sharlin 4y agoEvery C++ compiler released in the last 30 years will trivially inline oneliner getter functions, member or not.
- nomel 4y agoTrue, but this is making the assumption that whatever pointer offsets, shifting, and masking that you have in your method, to extract the 9th bit in the 5th word of a 512 bit struct, will result in the same operations as the simple struct access. It very well could be so (certainly for some architectures, most likely not for others), but I don't think all this complication is justified when a struct is available, especially if you're someone writing code for hardware, where these concepts are more known/less scary.
- jack_h 4y agoThis just illustrates how even in imperative languages like C/C++ programmers naturally reach for more declarative approaches. Type punning with a union is much the same story. It's just unfortunate that C/C++ has so many foot guns.
- chakkepolja 4y agoBy that logic isn't all abstraction 'declarative code'?
- junon 4y agoWhy not old_divider = (SpiRegs.CONFIG_REG >> SPI_CONFIG_CLK_DIVIDER_POS) & 1; Slightly cleaner IMO.
- AdamH12113 4y agoBecause the divider isn't necessarily one bit. Multi-bit register fields are very common. You can certainly hardcode the numbers if you want: old_divider = (SpiRegs.CONFIG_REG >> 24) & 0xff; which is easier to read but also easier to mess up when you're writing it. It's best to use the vendor's register definitions where possible. Although then you have the possibility of using the wrong #define constant, because all of these are just integers so the compiler can't tell you if you made a mistake. Another "fun" issue with using numbers is that sometimes an int is 16 bits, so you have to do (13 << 24ul) instead of (13 << 24).