zx-128A1 Dev Board: Reading ADC Calibration

Discussion specific to the ZX-1281 and ZX-1280 microcontrollers as well as the ZX-1281 and ZX-1280 Dev Boards.
Post Reply
dajacobs
Posts: 5
Joined: 29 November 2010, 15:20 PM

zx-128A1 Dev Board: Reading ADC Calibration

Post by dajacobs »

Hello,
I am having some issues retrieving the calibration values from the flash memory on the board. I am not sure if the post should be here or in the code section so feel free to move it as necessary.

The manual gives three steps
1.Load the Z-pointer with the byte address to read.
2.Load the NVM CMD register with the Read User Signature Row / Calibration
3.Execute the LPM instruction.

When I am loading the Command into the register, I am getting nonsense characters in the output even before I execute the lpm instruction.

Code: Select all

public const NVM_CMD_READ_USER_CALIB_ROW as byte = &H02
'~ 1.Load the Z-pointer with the byte address to read.
#asm
ldi R30, 0x20
#endasm

'~ 2.Load the NVM CMD register with the Read User Signature Row / Calibration Row command
Register.NVM_CMD = NVM_CMD_READ_USER_CALIB_ROW

'~ 3.Execute the LPM instruction.
#asm
lpm
#endasm
I would expect the output from this to be the Low Byte ADCACAL0 in register R0.

My last question is what is the best way to then take the value from the R0 register and store it into the calibration register of the ADC. To be honest, I've written assembly and c separately but this is my first mix of the two.

Thanks,
Daniel
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Re: zx-128A1 Dev Board: Reading ADC Calibration

Post by dkinzer »

dajacobs wrote:I am having some issues retrieving the calibration values from the flash memory on the board.
The xmega initialization code reads the ADC calibration values and writes them to the ADC. Consequently, what you're trying to do is unnecessary but you may want to try adjusting the calibration value. As a side note, if you include the options --list and --keep-files (on separate lines) in your .pjt file, after you compile you'll find a file with the same name as your project file but with a .lss extension in a sub-directory of the directory zxTempDir contained in the same directory as your project file. The .lss file contains an assembly language listing of your compiled application; search for readCalibrationWord in this file to find the code.
dajacobs wrote:When I am loading the Command into the register, I am getting nonsense characters in the output even before I execute the lpm instruction.
That is not surprising. You can't just load values into registers willy nilly because the (backend) compiler implements its own register usage strategy and interfering with it will almost always produce odd results.

The initialization code mentioned above uses an undocumented internal function to read values from the "production signature row" of the xmega. The name of the function is readCalibrationWord() and it takes an unsigned 8-bit parameter giving the offset from which to read, returning an unsigned 16-bit value. You can access this function from ZBasic by including the following declaration in your application.

Code: Select all

Declare Function readCalibrationWord(ByVal idx as Byte) As UnsignedInteger
dajacobs wrote:My last question is what is the best way to then take the value from the R0 register and store it into the calibration register of the ADC.
The simplest method, if you need assembly language code, is to implement a C-callable function in a separate assembly language file (having a .S (upper case S) extension). To do this, you'll need to understand the register usage conventions of the backend (gcc) compiler as far as how parameters are passed, how values are returned, which registers must be preserved and which registers may be modified by your assembly language function. This information is available in a section of the avr-libc documentation.

In some cases, it is easier to implement a separate C function (callable from ZBasic) that may have short sequences of inline assembly language. The inline assembler interface is very powerful but very complex and it takes quite a bit of study to master it. This information about inline assembly code is available in a section of the avr-libc documentation. I've included an implementation of GetCalibrationWord() below that illustrates what can be done.

Code: Select all

Declare Function GetCalibrationWord(ByVal idx as UnsignedInteger) As UnsignedInteger
#c
uint16_t
GetCalibrationWord(uint16_t idx)
{
    uint16_t val;
    uint8_t sreg, tmpReg;
    sreg = dsblInt();
    __asm__ __volatile__(
        "ldi %0, 0x02"     "\n\t"
        "sts 0x1ca, %0"    "\n\t"
        "lpm %A1, Z+"      "\n\t"
        "lpm %B1, Z+"      "\n\t"
        "sts 0x1ca, r1"    "\n\t"
        : "=&d" (tmpReg), "=&d" (val)
        : "z" (idx)
    );
    enblInt(sreg);
    return(val);
}
#endc

Sub Main()
    Debug.Print "&H"; CStrHex(GetCalibrationWord(&H20))
End Sub
An alternate form implemented in C only might be:

Code: Select all

