Close

multi-byte register-access considerations

A project log for AVR Random Potentially-Obsure Potentially-Usefuls

Random potentially-obscure potentially-useful things related to AVRs

eric-hertzEric Hertz 02/14/2017 at 18:350 Comments

4/16/17 Hmmm... I seem to have written this long ago, and completely forgotten that it was sitting in drafts... I haven't reread it, so don't recall *why* it was a draft and not published.

-----------------

When accessing multi-byte registers (e.g. the ADC's 10-bit, or Timer1's 16-bit), some things should be considered:

Some of this might seem a bit pedantic... But probably wise to consider, especially atomicity, and also if you're concerned about code-portability (e.g. between different versions of avr-gcc, or between different C compilers altogether).

The gist is: since AVRs are 8-bit processors, that means accessing registers wider than 8-bits requires multiple instructions, and often a specific order-of-operations.

Here are two examples, I'll go into in more detail:

  1. The 16-bit timer on the ATmega644 has the two registers: TCNT1L and TCNT1H used to read/write the Timer/Counter's count-value, ICR1L and ICR1H to read/write the Input-Capture Register, etc.
  2. The 10-bit timer on the ATtiny861 has TCNT1, ICR1, OCR1A, etc, which are 8-bit registers, and a *shared* TC1H used to write those registers' high-bits.

Despite the m644's apparently having separate high-bytes for its registers, the fact is BOTH these devices use a "Temporary High-Register" during read/write accesses, which is shared between multiple >8-bit registers.

For instance, when writing to the full 16-bits of the m644's TCNT1 one must *first* write the high byte to TCNT1H. This value is stored in the temporary high-byte register. Then when the Low Byte TCNT1L is written, both bytes in the *internal* 16-bit TCNT1 register will be updated simultaneously. (This is important! Otherwise your counter will briefly have a glitch! Further, it's not an option, you *have to* write these bytes in this order, otherwise you may inadvertently load garbage into the high-byte).

Similarly, when writing to the full 10-bits of the t861's 10-bit-wide TCNT1 *internal* register, one must *first* write the high-byte to the shared temporary high-byte register TC1H, then write the low-byte to the TCNT1 register-address. (It's a bit confusing because TCNT1 appears as an 8-bit register, by name, to the programmer, but is 10-bits internal to the device).

Read-back also has an order-of-operations. But note that the order is *opposite* that of write. First you must read the low-byte (TCNT1, in the case of the t861, TCNT1L, in the case of the m644). Doing-so automatically loads the devices' temporary high-byte register at that same instant. Then you read-back the high-byte from that temporary register. Again, on the m644, you'd read TCNT1H, whereas on the t861 you'd read TC1H.

These orders-of-operations assure that when you perform a read/write, all >8 bits are read/written at the same instant. But note that you can't reverse these operations and just expect a minor glitch for the couple clock-cycles between accesses. By which I mean: Imagine the reversed order-of-operations used to write TCNT1 on the m644. If you write TCNT1L first, whatever random value was last-stored in the temporary-high-byte register will be loaded at that instant alongside your (correct) low-byte. And writing TCNT1H, thereafter, will have no effect on your counter's TCNT value and serve no other purpose than to store that value until the next time it's indirectly (and most-likely mistakenly) accessed by a write to a 16-bit register's low-byte.

Note, too, that I specified "*a* 16-bit register's low-byte", rather than TCNT1L.

Even though the m644 has register-addresses explicitly for TCNT1H and ICR1H, reading/writing these high-byte registers actually accesses the (singular/shared) temporary-high-byte-register.

Basically, the m644's and t861's systems are the same, with the exception that the m644 has TC1H accessible at various memory-locations with different names.

In the earlier example, where one mistakenly writes the bytes of TCNT1 out of order, now the temporary-high-register stores your intended-value. Now say you use the same mistaken order-of-operations to write your OCR1A register's 16-bits. Now OCR1A contains the high-byte you previously-intended for TCNT1. Whoops!

-----------

So, make sure your order-of-operations is correct!

For the t861 a 10-bit write to TCNT1 might appear as follows:

