Agilent 4338B - Alternative 'calibration' method - Page 1
Forum section: electronics / general

author: gamalot
author_rank: Super Contributor
timestamp: March 31, 2022, 06:52:42 pm
content: 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:

author: TimFox
author_rank: Super Contributor
timestamp: March 31, 2022, 07:02:49 pm
content: I can understand why the non-repairability of secret software would drive one to re-invent the trimpot. Good work!

author: tv84
author_rank: Super Contributor
timestamp: September 02, 2022, 01:28:05 pm
content: So, with:TEST:MEM:ADDR xxxx:TEST:MEM:LONG?you can read the whole EEPROM space?What about?:TEST:MEM:ADDR xxxx:TEST:MEM:LONGCan you set values in the EEPROM with the :TEST:MEM:LONG command?

author: gamalot
author_rank: Super Contributor
timestamp: September 02, 2022, 02:48:59 pm
content: Quote from: tv84 on September 02, 2022, 01:28:05 pmYes, 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.

author: Miek
author_rank: Regular Contributor
timestamp: September 02, 2022, 09:40:22 pm
content: 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:$ od -Ax -j 0x80 --endian=big -f '4338B EEPROM DUMP.BIN'000080 -1.33072e-05 -7.6217e-06 1.6031385e-05 1.7778738e-06000090 1.014507 -0.02629138 1.0134906 -0.0260076620000a0 1.0134878 -0.026201833 1.0039018 -0.0256548150000b0 1.002896 -0.025374427 1.0028933 -0.0255665690000c0 1.0030564 -0.025575776 1.0020514 -0.0252956840000d0 1.0020487 -0.025487663 1.0039852 -0.0259262840000e0 1.0029793 -0.025645602 1.0029765 -0.025837760000f0 1.004992 -0.026207505 1.0039852 -0.025926284000100 1.0039824 -0.026118636 -nan -nan000110 -nan -nan -nan -nan*

author: gamalot
author_rank: Super Contributor
timestamp: September 03, 2022, 06:18:25 am
content: Quote from: Miek on September 02, 2022, 09:40:22 pmHi 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)

author: gamalot
author_rank: Super Contributor
timestamp: September 03, 2022, 12:08:51 pm
content: Quote from: Miek on September 02, 2022, 09:40:22 pmNow 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.

author: Miek
author_rank: Regular Contributor
timestamp: September 04, 2022, 01:01:19 pm
content: Quote from: gamalot on September 03, 2022, 12:08:51 pmAh, 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: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 ;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 constantsthe first value in the group is copied twice then remaining two values are copied, resulting in aset 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: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.real = cVar2.real;complex_ARRAY_00ffda48.imag = cVar2.imag;cVar2 = complex_unk3(&complex_00ffd988,&complex_00ffd990,cal_constants + iVar1 * 4 + i,&short_correction_value);complex_ARRAY_00ffda68.real = cVar2.real;complex_ARRAY_00ffda68.imag = cVar2.imag;i = i + 1;} while (i < 4);return;}Code: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: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:if (correction_enabled == 1) {iVar1 = (int)(short)iVar1 - (int)SHORT_ARRAY_0001ccc8;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: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) * -1local_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.

author: RolandK
author_rank: Regular Contributor
timestamp: September 06, 2022, 09:58:37 am
content: This may be the open short compensation values. See thehttps://www.keysight.com/us/en/assets/7018-06840/application-notes/5950-3000.pdfIn Figure 4.4 is the formula which looks a bit like the function "complex_unk1"

author: gamalot
author_rank: Super Contributor
timestamp: September 06, 2022, 10:09:41 am
content: Quote from: RolandK on September 06, 2022, 09:58:37 amI 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.