Declare Function GetCalibrationWord(ByVal idx as UnsignedInteger) As UnsignedInteger
#c
uint16_t
GetCalibrationWord(uint16_t idx)
{
    uint16_t val;
    uint8_t sreg;
    sreg = dsblInt();
    NVM_CMD = NVM_CMD_READ_CALIB_ROW_gc;
    val = pgm_read_word(idx);
    NVM_CMD = NVM_CMD_NO_OPERATION_gc;
    enblInt(sreg);
    return(val);
}
#endc
Last edited by dkinzer on 10 December 2010, 15:16 PM, edited 1 time in total.
- Don Kinzer
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

After thinking about the code that I posted above, I realized that I had forgotten to disable interrupts while the NVM_CMD register was set for reading the production signature row. I've modified the two functions to do this.

Also, the internal routine readCalibrationWord() doesn't disable interrupts because it is called early in the startup process when interrupts are not yet enabled. Consequently, it is not suitable to use in an application unless you add code around it to disable interrupts during the call.
- Don Kinzer
dajacobs
Posts: 5
Joined: 29 November 2010, 15:20 PM

Post by dajacobs »

Don,
Thank you for the response. I guess I have been bashing my head over this process for nothing. But to clarify, when you refer to the XMEGA initialization code when is that process accomplished? The XMEGA A manual says that the

Code: Select all

ADCACAL0 and ADCACAL1 contains the calibration value for the Analog to Digital Converter A(ADCA). Calibration of the Analog to Digital Converters are done during device. The calibration bytes are not loaded automatically into the ADC calibration registers, and this must be done from software


I figured as the user, I was responsible for writing that code myself. I added the options as you mentioned to my .pjt file but could not find the readCalibrationByte so maybe I am missing some configuration.

My intent was not to modify registers willy nilly but it was not clear from my study of the manuals that the ZBasic command (Register.NVM_CMD = NVM_CMD_READ_CALIB_ROW_gc) produced a different result than the C Command (NVM_CMD = NVM_CMD_READ_CALIB_ROW_gc).
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

dajacobs wrote:But to clarify, when you refer to the XMEGA initialization code when is that process accomplished?
There is some initialization code that is pulled in from the ZX Library when your application is linked. Consider this simple program:

Code: Select all

Dim adcVal as Integer
Sub Main()
  adcVal = GetADC(A.0)
End Sub
When this application is compiled for xmega-based ZX with the compiler flags --list and --keep-files, the resulting .lss file contains these fragments:

Code: Select all

     392&#58;  0e 94 39 09   call  0x1272  ; 0x1272 <calibrateADC>
...
00001272 <calibrateADC>&#58;
    1272&#58;  80 e2         ldi  r24, 0x20  ; 32
    1274&#58;  0e 94 48 09   call  0x1290  ; 0x1290 <readCalibrationWord>
    1278&#58;  e0 e0         ldi  r30, 0x00  ; 0
    127a&#58;  f2 e0         ldi  r31, 0x02  ; 2
    127c&#58;  84 87         std  Z+12, r24  ; 0x0c
    127e&#58;  95 87         std  Z+13, r25  ; 0x0d
    1280&#58;  84 e2         ldi  r24, 0x24  ; 36
    1282&#58;  0e 94 48 09   call  0x1290  ; 0x1290 <readCalibrationWord>
    1286&#58;  e0 e4         ldi  r30, 0x40  ; 64
    1288&#58;  f2 e0         ldi  r31, 0x02  ; 2
    128a&#58;  84 87         std  Z+12, r24  ; 0x0c
    128c&#58;  95 87         std  Z+13, r25  ; 0x0d
    128e&#58;  08 95         ret

00001290 <readCalibrationWord>&#58;
    1290&#58;  aa ec         ldi  r26, 0xCA  ; 202
    1292&#58;  b1 e0         ldi  r27, 0x01  ; 1
    1294&#58;  92 e0         ldi  r25, 0x02  ; 2
    1296&#58;  9c 93         st  X, r25
    1298&#58;  90 e0         ldi  r25, 0x00  ; 0
    129a&#58;  fc 01         movw  r30, r24
    129c&#58;  25 91         lpm  r18, Z+
    129e&#58;  34 91         lpm  r19, Z+
    12a0&#58;  1c 92         st  X, r1
    12a2&#58;  c9 01         movw  r24, r18
    12a4&#58;  08 95         ret
dajacobs wrote:My intent was not to modify registers willy nilly
It wasn't my intent to be insulting but, rather, to make a point. I should have phrased it differently.

