3

I've often seen cli() and sbi() in Arduino code. I usually don't mind them as I know what they do (clear or set the bit given as second argument in the register of the microcontroller given as first). I've always thought that those functions where only a clear way to perform simple bit manipulation, which can be quite error-prone. Something like that: #define cli(reg,bit) (*reg &= ~(1 << bit) and #define sbi(reg,bit) (*reg |= (1 << bit)).

Then I found out that the actual implementation is as follows:

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

It's quite like I used to think... but with two extra macros. I looked for them in the Arduino libraries:

#define _BV(bit) (1 << (bit))

and

#define _SFR_BYTE(sfr) _MMIO_BYTE(_SFR_ADDR(sfr))

which contains

#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

and

#if _SFR_ASM_COMPAT
#if (__SFR_OFFSET == 0x20)
#define _SFR_ADDR(sfr) _SFR_MEM_ADDR(sfr)
#elif !defined(__ASSEMBLER__)
#define _SFR_ADDR(sfr) (_SFR_IO_REG_P(sfr) ? (_SFR_IO_ADDR(sfr) + 0x20) : _SFR_MEM_ADDR(sfr))
#endif
#else  /* !_SFR_ASM_COMPAT */    
#define _SFR_ADDR(sfr) _SFR_MEM_ADDR(sfr)

Further research took me to definitions I couldn't understand (for example because they are explained in comments using a lot of acronyms I don't know). So I have the following questions:

  • Why is sfr casted to *(volatile uint8_t *)?
  • What do the acronyms used in that code mean? (ASM, SFR, MMIO)
  • Under what circumstances is __ASSEMBLER__ defined, and where (in which file)?
  • Why can't we simply write #define cli(reg,bit) (*reg &= ~(1 << bit) and #define sbi(reg,bit) (*reg |= (1 << bit))?

2 Answers 2

4

Why is sfr casted to *(volatile uint8_t *)?

Because you want the content (the first *) of a memory address that you specify, and you need it volatile so it doesn't cache it. Oh, and it's an 8-bit value.

What do the acronyms used in that code mean? (ASM, SFR, MMIO)

ASseMbly, Special Function Register and Memory Mapped Input/Output

  • ASM means assembly language.
  • A Special Function Register is something like DDRB.
  • Memory Mapped IO is a way of interacting with peripherals by reading from and writing to locations in the normal memory address space.

Under what circumstances is __ASSEMBLER__ defined, and where (in which file)?

It is defined by the compiler when you are compiling an assembly file.

Why can't we simply write #define cli(reg,bit) (reg &= ~(1 << bit) and #define sbi(reg,bit) (reg |= (1 << bit))?

Because reg is just an address. You need to convert that into a variable in memory that you can reliably access (hence the volatile). Further to that, not all chips use MMIO for all peripherals - some can be accessed by more efficient IO instructions. That said, in many cases yes you can just use simpler code. After all, things like DDRB |= 0x04 works fine. It all depends on how the value to pass as reg is defined originally. By forcing a specific volatile cast you can ensure that whatever way it was defined (with or without volatile, etc) it will work.

1
  • Thanks for the quick answer. I’m realizing I forgot the * before reg, it’s a stupid copy-paste error. I’ll add it to the question (the edit will influence only the last part of your answer) Commented Mar 3, 2018 at 23:23
4

On Atmel (now Microchip) AVR microcontrollers, the sbi, cbi, sbis, and sbic assembly language instructions can only address the 32 IO ports at addresses between 0x20 and 0x3f. The Atmega168 and 328 (used in Arduinos prior to the Leonardo) has WAY more than 32 IO registers, so only a subset of them can be addressed by sbi and cbi (the AVR instruction set was nailed down YEARS ago, when 32 IO registers seemed like more than enough).

As a result, operations that set or clear a single bit in those 32 IO registers can be performed by a single instruction which executes in two clock cycles, but the same operations on the remaining IO registers require a full-blown load-operate-store sequence that takes at least three cycles to execute.

From what I recall, AVRlibC had multiple macros that did almost the same thing, but had different use cases:

  • One would compile to the fastest and most efficient instruction(s) available for that particular IO address.

  • One would always compile to sbi/cbi, and would throw a compiler error if you tried to access a register outside of 0x20-0x3f

  • One would ALWAYS compile to the load-act-store sequence, without regard to whether or not it could have used sbi/cbi instead.

  • One would always compile to the load-act-store sequence, but would throw a compiler error if you tried using it on an address between 0x20 and 0x3f.

Basically, the choice enabled you to decide between optimized (but variable) performance, uniformly-slow (but deterministic) execution time, or the fastest-possible (but deterministic-at-compile-time) variant [which required that the programmer be aware of whether the register in question was within or above 0x20-0x3f].

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.