uint16_t count = 0xffff;
TC1H = count>>8;
TCNT1 = count&0xff;
and a 10-bit read:
uint8_t low_byte, high_byte;
low_byte = TCNT1;
high_byte = TC1H;
uint16_t count = low_byte | (high_byte<<8);
(The TCNT1 and TC1H reads likely *could* be placed directly in the assignment of count, rather than using temporary 8-bit variables... But... do-so carefully, and even that may not be safe! And, realistically, this isn't even guaranteed, after the optimizer has its way. For things like these, inline-assembly might be the best way to go. Write those macros once, and use them thereafter.)

For the m644 we're kind-of lucky you can simply write:

uint16_t count = 0xffff;
TCNT1 = count;

or

uint16_t count = TCNT1;
But note these come with a bit of risk. This is only possible because the m644's 16-bit TCNT1 *internal* register is exposed to the user as both TCNT1L

and the fake/temporary/buffered TCNT1H, which exist at sequential addresses.

AND your version of C, be it avr-gcc, or another, must explicitly support the proper order-of-operations for reads and writes to 16-bit I/O registers! (Note that some older versions of avr-gcc *don't*! Further, that it did once, then "regressed". And, in fact, there was a lot of debate as to whether to include that feature, at all... suggesting that it's *not* de-facto, and likely *not* in other C compilers.)

Further, imagine a case where you need very short duty-cycle pulses, using the PWM outputs. One might at one point attempt the following:

TCNT1 = 0x7ff7;
.........
OCR1AL = 0xff;
See the problem, here...?

The assignment to TCNT1 implicitly sets TCNT1H = 0x7f. But at the programmer-side of things writes to TCNT1H are stored to the temporary high-byte register. The internal TCNT1 register will be written 0x7ff7, as intended. But the temporary register still contains 0x7f. Now writing OCR1AL = 0xff will cause the internal OCR1A register to contain 0x7fff, NOT 0x00ff, as likely-intended.

Here's another case:

uint16_t thisCount = TCNT1;
...........
OCR1AL = 0xff;
Now we have no idea what OCR1A will contain, as its high-byte depends *entirely* on what was read in the previous read-instruction.

The t861, while a bit more difficult to code-for, forces the user to see some of these potential difficulties... So, if developing new habits, even aiming toward more-sophisticated microcontrollers (like the m644, in this case), it might be wise to consider using the method required for the t861: Read each byte explicitly, and in the proper order.

(Note, again, @K.C. Lee's experience, discovering that some C compilers don't actually respect the inherent order-of-operations! So it might be even wiser-still to write each byte-access on its own line, assigned to its own variable, then merge those variables in a final operation. Still, smart optimizers might optimize-that-out... Inline-assembly macros may be the best way to go).

-------------

ALLEGEDLY: one can use many 16-bit peripherals in 8-bit mode, by only ever accessing the Low-bytes. I've done it, on occasion, mostly from laziness. But looking into these details it seems very apparent to me that that could be risky. E.G. Imagine a case where the operating-mode of the Timer/Counter is changed, or another case where a 16-bit value is read/written before starting the system in 8-bit mode... e.g. the most-recent code-example could cause trouble even if it were only written to access 8-bits at a time!

uint8_t thisCount = TCNT1L;
.....
Maybe hours later...
.....
OCR1AL = 0xff;

Because, again, the action of reading TCNT1L causes the temporary-high-byte register to be loaded with the high-byte of TCNT1, whatever it may be, and now the high-byte of OCR1A is random.

---------------

http://www.atmel.com/images/doc1493.pdf

644 datasheet:

(OCR1A, etc. also use the temp-high register during *writes* but not during reads, and is not shown in drawings).

t861

http://www.atmel.com/Images/doc8027.pdf

------------

Atomicity!

You've already seen the earlier examples where mistakenly-written code could cause troubles... Even when accessing two *different* internal registers like ICR1 and TCNT1, the temp-register may contain unexpected data, etc.

And, you may already be familiar with the risks of accessing a multi-byte variable both inside and outside interrupts.

E.G. Briefly:

uint16_t globalVar = 0;

main()
{
...
   while(1)
   {
      printf("0x%x, ", globalVar);
   }
}

InterruptHandler()
{
    globalVar++;
}
Keep in mind, each access to globalVar is actually *two* instructions... one byte apiece.

So there may be a case where, say, globalVar = 0x00ff. globalVar's low-byte=0xff is read in main(), an interrupt occurs, now globalVar = 0x0100. The interrupt returns to main(), where globalVar's high-byte is read as 0x01.

Now we get the printout: "0xfe, 0x1ff, 0x101" Whoops!

So, the solution is to "make it atomic", or, essentially, disable interrupts while globalVar is being read, in main:

main()
{
...
   while(1)
   {
      uint16_t temp;
      cli();
        temp = globalVar;
      sei();   
      printf("0x%x, ", temp);
   }
}
Imagine how much worse it could be in the case of the many 16-bit registers which *share* a temporary-high-byte register!

Basically, if you access *any* 16-bit register both in main() *and* in an interrupt, then you'd better make them *all* atomic. All of them!

(So, now we're at: even if you're planning to use a 16-bit timer in 8-bit mode, each access to each register will amount to *numerous* instructions: Read the current status of Interrupt-Enable. Disable Interrupts. Access BOTH registers, in order. Restore Interrupt-Enable-Status.)

Discussions