I was referring to the fact that the two #asm...#endasm sequences changed the processor general purpose registers (i.e. r0 through r31). There is a way to inform the back end compiler that an assembly language sequence modifies specific registers but even that mechanism doesn't allow for conveying register usage information from one inline assembly sequence to another. Generally, each inline assembly sequence must either stand alone or else convey intermediate results through C variables. This limitation, along with the very complex syntax of the gcc inline assembly construct, leads many to opt for writing C-callable assembly routines rather than using inline assembly. You'll see advice to do just this quite frequently on the avrfreaks.net forum.

Below is an example of a C-callable assembly language function to read a calibration word. Place this in a file called, for example, readCalWord.S (note that an upper case S extension *must* be used) and add that filename to your .pjt file.

Code: Select all

#include <avr/io.h>

#if !defined&#40;NVM_CMD_READ_CALIB_ROW_gc&#41;
    #define NVM_CMD_READ_CALIB_ROW_gc       0x02
#endif

    .section .text

/*
 ** readCalWord
 *
 * Read a 16-bit value from the production signature row.
 *
 * On entry&#58;   r24 has the index from which to read
 *
 * On exit&#58;    r25&#58;24 has the 16-bit value
 *
 * C-callable&#58; uint16_t readCalWord&#40;uint8_t idx&#41;
 *
 * Modifies&#58;   r22, r24, r25, r30, r31
 *
 */
    .global readCalWord
readCalWord&#58;
#define sregSave    r22
#define idx         r24
#if defined&#40;__AVR_XMEGA__&#41;
    // make r31&#58;30 point to the desired word to read
    mov        r30, idx
    clr        r31

    // disable interrupts
    in        sregSave, SREG
    cli

    // read the calibration row word
    ldi        r24, NVM_CMD_READ_CALIB_ROW_gc
    sts        NVM_CMD, r24
    lpm        r24, Z+
    lpm        r25, Z

    // restore the NVM command register and the interrupt enable status
    sts        NVM_CMD, r1
    out        SREG, sregSave
#else
    ldi        r24, 0
    ldi        r25, 0
#endif
    ret
#undef sregSave
#undef idx

.end
Then, you can call the assembly language function like this:

Code: Select all

Declare Function readCalWord&#40;ByVal idx as Byte&#41; as UnsignedInteger

Sub Main&#40;&#41;
    Debug.Print "&H"; CStrHex&#40;readCalWord&#40;&H20&#41;&#41;
End Sub
- Don Kinzer
dajacobs
Posts: 5
Joined: 29 November 2010, 15:20 PM

Post by dajacobs »

dkinzer wrote:Generally, each inline assembly sequence must either stand alone or else convey intermediate results through C variables. This limitation, along with the very complex syntax of the gcc inline assembly construct, leads many to opt for writing C-callable assembly routines rather than using inline assembly.
Thank you that is much clearer. I thought that the asm blocks were simply inserting verbatim assembly code in between the blocks that were converted from the zbasic to c to assembly in the complier.

With regards to the readCalibrationWord. I have written my code to read registers such as (Register.ADCA_CH0_RES) rather than the getADC library function. Could that be the reason why I don't have a readCalibrationWord in my .lss file?

Should I convert my code to use the getADC, or will calling a blanket getADC at the start of my code be sufficient to load the calibration in at the start?

Thank you,
Daniel
dkinzer
Site Admin
Posts: 3120
Joined: 03 September 2005, 13:53 PM
Location: Portland, OR

Post by dkinzer »

dajacobs wrote:I thought that the asm blocks were simply inserting verbatim assembly code in between the blocks that were converted from the zbasic to c to assembly in the complier.
Well, in fact, that is exactly what happens. The #asm..#endasm construct is translated into a gcc inline assembly construct. You can see the translated inline assembly code in the .c files that are left in the zxTempDir/<project-name> directory when the --keep-files option is used.

The problem is that the assembly code *must* be written in a way that cooperates with the backend compiler (avr-gcc), meaning that it must either not modify *any* general purpose registers (as a net result) or it must inform the compiler as to which general purpose registers were modified so that the gcc code generator can take that information into account in its register allocation strategy.
dajacobs wrote:I have written my code to read registers such as (Register.ADCA_CH0_RES) rather than the getADC library function.
In that case, the ADC calibration code is not included. You could include a token GetADC() call to get the ADC calibration code loaded or you could use one of the several functions to read calibration words and write them to the ADC calibration registers. An alternate strategy is to use an undocumented means to load the ADC calibration code:

Code: Select all

#c
    REQUEST_ENTRY&#40;adcCal&#41;;
#endc
- Don Kinzer
Post Reply