Author Topic: Agilent 4338B - Alternative 'calibration' method  (Read 2483 times)

0 Members and 1 Guest are viewing this topic.

Online gamalotTopic starter

  • Super Contributor
  • ***
  • Posts: 1386
  • Country: au
  • Correct my English
    • Youtube
Agilent 4338B - Alternative 'calibration' method
« on: March 31, 2022, 06:52:42 pm »
I got an Agilent 4438B milliohmmeter a couple of days ago, after a quick test I found that it has gain error, all ranges seem to be reading about 0.3%-0.5% less.

Since I couldn't find the software for calibrating the 4338B (and others in the series), I tried to figure out how it was calibrated by reading its EEPROM and firmware, unfortunately I didn't succeed.

So I started my plan B, I reverse engineered the voltage sense amplifier and then increased its gain by modifying the resistor values in the feedback network.

Now its error is within the acceptable range, I am very happy!

Memory dumps and schematics attached:
 
The following users thanked this post: TiN, materialsguy, chuckb, alm

Offline TimFox

  • Super Contributor
  • ***
  • Posts: 8427
  • Country: us
  • Retired, now restoring antique test equipment
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #1 on: March 31, 2022, 07:02:49 pm »
I can understand why the non-repairability of secret software would drive one to re-invent the trimpot.  Good work!
 

Offline tv84

  • Super Contributor
  • ***
  • Posts: 3312
  • Country: pt
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #2 on: September 02, 2022, 01:28:05 pm »
So, with

:TEST:MEM:ADDR  xxxx
:TEST:MEM:LONG?


you can read the whole EEPROM space?

What about?

:TEST:MEM:ADDR xxxx
:TEST:MEM:LONG


Can you set values in the EEPROM with the :TEST:MEM:LONG command?
 

Online gamalotTopic starter

  • Super Contributor
  • ***
  • Posts: 1386
  • Country: au
  • Correct my English
    • Youtube
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #3 on: September 02, 2022, 02:48:59 pm »
So, with

:TEST:MEM:ADDR  xxxx
:TEST:MEM:LONG?


you can read the whole EEPROM space?

What about?

:TEST:MEM:ADDR xxxx
:TEST:MEM:LONG


Can you set values in the EEPROM with the :TEST:MEM:LONG command?

Yes, I wrote a small piece of Python code to read the all 8K bytes from EEPROM by using the command found by @Miek.

After comparison, I found that the first 4K bytes are exactly same to what I read with the programmer before, and the other 4K bytes are different, but I guess possibly they are used to store something like user settings so it's ok.

I did write to EEPROM and it was also successful.
 
The following users thanked this post: tv84, Miek

Offline Miek

  • Regular Contributor
  • *
  • Posts: 81
  • Country: gb
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #4 on: September 02, 2022, 09:40:22 pm »
Just in case you (or anyone else) do end up wanting to look further into editing the calibration data, here's what I could work out on it so far:

The calibration block is stored at 0x80 in the EEPROM and is 0xf7e bytes long. There is a checksum for it at 0xFFE, which needs to be correct. This is calculated by adding up all the bytes in the block and casting it down to a uint16.

The actual calibration constants don't take up much of that block. They're stored as floats at the start of the block. The first four floats are handled specially and I'm guessing that they're offsets, since they're very small and can be positive or negative in your example. Also the firmware defaults them to 0.0 if the EEPROM data is invalid.

After that, there are pairs of gain & offset corrections. They are loaded as groups of 3 pairs, for a total of 5 groups. I assume these will correspond to points throughout the measurement ranges.

These are the float values from your EEPROM:
Code: [Select]
$ od -Ax -j 0x80 --endian=big -f '4338B EEPROM DUMP.BIN'
000080    -1.33072e-05     -7.6217e-06   1.6031385e-05   1.7778738e-06
000090        1.014507     -0.02629138       1.0134906    -0.026007662
0000a0       1.0134878    -0.026201833       1.0039018    -0.025654815
0000b0        1.002896    -0.025374427       1.0028933    -0.025566569
0000c0       1.0030564    -0.025575776       1.0020514    -0.025295684
0000d0       1.0020487    -0.025487663       1.0039852    -0.025926284
0000e0       1.0029793    -0.025645602       1.0029765     -0.02583776
0000f0        1.004992    -0.026207505       1.0039852    -0.025926284
000100       1.0039824    -0.026118636            -nan            -nan
000110            -nan            -nan            -nan            -nan
*
« Last Edit: September 03, 2022, 06:38:53 am by Miek »
 
