ATMega: why the need to copy the ADC 16-bit pseudo-register's value before use? - Page 1
Forum section: electronics / general

author: HwAoRrDk
author_rank: Super Contributor
timestamp: February 02, 2017, 02:20:44 pm
content: I have some code that samples the ADC on an ATmega328P. But there's some behaviour that I don't understand the nature of.Can someone explain why I appear to need to copy the value of the 16-bitADCpseudo-register to a temporary variable before doing something with it? If I try using it directly, it always seems to have a value of zero.For example, here is a stripped-down example of what I was doing in my code:Code:typedef struct {uint32_t otherVal;uint16_t adcVal;} myStruct_t;/* Triggered by timer @ 1KHz */ISR(ADC_vect) {myStruct_t newVal = { someOtherVal, ADC };circBuf.Add(&newVal);}/* circBuf is a global var of an instance of this class: */template <class T, size_t MAX_SIZE> class CircularBuffer {/* Other code omitted for brevity... */void Add(const T *data) {head = (head + 1) % MAX_SIZE;if(count < MAX_SIZE) count++;memcpy(&buffer, data, sizeof(T));}}But this would result in all entries in my buffer having zero for the value read from ADC. But, if in my ADC interrupt routine I do this instead:Code:ISR(ADC_vect) {uint16_t result = ADC; // <----myStruct_t newVal = { someOtherVal, result };circularBuffer.Add(&newVal);}I get the sampled ADC value as expected. What's going on here?Is it something to do with putting the value into a struct? Or perhaps that I am passing a pointer to the struct into theAdd()method, which then copies the data withmemcpy()? Or somehow related to the need to read the underlyingADCLandADCHregisters in order? Are there situations where the 16-bitADCpsuedo-register can be used and this doesn't happen? (I have no idea how that macro works behind the scenes - presumably some wizardry.)

author: Len
author_rank: Frequent Contributor
timestamp: February 02, 2017, 05:49:08 pm
content: I would expect it to work the same in both cases. In fact, the compiler would likely generate the same code for both because the value of newVal does nothing other than get put into the struct. But it's hard to be sure without seeing the definition of myStruct_t.If it were me, this is the point where I'd start looking at the machine or assembler code produced by the compiler.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: February 02, 2017, 06:31:58 pm
content: Oh yeah, I forgot the struct definition. In case it matters, I have added it to my original post.One thing I thought of earlier was perhaps generating code that lazily evaluated the values in the struct, so the data wasn't read from the ADC registers untilmemcpy()was invoked, and that doesn't copy the bytes of the pseudo-register's value in the required order (i.e.ADCLthenADCH. I tried looking at avr-libc's implementation ofmemcpy(), but it's in assembler, so I have no clue what I'm looking at.I found the.lssfile with the assembly output from compilation of my code, but again, I'm not sure what to look at.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: February 02, 2017, 08:17:45 pm
content: I found what appears to be the code for the ADC ISR ('__vector_21') in the assembly, and it's almost entirely identical in both versions. Literally the only difference in the code is as follows.This is usingADCdirectly:Code:808:40 91 9b 02ldsr20, 0x029B; 0x80029b <someOtherVal>80c:50 91 9c 02ldsr21, 0x029C; 0x80029c <someOtherVal+0x1>810:60 91 9d 02ldsr22, 0x029D; 0x80029d <someOtherVal+0x2>814:70 91 9e 02ldsr23, 0x029E; 0x80029e <someOtherVal+0x3>818:20 91 78 00ldsr18, 0x0078; 0x800078 <__TEXT_REGION_LENGTH__+0x7e0078>81c:30 91 79 00ldsr19, 0x0079; 0x800079 <__TEXT_REGION_LENGTH__+0x7e0079>And this is with assigningADCto a temp variable:Code:808:20 91 78 00ldsr18, 0x0078; 0x800078 <__TEXT_REGION_LENGTH__+0x7e0078>80c:30 91 79 00ldsr19, 0x0079; 0x800079 <__TEXT_REGION_LENGTH__+0x7e0079>810:40 91 9b 02ldsr20, 0x029B; 0x80029b <someOtherVal>814:50 91 9c 02ldsr21, 0x029C; 0x80029c <someOtherVal+0x1>818:60 91 9d 02ldsr22, 0x029D; 0x80029d <someOtherVal+0x2>81c:70 91 9e 02ldsr23, 0x029E; 0x80029e <someOtherVal+0x3>According to the datasheet, 0x78 and 0x79 are the addresses of theADCLandADCHregisters, respectively. So, it just seems to be doing stuff with them in a different order?Oh, by the way, I think the compiler inlined my calls to theAdd()method of the buffer class, as I couldn't find any hint of that code separately elsewhere in the file. But if it did so - like I said above - it's identical code there for each.

author: Jeroen3
author_rank: Super Contributor
timestamp: February 02, 2017, 08:30:35 pm
content: The 16 bit registers is locked by hardware when reading to prevent it from changing during read. An AVR can't do an atomic read of 16 bits.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 02, 2017, 08:40:03 pm
content: RTFM: the high byte is latched into a temporary register during reading. The values are read out as a pair. Any delay in reading will cause the data to be lost.This is usually handled transparently by the compiler, but it shouldn't be forgotten by the programmer.Tim

author: HwAoRrDk
author_rank: Super Contributor
timestamp: February 02, 2017, 09:53:23 pm
content: Well, I have RTFM, and sorry but nothing jumps out at me that explains why I am seeing the behaviour I am.For a start, I'm not explicitly doing anything in my code withADCL/ADCH; I'm usingADCand leaving the compiler to handle it. If something is interfering with the orderly reading ofADCLandADCHin sequence, then what the hell am I supposed to do about that?Also, I'm only readingADCwithin theADC_vectISR where interrupts are globally disabled, so I don't see how any other code dealing with 16-bit registers (such as that for Timer1) can clobber the temp register and lose the value. (If it does indeed utilise it - there's actually no mention of that in the entire section covering the ADC, only about the locking.)

author: JPortici
author_rank: Super Contributor
timestamp: February 02, 2017, 11:11:48 pm
content: i have no idea of the internal workings of the 328but on modern PIC16/18 the 16 bit timers can be configured to have syncronous or asyncronous read/writes on the timers counters registers... which mean that in syncronous mode the TMRxH register is available/written after having read/wrote the TMRxL register, otherwise the content is lost. (this is to prevent the case of reading / writing TMRxH when TMRxL is 0xFF)maybe something simillar is happening, as teslacoil is suggesting

author: Jeroen3
author_rank: Super Contributor
timestamp: February 03, 2017, 05:52:54 pm
content: Disabling interrupts is not enough. The hardware itself can modify the register. This is why it's buffered when you read the first 8 bit. The buffer is unlocked after the next 8 bit is read.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: February 03, 2017, 06:42:07 pm
content: Alright, alright, so at some point, somehow, the low 8 bits are being read but the high 8 bits are not, meaning no further updating of the registers is possible by the ADC, hence getting a value of zero forever onwards. Yes, I get that. But how!?!I am never explicitly reading the low/high registers myself, I am letting the compiler do the work by only using the single 16-bit wideADCpseudo-register in my code.I have no control or insight over how or when the low & high registers are being read!I went spelunking in the avr-libc code, trying to track down what the hell theADCmacro actually is/does. Came up with the following:Code:/* In iom328p.h: */#define ADC _SFR_MEM16(0x78)/* In sfr_defs.h: */#define _SFR_MEM16(mem_addr) _MMIO_WORD(mem_addr)#define _MMIO_WORD(mem_addr) (*(volatile uint16_t *)(mem_addr))So, if I am interpreting correctly,ADCis defined as the dereferenced unsigned 16-bit integer value of a pointer whose address is the location of theADCLregister. So, please tell me, how is any piece of code that accesses this value going to end up reading anything other than the entire 16 bits of data spanning both low and high registers?

author: T3sl4co1l
author_rank: Super Contributor
timestamp: February 03, 2017, 09:35:36 pm
content: All the compiler knows is it's reading memory. If it's evaluating, say, a 16-bit multiply, then it needs those bytes at different times, and may assume that the two bytes can be read in a non-atomic way.Indeed, there are direct IO port instructions: SBI, CBI, IN and OUT, available for the lowest IO addresses. They are also one or more cycles faster than the alternative. Compilers don't use them because they aren't very general, whereas the LD/ST instructions can be used anywhere in the address space.You have plenty of control over how the registers are read! Inspect the machine code output. If it isn't done in a satisfactory way, look at ways of changing it: read it into a temporary variable; read the two bytes into an array in quick succession (maybe, but this has other problems); inline some assembler to do it yourself; etc.Tim

author: HwAoRrDk
author_rank: Super Contributor
timestamp: February 03, 2017, 10:00:10 pm
content: Okay, so you're saying that sometimes the compiler gets it wrong - or rather, doesn't know any better - and may end up generating code that reads the two bytes from the given address in the wrong order, inadvertently blocking the ADC from updating with new sampled values any further.But the thing I still don't understand is how this possible explanation relates to what is going on in the assembly code as I posted before. It's my interpretation (and I readily admit I don't really know what I'm doing when it comes to assembly language) that, in both cases, the code is simply loading each byte from the relevant address into another register, and then all further code uses those two other registers. And more importantly, seems to be doing itin the correct order- 0x78/ADCL followed by 0x79/ADCH - inbothcases.I suppose I should be thankful I already have the solution - like you said, copy into a temporary variable first. But this has been driving me mad because I don't understand what's going on.

author: mikerj
author_rank: Super Contributor
timestamp: February 03, 2017, 10:43:25 pm
content: Quote from: HwAoRrDk on February 02, 2017, 06:31:58 pmThe code you've shown is reading the value of ADCL and ADCH (in the correct order) into a pair of registers, but you haven't shown what the compiler is doing with those registers afterwards. Could you post the entire assembly listing the of the broken version of the ISR?

author: HwAoRrDk
author_rank: Super Contributor
timestamp: February 03, 2017, 11:44:58 pm
content: Sure, why not. Here it is:Code:000007ea <__vector_21>:7ea:1f 92pushr17ec:0f 92pushr07ee:0f b6inr0, 0x3f; 637f0:0f 92pushr07f2:11 24eorr1, r17f4:2f 93pushr187f6:3f 93pushr197f8:4f 93pushr207fa:5f 93pushr217fc:6f 93pushr227fe:7f 93pushr23800:8f 93pushr24802:9f 93pushr25804:ef 93pushr30806:ff 93pushr31808:40 91 9b 02ldsr20, 0x029B; 0x80029b <someOtherVal>80c:50 91 9c 02ldsr21, 0x029C; 0x80029c <someOtherVal+0x1>810:60 91 9d 02ldsr22, 0x029D; 0x80029d <someOtherVal+0x2>814:70 91 9e 02ldsr23, 0x029E; 0x80029e <someOtherVal+0x3>818:20 91 78 00ldsr18, 0x0078; 0x800078 <__TEXT_REGION_LENGTH__+0x7e0078>81c:30 91 79 00ldsr19, 0x0079; 0x800079 <__TEXT_REGION_LENGTH__+0x7e0079>820:80 91 7c 00ldsr24, 0x007C; 0x80007c <__TEXT_REGION_LENGTH__+0x7e007c>824:8f 70andir24, 0x0F; 15826:19 f0breq.+6; 0x82e <__vector_21+0x44>828:81 30cpir24, 0x01; 182a:59 f1breq.+86; 0x882 <__vector_21+0x98>82c:53 c0rjmp.+166; 0x8d4 <__vector_21+0xea>82e:ed e8ldir30, 0x8D; 141830:f2 e0ldir31, 0x02; 2832:80 81ldr24, Z834:91 81lddr25, Z+1; 0x01836:01 96adiwr24, 0x01; 1838:8f 71andir24, 0x1F; 3183a:99 27eorr25, r2583c:91 83stdZ+1, r25; 0x0183e:80 83stZ, r24840:e0 91 8f 02ldsr30, 0x028F; 0x80028f <circBufA+0xc2>844:f0 91 90 02ldsr31, 0x0290; 0x800290 <circBufA+0xc3>848:e0 32cpir30, 0x20; 3284a:f1 05cpcr31, r184c:28 f4brcc.+10; 0x858 <__vector_21+0x6e>84e:31 96adiwr30, 0x01; 1850:f0 93 90 02sts0x0290, r31; 0x800290 <circBufA+0xc3>854:e0 93 8f 02sts0x028F, r30; 0x80028f <circBufA+0xc2>858:fc 01movwr30, r2485a:ee 0faddr30, r3085c:ff 1fadcr31, r3185e:e8 0faddr30, r24860:f9 1fadcr31, r25862:ee 0faddr30, r30864:ff 1fadcr31, r31866:e3 53subir30, 0x33; 51868:fe 4fsbcir31, 0xFE; 25486a:40 83stZ, r2086c:51 83stdZ+1, r21; 0x0186e:62 83stdZ+2, r22; 0x02870:73 83stdZ+3, r23; 0x03872:35 83stdZ+5, r19; 0x05874:24 83stdZ+4, r18; 0x04876:ec e7ldir30, 0x7C; 124878:f0 e0ldir31, 0x00; 087a:80 81ldr24, Z87c:81 60orir24, 0x01; 187e:80 83stZ, r24880:29 c0rjmp.+82; 0x8d4 <__vector_21+0xea>882:e9 ecldir30, 0xC9; 201884:f1 e0ldir31, 0x01; 1886:80 81ldr24, Z888:91 81lddr25, Z+1; 0x0188a:01 96adiwr24, 0x01; 188c:8f 71andir24, 0x1F; 3188e:99 27eorr25, r25890:91 83stdZ+1, r25; 0x01892:80 83stZ, r24894:e0 91 cb 01ldsr30, 0x01CB; 0x8001cb <circBufB+0xc2>898:f0 91 cc 01ldsr31, 0x01CC; 0x8001cc <circBufB+0xc3>89c:e0 32cpir30, 0x20; 3289e:f1 05cpcr31, r18a0:28 f4brcc.+10; 0x8ac <__vector_21+0xc2>8a2:31 96adiwr30, 0x01; 18a4:f0 93 cc 01sts0x01CC, r31; 0x8001cc <circBufB+0xc3>8a8:e0 93 cb 01sts0x01CB, r30; 0x8001cb <circBufB+0xc2>8ac:fc 01movwr30, r248ae:ee 0faddr30, r308b0:ff 1fadcr31, r318b2:e8 0faddr30, r248b4:f9 1fadcr31, r258b6:ee 0faddr30, r308b8:ff 1fadcr31, r318ba:e7 5fsubir30, 0xF7; 2478bc:fe 4fsbcir31, 0xFE; 2548be:40 83stZ, r208c0:51 83stdZ+1, r21; 0x018c2:62 83stdZ+2, r22; 0x028c4:73 83stdZ+3, r23; 0x038c6:35 83stdZ+5, r19; 0x058c8:24 83stdZ+4, r18; 0x048ca:ec e7ldir30, 0x7C; 1248cc:f0 e0ldir31, 0x00; 08ce:80 81ldr24, Z8d0:8e 7fandir24, 0xFE; 2548d2:80 83stZ, r248d4:ff 91popr318d6:ef 91popr308d8:9f 91popr258da:8f 91popr248dc:7f 91popr238de:6f 91popr228e0:5f 91popr218e2:4f 91popr208e4:3f 91popr198e6:2f 91popr188e8:0f 90popr08ea:0f beout0x3f, r0; 638ec:0f 90popr08ee:1f 90popr18f0:18 95retiThat came from the following C code. There's a little more to it than the stripped-down example I originally posted, but not much; just a few lines to alternate back-and-forth between two different ADC channels and buffers.Code:ISR(ADC_vect) {myStruct_t newVal = { someOtherVal, ADC };switch(ADMUX & (_BV(MUX3) | _BV(MUX2) | _BV(MUX1) | _BV(MUX0))) {case 0b00000000:circBufA.Add(&newVal);ADMUX |= _BV(MUX0);break;case 0b00000001:circBufB.Add(&newVal);ADMUX &= ~(_BV(MUX0));break;}}As I said earlier, I believe (although I'm not 100% certain) the compiler has inlined the calls toAdd().