The following users thanked this post: gamalot

Online gamalotTopic starter

  • Super Contributor
  • ***
  • Posts: 1386
  • Country: au
  • Correct my English
    • Youtube
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #5 on: September 03, 2022, 06:18:25 am »
Just in case you (or anyone else) do end up wanting to look further into editing the calibration data, here's what I could work out on it so far:

The calibration block is stored at 0x80 in the EEPROM and is 0xf7e bytes long. There is a checksum for it at 0xFFE, which needs to be correct. This is calculated by adding up all the bytes in the block, adding 0x1fe, and casting it down to a uint16.

The actual calibration constants don't take up much of that block. They're stored as floats at the start of the block. The first four floats are handled specially and I'm guessing that they're offsets, since they're very small and can be positive or negative in your example. Also the firmware defaults them to 0.0 if the EEPROM data is invalid.

After that, there are pairs of gain & offset corrections. They are loaded as groups of 3 pairs, for a total of 5 groups. I assume these will correspond to points throughout the measurement ranges.

These are the float values from your EEPROM:
Code: [Select]
$ od -Ax -j 0x80 --endian=big -f '4338B EEPROM DUMP.BIN'
000080    -1.33072e-05     -7.6217e-06   1.6031385e-05   1.7778738e-06
000090        1.014507     -0.02629138       1.0134906    -0.026007662
0000a0       1.0134878    -0.026201833       1.0039018    -0.025654815
0000b0        1.002896    -0.025374427       1.0028933    -0.025566569
0000c0       1.0030564    -0.025575776       1.0020514    -0.025295684
0000d0       1.0020487    -0.025487663       1.0039852    -0.025926284
0000e0       1.0029793    -0.025645602       1.0029765     -0.02583776
0000f0        1.004992    -0.026207505       1.0039852    -0.025926284
000100       1.0039824    -0.026118636            -nan            -nan
000110            -nan            -nan            -nan            -nan
*

Hi Miek, thank you so much for help me again!

I tried some work you shared here before, and then I gave up.  :-[

I still have the Python code I wrote for checksum, it simply adds those bytes from 0x80 to 0xffd, then gets a 16-bit integer, and put this 16-bit integer into offset 0xffe-0xfff. I believe you got the algorithm by analyzing the decompiled code, there should be nothing wrong. Glad that at least our results were consistent.

About the calibration data, my previous thought was that they were in groups of four floats, corresponding to 8 ranges. But the fact that there are only 30 floats there, not the 32 I was expecting, I guess that's why I gave up at the time.

If you're reading the code, maybe you can help me out with this, I'd really appreciate it!

(I'm going to try that too, although the last time I did something like this was probably about 25 years ago)

Online gamalotTopic starter

  • Super Contributor
  • ***
  • Posts: 1386
  • Country: au
  • Correct my English
    • Youtube
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #6 on: September 03, 2022, 12:08:51 pm »
After that, there are pairs of gain & offset corrections. They are loaded as groups of 3 pairs, for a total of 5 groups. I assume these will correspond to points throughout the measurement ranges.

Now I think I know why the calibration data is stored in 5 groups, the signal source has 5 levels of output, 1uA, 10uA, 100uA, 1mA and 10mA.

Offline Miek

  • Regular Contributor
  • *
  • Posts: 81
  • Country: gb
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #7 on: September 04, 2022, 01:01:19 pm »
Now I think I know why the calibration data is stored in 5 groups, the signal source has 5 levels of output, 1uA, 10uA, 100uA, 1mA and 10mA.

Ah, nice! That makes sense.

I did a bit more digging and got a bit closer. I was wrong about some of the values being gains/offsets - they're actually all vectors/complex numbers (including the special ones at the start).

Here's my labelled version of the function that loads in the calibration constants:
Code: [Select]
void load_cal_data(short param_1)

{
    undefined3 extraout_var;
    undefined3 extraout_var_00;
    uint cal_invalid;
    bool bVar2;
    bool bVar3;
    uint uVar1;
    int i;
    int iVar4;
    cal_pair buf [15];
   
    cal_invalid = get_cal_invalid();
    if ((cal_invalid == 0) && (param_1 == 1)) {
                    /* load the first two complex numbers */
        eeprom_memcpy((int)&DAT_001e0100,(undefined *)&complex_00ffd990,8);
        eeprom_memcpy((int)&UNK_001e0110,(undefined *)&complex_00ffd988,8);
                    /* load the 5 groups of cal constants
                       
                       the first value in the group is copied twice then remaining two values are copied, resulting in a
                       set of four values in memory */
        eeprom_memcpy((int)&UNK_001e0120,(undefined *)buf,0x78);
        i = 0;
        do {
            cal_constants[i * 4].real = buf[i * 3].gain;
            cal_constants[i * 4].imag = buf[i * 3].offset;
            cal_constants[i * 4 + 1].real = buf[i * 3].gain;
            cal_constants[i * 4 + 1].imag = buf[i * 3].offset;
            cal_constants[i * 4 + 2].real = buf[i * 3 + 1].gain;
            cal_constants[i * 4 + 2].imag = buf[i * 3 + 1].offset;
            cal_constants[i * 4 + 3].real = buf[i * 3 + 2].gain;
            cal_constants[i * 4 + 3].imag = buf[i * 3 + 2].offset;
            i = i + 1;
        } while (i < 5);
        bVar2 = complex_mag_squared_limit1(&complex_00ffd990);
        bVar3 = complex_mag_squared_limit2(&complex_00ffd988);
        cal_invalid = CONCAT31(extraout_var,bVar2) | CONCAT31(extraout_var_00,bVar3);
        i = 0;
        do {
            iVar4 = 0;
            do {
                uVar1 = check_cal_constant_limits(cal_constants + iVar4 + i * 4);
                cal_invalid = cal_invalid | uVar1;
                iVar4 = iVar4 + 1;
            } while (iVar4 < 4);
            i = i + 1;
        } while (i < 5);
        CAL_DATA_VALID = 1;
    }
    if ((cal_invalid != 0) || (param_1 == 0)) {
                    /* set defaults */
        complex_00ffd988.real = 0;
        complex_00ffd988.imag = 0;
        complex_00ffd990.real = 0;
        complex_00ffd990.imag = 0;
        i = 0;
        do {
            iVar4 = 0;
            do {
                    /* 1.0 */
                cal_constants[iVar4 + i * 4].real = 0x3f800000;
                cal_constants[iVar4 + i * 4].imag = 0;
                iVar4 = iVar4 + 1;
            } while (iVar4 < 4);
            i = i + 1;
        } while (i < 5);
        CAL_DATA_VALID = 0;
    }
    FUN_000123b6();
    return;
}

It's called after the firmware does some basic checks for model number & checksum. It does some of its own checks to make sure the magnitude of the values are within reasonable limits.
It then calls out to a function that does a bit of maths on all of these numbers, along with a value in memory that is written by the SHORT correction process:

Code: [Select]
void FUN_000123b6(void)

{
    int iVar1;
    int i;
    complex cVar2;
   
    iVar1 = get_cal_select(0);
    complex_00ffda40.real = complex_00ffd988.real;
    complex_00ffda40.imag = complex_00ffd988.imag;
    i = 0;
    do {
        cVar2 = complex_unk2(&complex_00ffd988,&complex_00ffd990,cal_constants + iVar1 * 4 + i,&short_correction_value);
        complex_ARRAY_00ffda48[i].real = cVar2.real;
        complex_ARRAY_00ffda48[i].imag = cVar2.imag;
        cVar2 = complex_unk3(&complex_00ffd988,&complex_00ffd990,cal_constants + iVar1 * 4 + i,&short_correction_value);
        complex_ARRAY_00ffda68[i].real = cVar2.real;
        complex_ARRAY_00ffda68[i].imag = cVar2.imag;
        i = i + 1;
    } while (i < 4);
    return;
}

Code: [Select]
complex complex_unk2(complex *param_1,complex *param_2,complex *param_3,complex *param_4)

{
    complex temp_1;
    complex temp_2;
    complex temp_3;
    complex result;
   
    cmul(param_2,param_3,&temp_1);
    cadd(&temp_1,param_4,&temp_2);
    cmul(param_1,param_4,&temp_1);
    cadd(&temp_1,param_3,&temp_3);
    cdiv(&temp_2,&temp_3,&result);
                    /* result = (param_2 * param_3 + param_4) / (param_1 * param_4 + param_3) */
    return result;
}

Code: [Select]
complex complex_unk3(complex *param_1,complex *param_2,complex *param_3,complex *param_4)

{
    complex temp;
    complex result;
   
    cmul(param_1,param_4,&temp);
    cadd(&temp,param_3,&result);
    return result;
}

These intermediate values are then used in what I think is the actual correction code after taking a measurement:

Code: [Select]
        if (correction_enabled == 1) {
            iVar1 = (int)(short)iVar1 - (int)SHORT_ARRAY_0001ccc8[iVar2];
            uStack36 = complex_unk1(&measurement_maybe,&complex_00ffda40,complex_ARRAY_00ffda48 + iVar1,
                                    complex_ARRAY_00ffda68 + iVar1);
        }
        else {
            uStack36 = (complex)CONCAT44(measurement_maybe.real,measurement_maybe.imag);
        }
                    /* apply CALC1/2:FORM settings, for R / R-L / R-X / etc. */
        format_measurement(&uStack36,CVar7,&local_16);
Code: [Select]
complex complex_unk1(complex *param_1,complex *param_2,complex *param_3,complex *param_4)

{
    uint uVar1;
    uint uVar2;
    uint uVar3;
    uint uVar4;
    undefined4 uVar5;
    complex cStack36;
    complex local_1c;
    complex local_14;
    complex result;
   
    uVar1 = fmul(param_1->real,param_2->real);
    uVar2 = fmul(param_1->imag,param_2->imag);
    uVar3 = fmul(param_1->real,param_2->imag);
    uVar4 = fmul(param_1->imag,param_2->real);
    uVar5 = fsub(uVar1,uVar2);
                    /* uVar5 - 1 */
    uVar1 = fadd(uVar5,0xbf800000);
                    /* invert? */
    local_14.real = (float)(uVar1 ^ 0x80000000);
    uVar1 = fadd(uVar3,uVar4);
    local_14.imag = (float)(uVar1 ^ 0x80000000);
    local_1c.real = (float)fsub(param_1->real,param_3->real);
    local_1c.imag = (float)fsub(param_1->imag,param_3->imag);
                    /* local_14 = ((param_1 * param_2) - 1) * -1
                       local_1c = param_1 - param_3 */
    cdiv(&local_1c,&local_14,&cStack36);
    cmul(&cStack36,param_4,&result);
    return result;
}

I don't really understand exactly what all the vector maths is doing right now, maybe someone else can chime in there. Though, there's probably enough information there to start experimenting with some different values or event implement the reverse of that despite not fully understanding it.
 
The following users thanked this post: gamalot

Offline RolandK

  • Regular Contributor
  • *
  • Posts: 109
  • Country: de
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #8 on: September 06, 2022, 09:58:37 am »
This may be the open short compensation values. See the https://www.keysight.com/us/en/assets/7018-06840/application-notes/5950-3000.pdf

In Figure 4.4 is the formula which looks a bit like the function "complex_unk1"
Why do old shaffner filters blow? - because there are rifas inside.
Why do rifas blow? Only time shows if the best new thing is really best. Here it is not.
 

Online gamalotTopic starter

  • Super Contributor
  • ***
  • Posts: 1386
  • Country: au
  • Correct my English
    • Youtube
Re: Agilent 4338B - Alternative 'calibration' method
« Reply #9 on: September 06, 2022, 10:09:41 am »
This may be the open short compensation values. See the https://www.keysight.com/us/en/assets/7018-06840/application-notes/5950-3000.pdf

In Figure 4.4 is the formula which looks a bit like the function "complex_unk1"

I know about that book - Impedance Measurement Handbook, it's famous here!  :-+

My guess is that those float pairs are compensation for complex impedance. These pairs are divided into 5 groups, corresponding to 5 different excitation currents; there are 3 pairs in each group, which may correspond to 3 measurement speeds, or 3 ranges.

I might try changing the EEPROM content tomorrow to verify my guess, and I'll do some preparations tonight.
« Last Edit: September 06, 2022, 10:11:59 am by gamalot »
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf