docs: readme update

This commit is contained in:
2025-12-08 16:10:54 +06:00
parent f4187f369d
commit 451d06619c
6 changed files with 1065 additions and 783 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.languageServer": "None"
}

125
README.md
View File

@@ -2,6 +2,8 @@
A utility for encrypting, decrypting, and parsing Rigol DG[89]00 Pro Arbitrary Waveform Generator `setup.stp`.
HF flatness calibration example in [hfflat_cal.py](./hfflat_cal.py), thanks to zrq from EEVBlog.
## Command-Line Options
```sh
@@ -27,109 +29,144 @@ options:
### Prerequisites
1. Install ADB (Android Debug Bridge) on your system:
1. Install [Python 3](https://www.python.org/downloads/)
2. Install ADB (Android Debug Bridge) on your system:
- **Linux**: `sudo apt install adb` (or equivalent for your distribution)
- **Windows**: Download and install [Android Platform Tools](https://developer.android.com/studio/releases/platform-tools)
- **Linux**: `sudo apt install adb` (or equivalent for your distribution)
- **Windows**: Download and install [Android Platform Tools](https://developer.android.com/studio/releases/platform-tools)
2. Connect your Rigol AFG to your local network and note its IP address
3. Connect your Rigol AFG to your local network and note its IP address
### Extracting the Setup File
1. Connect to your device via ADB:
```
adb connect IP:55555
```
```sh
$ adb connect IP:55555
```
Replace `IP` with your device's IP address
Replace `IP` with your device's IP address
2. Pull the setup file from the device:
```
adb pull /rigol/data/setup.stp
```
```sh
$ adb pull /rigol/data/setup.stp
```
3. Read your CPU serial:
```
adb shell -- /rigol/shell/get_cpu_serial_num.sh
```
```sh
$ adb shell -- /rigol/shell/get_cpu_serial_num.sh
```
4. **IMPORTANT**: Create a backup of the original file:
```
cp setup.stp setup.stp.backup
```
```sh
$ cp setup.stp setup.stp.backup
```
### Modifying the Setup File
For reference, here's what DG922 setup file could look like after correct modification:
```sh
$ ./rigol_setup_crypter.py -p -f ../databackup/data/setup.stp.decrypted
CSV header:
Manufacturer,InstrModel,InstrSN,CalibrationDate,SineMaxFreq,SquareMaxFreq,RampMaxFreq,PulseMaxFreq,ArbMaxFreq,HarmonicMaxFreq,MinFreq,HarmonicMinFreq,ARMSerial,MaxChannels,ArbWaveLenLicense,ArbWaveLenValidTime,DuoChanChannelValidTime
RIGOL TECHNOLOGIES,DG922 Pro,DG8P123456789,2023-11-11,200000000000000,60000000000000,5000000000000,50000000000000,50000000000000,100000000000000,1,1000,4b97ef24e77c4aeb933b,2,MEM,Forever,Forever
Instrument Information:
Manufacturer: RIGOL TECHNOLOGIES
Model: DG922 Pro
Serial Number: DG8P123456789
Calibration Date: 2023-11-11
ARM CPU Serial: 4b97ef24e77c4aeb933b
Max Channels: 2
Frequency Specifications:
Sine Max Frequency: 200000000000000 (200.000000 MHz)
Square Max Frequency: 60000000000000 (60.000000 MHz)
Ramp Max Frequency: 5000000000000 (5.000000 MHz)
Pulse Max Frequency: 50000000000000 (50.000000 MHz)
Arb Max Frequency: 50000000000000 (50.000000 MHz)
Harmonic Max Frequency: 100000000000000 (100.000000 MHz)
Min Frequency: 1 (1.000000e-12 MHz)
Harmonic Min Frequency: 1000 (1.000000e-09 MHz)
Licensing Information:
Arb Wavelength License: MEM (Memory Depth License)
Arb Wavelength Valid Time: Forever (Active (No Expiration))
Duo Channel Valid Time: Forever (Active (No Expiration))
```
1. Decrypt the setup file:
```
python rigol_setup_crypter.py -d -f setup.stp -k YOUR_CPU_SERIAL -o setup.csv
```
```sh
$ python rigol_setup_crypter.py -d -f setup.stp -k YOUR_CPU_SERIAL -o setup.csv
```
Replace `YOUR_CPU_SERIAL` with your device's CPU serial number
Replace `YOUR_CPU_SERIAL` with your device's CPU serial number
2. Parse the file to better understand its structure:
```
python rigol_setup_crypter.py -p -f setup.csv
```
```sh
$ python rigol_setup_crypter.py -p -f setup.csv
```
3. Edit the CSV file using a text editor of your choice
4. Verify your changes by parsing again:
```
python rigol_setup_crypter.py -p -f setup.csv
```
```sh
$ python rigol_setup_crypter.py -p -f setup.csv
```
5. Encrypt the modified file:
```
python rigol_setup_crypter.py -e -f setup.csv -k YOUR_CPU_SERIAL -o setup.stp.new
```
```sh
$ python rigol_setup_crypter.py -e -f setup.csv -k YOUR_CPU_SERIAL -o setup.stp.new
```
### Uploading the Modified File
1. Push the modified file back to the device:
```
adb push setup.stp.new /rigol/data/setup.stp
```
```sh
$ adb push setup.stp.new /rigol/data/setup.stp
```
2. Restart your device to apply the changes
```
adb shell -- reboot
```
```sh
$ adb shell -- reboot
```
## Examples
### Decrypt a setup file
```
python rigol_setup_crypter.py -d -f setup.stp -k d95ebe35672e20c2fc08 -o decrypted.csv
```sh
$ python rigol_setup_crypter.py -d -f setup.stp -k d95ebe35672e20c2fc08 -o decrypted.csv
```
### Decrypt a text string and output to console
```
python rigol_setup_crypter.py -d -t "FBC6BDDB0E..." -k d95ebe35672e20c2fc08
```sh
$ python rigol_setup_crypter.py -d -t "FBC6BDDB0E..." -k d95ebe35672e20c2fc08
```
### Parse a decrypted CSV
```
python rigol_setup_crypter.py -p -f decrypted.csv
```sh
$ python rigol_setup_crypter.py -p -f decrypted.csv
```
### Encrypt a modified CSV
```
python rigol_setup_crypter.py -e -f modified.csv -k d95ebe35672e20c2fc08 -o new_setup.stp
````sh
$ python rigol_setup_crypter.py -e -f modified.csv -k d95ebe35672e20c2fc08 -o new_setup.stp
```
## Warning
Keep a backup of the original `setup.csv`.
````

View File

@@ -21,7 +21,7 @@
"import matplotlib.pyplot as plt\n",
"import os\n",
"\n",
"GEN_CH = 2 # generator chan to cal\n",
"GEN_CH = 1 # generator chan to cal\n",
"OSC_CH = 1 # scope chan\n",
"\n",
"# VISA resource strs\n",
@@ -300,6 +300,77 @@
"output_hex = f\"cal_hfflat{GEN_CH}_mod.hex\"\n",
"generate_final_hex(input_hex, output_hex, meas_data)"
]
},
{
"cell_type": "markdown",
"id": "f32494b3",
"metadata": {},
"source": [
"Push the generated `cal_hfflat{gen_ch}_mod.hex` to the device.\n",
"\n",
"I.e. `adb push cal_hfflat1_mod.hex /rigol/data/cal_hfflat1.hex`, reboot, then do the verification sweep if needed."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a7069a1e",
"metadata": {},
"outputs": [],
"source": [
"def check_final_flatness(controller, gen_ch, osc_ch):\n",
" print(\"=\" * 32)\n",
" print(f\"Verification sweep: Channel {gen_ch}\")\n",
" print(\"=\" * 32)\n",
"\n",
" new_meas_data = run_calibration_sweeps(controller, gen_ch, osc_ch)\n",
" _fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(10, 14))\n",
"\n",
" plot_cfg = [\n",
" {\"key\": \"rng1\", \"title\": \"Range 1 (Mid: 1.26V)\", \"freq\": FREQ_FULL},\n",
" {\"key\": \"rng2\", \"title\": \"Range 2 (High: 5.0V)\", \"freq\": FREQ_HIGH_VOLT},\n",
" {\"key\": \"rng3\", \"title\": \"Range 3 (Low: 0.63V)\", \"freq\": FREQ_FULL},\n",
" ]\n",
"\n",
" for i, cfg in enumerate(plot_cfg):\n",
" key = cfg[\"key\"]\n",
" freqs_mhz = cfg[\"freq\"] / 1e6\n",
" vals = new_meas_data[key]\n",
"\n",
" # flatness ratio = V_meas / V_ref (at 200kHz)\n",
" ref_val = vals[0]\n",
" normalized = vals / ref_val\n",
"\n",
" # peak deviation\n",
" max_dev = np.max(np.abs(normalized - 1.0)) * 100\n",
"\n",
" ax = axes[i]\n",
" ax.plot(freqs_mhz, normalized, \"b.-\", linewidth=2, label=\"Measured Response\")\n",
"\n",
" ax.axhline(\n",
" 1.0, color=\"k\", linestyle=\"-\", alpha=0.8, linewidth=1, label=\"Target (1.0)\"\n",
" )\n",
" ax.axhline(\n",
" 1.01, color=\"g\", linestyle=\"--\", alpha=0.5, label=r\"$\\pm 1\\%$ Tolerance\"\n",
" )\n",
" ax.axhline(0.99, color=\"g\", linestyle=\"--\", alpha=0.5)\n",
"\n",
" ax.set_title(f\"{cfg['title']} - Max Deviation: {max_dev:.2f}%\")\n",
" ax.set_ylabel(\"Normalized Gain ($V_{out} / V_{ref}$)\")\n",
" ax.set_xlabel(\"Frequency (MHz)\")\n",
"\n",
" if max_dev < 5.0:\n",
" ax.set_ylim(0.95, 1.05)\n",
"\n",
" ax.legend(loc=\"upper right\")\n",
" ax.grid(True, which=\"both\", alpha=0.3)\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"check_final_flatness(instr, GEN_CH, OSC_CH)"
]
}
],
"metadata": {

849
dg800p_cal_zrq.ipynb Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,737 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Overview\n",
"This notebook calibrates the high-frequency output flatness of a signal generator. It works by:\n",
"1. Connecting to the signal generator and an oscilloscope via VISA (TCPIP).\n",
"2. Measuring the generator's actual RMS output voltage across specified frequency points for different amplitude ranges using the oscilloscope.\n",
"3. Reading the generator's existing high-frequency flatness calibration data (`cal_hfflat*.hex` files).\n",
"4. Calculating correction factors based on the difference between the measured response and the ideal flat response.\n",
"5. Applying these corrections to the existing calibration data, focusing on higher frequency points within each range.\n",
"6. Saving the updated calibration data into new `.hex` files.\n",
"7. Providing commands to upload the new calibration files to the generator using ADB (Android Debug Bridge).\n",
"\n",
"## Hardware Setup\n",
"* Connect the Signal Generator and Oscilloscope to the network.\n",
"* Connect the Signal Generator's output channel (CH1 or CH2) to the Oscilloscope's input channel (specified in `OSC_MEAS_CHAN`).\n",
"* Use a high-quality 50 Ohm coaxial cable.\n",
"* **Important:** Ensure the Oscilloscope's input channel impedance is set to **50 Ohms**.\n",
"* Enable ADB debugging on the Signal Generator (likely via network)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Configuration\n",
"Adjust the parameters below to match your setup."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import pyvisa\n",
"import time\n",
"import os\n",
"import pathlib # Optional, can use os.path instead\n",
"\n",
"# --- Instrument & Connection Settings ---\n",
"GEN_IP = \"192.168.138.178\" # IP Address of the Signal Generator\n",
"OSC_IP = \"192.168.138.244\" # IP Address of the Oscilloscope\n",
"ADB_DEVICE_ID = (\n",
" \"192.168.138.178:55555\" # ADB identifier for the generator (IP:Port or Serial)\n",
")\n",
"\n",
"# --- File Paths ---\n",
"# Directory where original .hex files are located and where measurement/modified files will be saved.\n",
"# Use raw string (r'...') or forward slashes ('/') for paths.\n",
"DATA_DIR = r\"C:\\Users\\xxx\\dgpro_calibration_data\" # CHANGE THIS PATH\n",
"\n",
"# Ensure the data directory exists\n",
"pathlib.Path(DATA_DIR).mkdir(\n",
" parents=True, exist_ok=True\n",
") # Create dir if it doesn't exist\n",
"\n",
"# Original and modified calibration filenames (don't change filename structure unless necessary)\n",
"ORIGINAL_HEX_FILES = {1: \"cal_hfflat1.hex\", 2: \"cal_hfflat2.hex\"}\n",
"MODIFIED_HEX_FILES = {1: \"cal_hfflat1_mod.hex\", 2: \"cal_hfflat2_mod.hex\"}\n",
"MEASUREMENT_FILE_TEMPLATE = (\n",
" \"ch{channel}-rng{range_idx}-measurement.npz\" # Template for saving measurements\n",
")\n",
"\n",
"# --- SCPI Commands ---\n",
"# Adjust these if your instruments use different commands\n",
"GEN_IDN_CMD = \"*IDN?\"\n",
"OSC_IDN_CMD = \"*IDN?\"\n",
"GEN_SET_VOLT_CMD = \":SOURce{ch}:VOLTage:AMPLitude {volt:.7e} VPP\" # Set Vpp\n",
"GEN_SET_FREQ_CMD = \":SOURce{ch}:FREQuency {freq:.4e}\" # Set Frequency in Hz\n",
"# *** IMPORTANT: Change this for your specific oscilloscope model! ***\n",
"# Example for many Siglent scopes: Query RMS voltage on Channel 1 -> 'C1:PAVA? RMS'\n",
"# Example from original code (might be Rigol): ':MEAS:ITEM? VRMS,CHAN1'\n",
"OSC_MEAS_CMD = \"C1:PAVA? RMS\" # Placeholder - REPLACE WITH YOUR SCOPE'S COMMAND\n",
"OSC_MEAS_CHAN = \"1\" # Oscilloscope channel connected to the generator output (used if needed in OSC_MEAS_CMD)\n",
"\n",
"# --- Calibration Settings ---\n",
"CHANNELS_TO_CALIBRATE = [\n",
" 1,\n",
" 2,\n",
"] # List of generator channels to calibrate (e.g., [1], [2], or [1, 2])\n",
"SETTLE_TIME_VOLT = 3.0 # Seconds to wait after setting voltage\n",
"SETTLE_TIME_FREQ = 0.7 # Seconds to wait after setting frequency\n",
"\n",
"# Define the calibration ranges, frequencies, and corresponding indices in the .hex file.\n",
"# Each range dictionary needs:\n",
"# 'voltage': Target Vpp for measurements in this range.\n",
"# 'frequencies_uHz': List/array of frequencies in microHertz (uHz).\n",
"# 'hex_start_idx': Starting index (1-based) in the hex file for this range's data.\n",
"# 'hex_end_idx': Ending index (inclusive) in the hex file for this range's data.\n",
"# 'points_to_modify': Number of *highest frequency points* within this range to apply correction to.\n",
"\n",
"# Frequencies are defined once and reused (converted from uHz to Hz later)\n",
"# Note: The original code reversed the lists, meaning measurements went from high to low freq. Preserving that.\n",
"# Also, the number of points varied slightly per range.\n",
"FREQ_LIST_RANGE_1_3_uHz = np.array(\n",
" list(\n",
" reversed(\n",
" [\n",
" 200000000000000,\n",
" 175000000000000,\n",
" 150000000000000,\n",
" 120000000000000,\n",
" 100000000000000,\n",
" 90000000000000,\n",
" 80000000000000,\n",
" 70000000000000,\n",
" 60000000000000,\n",
" 50000000000000,\n",
" 40000000000000,\n",
" 30000000000000,\n",
" 20000000000000,\n",
" 10000000000000,\n",
" 8000000000000,\n",
" 6000000000000,\n",
" 4000000000000,\n",
" 2000000000000,\n",
" 1000000000000,\n",
" 900000000000,\n",
" 800000000000,\n",
" 700000000000,\n",
" 600000000000,\n",
" 500000000000,\n",
" 400000000000,\n",
" 300000000000,\n",
" 200000000000,\n",
" ]\n",
" )\n",
" )\n",
") # 27 points: 200kHz to 200MHz\n",
"\n",
"FREQ_LIST_RANGE_2_uHz = np.array(\n",
" list(\n",
" reversed(\n",
" [\n",
" 100000000000000,\n",
" 90000000000000,\n",
" 80000000000000,\n",
" 70000000000000,\n",
" 60000000000000,\n",
" 50000000000000,\n",
" 40000000000000,\n",
" 30000000000000,\n",
" 20000000000000,\n",
" 10000000000000,\n",
" 8000000000000,\n",
" 6000000000000,\n",
" 4000000000000,\n",
" 2000000000000,\n",
" 1000000000000,\n",
" 900000000000,\n",
" 800000000000,\n",
" 700000000000,\n",
" 600000000000,\n",
" 500000000000,\n",
" 400000000000,\n",
" 300000000000,\n",
" 200000000000,\n",
" ]\n",
" )\n",
" )\n",
") # 23 points: 200kHz to 100MHz\n",
"\n",
"# Structure defining the ranges for calibration\n",
"# Indices match boundaries observed in original code (1-27, 28-50, 51-77)\n",
"# Note: Vpp values from original code comments/calculations are used.\n",
"CAL_RANGES = [\n",
" { # Range 1 (corresponds to original `rng1`)\n",
" \"voltage\": 1.2649111, # Vpp\n",
" \"frequencies_uHz\": FREQ_LIST_RANGE_1_3_uHz,\n",
" \"hex_start_idx\": 1,\n",
" \"hex_end_idx\": 27,\n",
" \"points_to_modify\": 12,\n",
" },\n",
" { # Range 2 (corresponds to original `rng2`)\n",
" \"voltage\": 5.0, # Vpp\n",
" \"frequencies_uHz\": FREQ_LIST_RANGE_2_uHz,\n",
" \"hex_start_idx\": 28,\n",
" \"hex_end_idx\": 50, # 23 points total\n",
" \"points_to_modify\": 12,\n",
" },\n",
" { # Range 3 (corresponds to original `rng3` - 0dBm into 50 Ohm)\n",
" \"voltage\": 0.6324556, # Vpp (approx 0 dBm)\n",
" \"frequencies_uHz\": FREQ_LIST_RANGE_1_3_uHz,\n",
" \"hex_start_idx\": 51,\n",
" \"hex_end_idx\": 77,\n",
" \"points_to_modify\": 12,\n",
" },\n",
"]\n",
"\n",
"# Hex file properties\n",
"HEX_DATA_TYPE = \"<f8\" # Little-endian 64-bit float\n",
"HEX_POINTS_PER_CHANNEL = 78 # Total points = 624 bytes / 8 bytes/point\n",
"\n",
"# --- Plotting Defaults ---\n",
"plt.rcParams[\"figure.figsize\"] = (10, 6) # Default figure size\n",
"\n",
"print(\"Configuration loaded.\")\n",
"print(f\"Data Directory: {DATA_DIR}\")\n",
"print(f\"Generator IP: {GEN_IP}, Oscilloscope IP: {OSC_IP}\")\n",
"print(f\"Calibrating Channels: {CHANNELS_TO_CALIBRATE}\")\n",
"print(f\"Oscilloscope Measurement Command: {OSC_MEAS_CMD}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Helper Functions"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def connect_instruments(gen_ip, osc_ip):\n",
" \"\"\"Connects to Generator and Oscilloscope using pyvisa.\"\"\"\n",
" gen = None\n",
" osc = None\n",
" try:\n",
" print(\"Connecting to instruments...\")\n",
" rm = pyvisa.ResourceManager()\n",
" gen_addr = f\"TCPIP0::{gen_ip}::INSTR\"\n",
" osc_addr = f\"TCPIP0::{osc_ip}::INSTR\"\n",
"\n",
" gen = rm.open_resource(gen_addr)\n",
" gen.timeout = 10000 # ms\n",
" gen.read_termination = \"\\n\"\n",
" gen.write_termination = \"\\n\"\n",
" print(f\"Generator Connected: {gen.query(GEN_IDN_CMD).strip()}\")\n",
"\n",
" osc = rm.open_resource(osc_addr)\n",
" osc.timeout = 10000 # ms\n",
" osc.read_termination = \"\\n\"\n",
" osc.write_termination = \"\\n\"\n",
" print(f\"Oscilloscope Connected: {osc.query(OSC_IDN_CMD).strip()}\")\n",
" print(\"-\" * 30)\n",
" return gen, osc\n",
" except pyvisa.errors.VisaIOError as e:\n",
" print(f\"VISA Error connecting to instruments: {e}\")\n",
" if gen:\n",
" gen.close()\n",
" if osc:\n",
" osc.close()\n",
" return None, None\n",
" except Exception as e:\n",
" print(f\"An unexpected error occurred during connection: {e}\")\n",
" if gen:\n",
" gen.close()\n",
" if osc:\n",
" osc.close()\n",
" return None, None\n",
"\n",
"\n",
"def measure_flatness(gen, osc, channel, voltage_vpp, frequencies_hz, osc_meas_cmd):\n",
" \"\"\"Sets generator and measures RMS voltage using the oscilloscope.\"\"\"\n",
" measured_amps_rms = np.zeros_like(frequencies_hz, dtype=float)\n",
"\n",
" print(f\"Starting measurement: CH{channel}, Voltage={voltage_vpp:.4f} Vpp\")\n",
"\n",
" # Set initial voltage\n",
" cmd = GEN_SET_VOLT_CMD.format(ch=channel, volt=voltage_vpp)\n",
" # print(f\"Sending to Gen: {cmd}\") # Uncomment for debugging\n",
" gen.write(cmd)\n",
" time.sleep(SETTLE_TIME_VOLT)\n",
"\n",
" # Set initial frequency (first in the list)\n",
" cmd = GEN_SET_FREQ_CMD.format(ch=channel, freq=frequencies_hz[0])\n",
" # print(f\"Sending to Gen: {cmd}\") # Uncomment for debugging\n",
" gen.write(cmd)\n",
" time.sleep(SETTLE_TIME_FREQ) # Allow settling before first measurement\n",
"\n",
" # Loop through frequencies\n",
" for i, freq_hz in enumerate(frequencies_hz):\n",
" cmd = GEN_SET_FREQ_CMD.format(ch=channel, freq=freq_hz)\n",
" # print(f\"Sending to Gen: {cmd}\") # Uncomment for debugging\n",
" gen.write(cmd)\n",
" time.sleep(SETTLE_TIME_FREQ)\n",
"\n",
" # Measure RMS Voltage\n",
" try:\n",
" # Adapt query based on expected Siglent format (value first, then unit maybe)\n",
" # Or based on original format (just the value)\n",
" query_cmd = osc_meas_cmd.replace(\n",
" \"C1\", f\"C{OSC_MEAS_CHAN}\"\n",
" ) # Replace channel if needed\n",
" # print(f\"Querying Scope: {query_cmd}\") # Uncomment for debugging\n",
" response = osc.query(query_cmd).strip()\n",
"\n",
" # Attempt to extract float - adjust parsing if needed based on scope response\n",
" # e.g., if it returns \"1.234 VRMS\", split and take first part\n",
" try:\n",
" measured_vrms = float(response.split()[0]) # Example parsing\n",
" except ValueError:\n",
" measured_vrms = float(\n",
" response\n",
" ) # Assume direct float response if split fails\n",
"\n",
" measured_amps_rms[i] = measured_vrms\n",
" print(\n",
" f\" Freq: {freq_hz / 1e6:.3f} MHz, Measured VRMS: {measured_vrms:.6f} V\"\n",
" )\n",
" except pyvisa.errors.VisaIOError as e:\n",
" print(f\"VISA Error during measurement at {freq_hz / 1e6:.3f} MHz: {e}\")\n",
" measured_amps_rms[i] = np.nan # Mark as invalid\n",
" except Exception as e:\n",
" print(\n",
" f\"Unexpected error during measurement at {freq_hz / 1e6:.3f} MHz: {e}\"\n",
" )\n",
" measured_amps_rms[i] = np.nan # Mark as invalid\n",
"\n",
" print(f\"Measurement finished for CH{channel}, Voltage={voltage_vpp:.4f} Vpp.\")\n",
" print(\"-\" * 30)\n",
" return frequencies_hz, measured_amps_rms\n",
"\n",
"\n",
"def calculate_and_update_cal(\n",
" original_hex_path, all_measurements, cal_ranges, hex_data_type, output_hex_path\n",
"):\n",
" \"\"\"Calculates new calibration factors and writes the modified .hex file.\"\"\"\n",
" print(f\"Processing calibration for: {output_hex_path}\")\n",
"\n",
" # 1. Read original calibration data\n",
" try:\n",
" with open(original_hex_path, \"rb\") as fh:\n",
" original_cal_data = np.fromfile(fh, dtype=hex_data_type)\n",
" print(f\"Read {len(original_cal_data)} points from {original_hex_path}\")\n",
" if len(original_cal_data) != HEX_POINTS_PER_CHANNEL:\n",
" print(\n",
" f\"Warning: Expected {HEX_POINTS_PER_CHANNEL} points, found {len(original_cal_data)}.\"\n",
" )\n",
" # Decide how to handle: error out, proceed with caution?\n",
" # For now, proceed cautiously. Add error handling if needed.\n",
" # return False\n",
" except FileNotFoundError:\n",
" print(f\"Error: Original calibration file not found: {original_hex_path}\")\n",
" return False\n",
" except Exception as e:\n",
" print(f\"Error reading {original_hex_path}: {e}\")\n",
" return False\n",
"\n",
" modified_cal_data = original_cal_data.copy()\n",
" plt.figure() # Create a figure for plotting results for this channel\n",
"\n",
" # 2. Process each calibration range\n",
" for i, cal_range in enumerate(cal_ranges):\n",
" range_idx = i + 1 # 1-based index for user feedback/filenames\n",
" print(f\" Applying corrections for Range {range_idx}...\")\n",
"\n",
" # Find the measurement data for this range\n",
" measurement_data = all_measurements.get(range_idx)\n",
" if measurement_data is None:\n",
" print(f\" Error: Measurement data for range {range_idx} not found.\")\n",
" continue # Skip this range\n",
"\n",
" measured_freqs = measurement_data[\"frequencies_hz\"]\n",
" measured_amps = measurement_data[\"measured_amps_rms\"]\n",
"\n",
" # Basic check for NaNs or zeros which would break calculation\n",
" if np.isnan(measured_amps).any() or measured_amps[0] == 0:\n",
" print(\n",
" f\" Warning: Invalid measurement data (NaN or zero at start) for range {range_idx}. Skipping correction.\"\n",
" )\n",
" continue\n",
"\n",
" # Get section boundaries (using 0-based Python indexing)\n",
" start_idx = cal_range[\"hex_start_idx\"] # Config uses 1-based for clarity\n",
" end_idx = cal_range[\"hex_end_idx\"] + 1 # Python slice excludes end\n",
" num_points_in_range = end_idx - start_idx\n",
" points_to_modify = cal_range[\"points_to_modify\"]\n",
"\n",
" if len(measured_amps) != num_points_in_range:\n",
" print(\n",
" f\" Warning: Mismatch between measurement points ({len(measured_amps)}) and expected hex range points ({num_points_in_range}) for range {range_idx}. Skipping.\"\n",
" )\n",
" continue\n",
"\n",
" # Extract the relevant section from the *original* calibration data\n",
" original_section = original_cal_data[start_idx:end_idx]\n",
"\n",
" # --- Calculate Correction Factors ---\n",
" # Normalize measured response relative to the first point in the range\n",
" relative_response = measured_amps / measured_amps[0]\n",
"\n",
" # Avoid division by zero if relative_response has zeros (shouldn't happen if checked above)\n",
" relative_response[relative_response == 0] = (\n",
" 1e-9 # Replace zeros with small number\n",
" )\n",
"\n",
" # New calibration factor = Original Factor / Measured Relative Response\n",
" # If measured output dropped (relative_response < 1), new factor > original factor.\n",
" # If measured output peaked (relative_response > 1), new factor < original factor.\n",
" new_cal_section = original_section / relative_response\n",
" # --- Apply Correction to the specified points ---\n",
" # Determine indices *within the section* to modify (last 'points_to_modify')\n",
" modify_start_in_section = num_points_in_range - points_to_modify\n",
" modify_end_in_section = num_points_in_range\n",
"\n",
" # Determine indices *within the full modified_cal_data array*\n",
" modify_start_global = start_idx + modify_start_in_section\n",
" modify_end_global = start_idx + modify_end_in_section\n",
"\n",
" print(\n",
" f\" Modifying indices {modify_start_global} to {modify_end_global - 1} (total {points_to_modify} points)\"\n",
" )\n",
"\n",
" # Update the modified data array only for the selected points\n",
" modified_cal_data[modify_start_global:modify_end_global] = new_cal_section[\n",
" modify_start_in_section:modify_end_in_section\n",
" ]\n",
"\n",
" # --- Plotting for verification ---\n",
" plot_indices = np.arange(start_idx, end_idx)\n",
" plt.plot(\n",
" plot_indices,\n",
" original_section,\n",
" \"--\",\n",
" label=f\"Orig Range {range_idx} (Idx {start_idx}-{end_idx - 1})\",\n",
" )\n",
" plt.plot(\n",
" plot_indices, new_cal_section, \"-\", label=f\"New Calc Range {range_idx}\"\n",
" )\n",
" # Highlight modified points\n",
" plt.scatter(\n",
" plot_indices[modify_start_in_section:modify_end_in_section],\n",
" new_cal_section[modify_start_in_section:modify_end_in_section],\n",
" color=\"red\",\n",
" s=20,\n",
" zorder=5,\n",
" label=f\"Modified Pts Rng {range_idx}\",\n",
" )\n",
"\n",
" # Add overall plot elements\n",
" plt.title(f\"Calibration Factors - {os.path.basename(output_hex_path)}\")\n",
" plt.xlabel(\"Index in .hex file\")\n",
" plt.ylabel(\"Calibration Factor\")\n",
" plt.legend(fontsize=\"small\")\n",
" plt.grid(True)\n",
" plt.tight_layout()\n",
" plot_filename = os.path.splitext(output_hex_path)[0] + \"_comparison.png\"\n",
" plt.savefig(plot_filename)\n",
" print(f\"Saved comparison plot to {plot_filename}\")\n",
" plt.show()\n",
"\n",
" # 3. Write the modified calibration data\n",
" try:\n",
" with open(output_hex_path, \"wb\") as fh:\n",
" modified_cal_data.tofile(fh)\n",
" print(\n",
" f\"Successfully wrote {len(modified_cal_data)} points to {output_hex_path}\"\n",
" )\n",
" return True\n",
" except Exception as e:\n",
" print(f\"Error writing modified file {output_hex_path}: {e}\")\n",
" return False"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Connect to Instruments"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"gen, osc = connect_instruments(GEN_IP, OSC_IP)\n",
"\n",
"# Proceed only if connection is successful\n",
"if gen is None or osc is None:\n",
" print(\n",
" \"\\nERROR: Instrument connection failed. Please check IPs, connections, and VISA setup.\"\n",
" )\n",
" # Optionally raise an error: raise ConnectionError(\"Failed to connect to instruments\")\n",
"else:\n",
" print(\"\\nInstruments connected successfully.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Perform Measurements\n",
"This section iterates through the specified channels and calibration ranges, performing measurements for each."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"all_channel_measurements = {} # Dictionary to store results {channel: {range_idx: data}}\n",
"\n",
"if gen and osc: # Only run if instruments are connected\n",
" for channel in CHANNELS_TO_CALIBRATE:\n",
" print(f\"\\n--- Starting Measurements for Channel {channel} ---\")\n",
" channel_measurements = {} # Store results for this channel {range_idx: data}\n",
"\n",
" for i, cal_range in enumerate(CAL_RANGES):\n",
" range_idx = i + 1 # 1-based index\n",
" voltage_vpp = cal_range[\"voltage\"]\n",
" # Convert frequencies from uHz to Hz for measurement\n",
" frequencies_hz = cal_range[\"frequencies_uHz\"] / 1e6\n",
"\n",
" # Perform the measurement\n",
" measured_freqs_hz, measured_amps_rms = measure_flatness(\n",
" gen, osc, channel, voltage_vpp, frequencies_hz, OSC_MEAS_CMD\n",
" )\n",
"\n",
" # Store results\n",
" measurement_data = {\n",
" \"frequencies_hz\": measured_freqs_hz,\n",
" \"measured_amps_rms\": measured_amps_rms,\n",
" \"voltage_vpp\": voltage_vpp,\n",
" \"channel\": channel,\n",
" \"range_idx\": range_idx,\n",
" }\n",
" channel_measurements[range_idx] = measurement_data\n",
"\n",
" # Save intermediate results for this range\n",
" filename = MEASUREMENT_FILE_TEMPLATE.format(\n",
" channel=channel, range_idx=range_idx\n",
" )\n",
" filepath = os.path.join(DATA_DIR, filename)\n",
" try:\n",
" np.savez(filepath, **measurement_data)\n",
" print(f\"Saved measurement data to: {filepath}\")\n",
" except Exception as e:\n",
" print(f\"Error saving measurement data to {filepath}: {e}\")\n",
"\n",
" print(\"-\" * 20) # Separator between ranges\n",
"\n",
" all_channel_measurements[channel] = channel_measurements\n",
" print(f\"--- Finished Measurements for Channel {channel} ---\")\n",
"\n",
"else:\n",
" print(\"\\nSkipping measurements due to connection failure.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. Calculate New Calibration and Generate .hex Files\n",
"This section processes the measurement data saved in step 4 and generates the modified `.hex` files."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if all_channel_measurements: # Proceed only if measurements were taken\n",
" for channel in CHANNELS_TO_CALIBRATE:\n",
" print(f\"\\n--- Calculating Calibration for Channel {channel} ---\")\n",
"\n",
" original_hex_path = os.path.join(DATA_DIR, ORIGINAL_HEX_FILES[channel])\n",
" output_hex_path = os.path.join(DATA_DIR, MODIFIED_HEX_FILES[channel])\n",
" measurements_for_channel = all_channel_measurements.get(channel)\n",
"\n",
" if not measurements_for_channel:\n",
" print(\n",
" f\"Error: No measurement data found for Channel {channel}. Skipping calculation.\"\n",
" )\n",
" continue\n",
"\n",
" success = calculate_and_update_cal(\n",
" original_hex_path,\n",
" measurements_for_channel,\n",
" CAL_RANGES,\n",
" HEX_DATA_TYPE,\n",
" output_hex_path,\n",
" )\n",
"\n",
" if success:\n",
" print(\n",
" f\"Successfully generated modified calibration file for Channel {channel}.\"\n",
" )\n",
" else:\n",
" print(\n",
" f\"Failed to generate modified calibration file for Channel {channel}.\"\n",
" )\n",
"\n",
"else:\n",
" print(\n",
" \"\\nSkipping calibration calculation because no measurements were performed or loaded.\"\n",
" )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. Visualize Original vs. Modified Calibration Data (Optional)\n",
"Plot the original and newly generated `_mod.hex` files together for comparison."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"\\n--- Plotting Final Comparison ---\")\n",
"plt.figure(figsize=(12, 7))\n",
"\n",
"file_labels = []\n",
"\n",
"# Plot original files\n",
"for ch in CHANNELS_TO_CALIBRATE:\n",
" fname = os.path.join(DATA_DIR, ORIGINAL_HEX_FILES[ch])\n",
" try:\n",
" with open(fname, \"rb\") as fh:\n",
" data = np.fromfile(fh, dtype=HEX_DATA_TYPE)\n",
" plt.plot(data, \"--\", label=f\"Original CH{ch}\")\n",
" file_labels.append(f\"Orig CH{ch}\")\n",
" except FileNotFoundError:\n",
" print(f\"Warning: Original file not found for plotting: {fname}\")\n",
" except Exception as e:\n",
" print(f\"Error reading {fname} for plotting: {e}\")\n",
"\n",
"# Plot modified files\n",
"for ch in CHANNELS_TO_CALIBRATE:\n",
" fname = os.path.join(DATA_DIR, MODIFIED_HEX_FILES[ch])\n",
" try:\n",
" with open(fname, \"rb\") as fh:\n",
" data = np.fromfile(fh, dtype=HEX_DATA_TYPE)\n",
" plt.plot(data, \"-\", linewidth=2, label=f\"Modified CH{ch}\")\n",
" file_labels.append(f\"Mod CH{ch}\")\n",
" except FileNotFoundError:\n",
" print(f\"Warning: Modified file not found for plotting: {fname}\")\n",
" except Exception as e:\n",
" print(f\"Error reading {fname} for plotting: {e}\")\n",
"\n",
"# Add vertical lines indicating range boundaries (using 0-based index)\n",
"# Boundaries are *after* the end index of a range\n",
"boundaries = sorted(list(set([r[\"hex_end_idx\"] for r in CAL_RANGES])))\n",
"if boundaries:\n",
" plt.vlines(\n",
" boundaries[:-1],\n",
" ymin=plt.ylim()[0],\n",
" ymax=plt.ylim()[1],\n",
" colors=\"r\",\n",
" linestyles=\":\",\n",
" label=\"Range Boundary\",\n",
" )\n",
" # file_labels.append('Boundary') # Optional: add to legend\n",
"\n",
"plt.title(\"Comparison of Original and Modified HF Flatness Calibration Data\")\n",
"plt.xlabel(\"Index in .hex file\")\n",
"plt.ylabel(\"Calibration Factor\")\n",
"plt.legend()\n",
"plt.grid(True)\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. Upload Calibration Files to Generator using ADB"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"\\n--- ADB Upload Commands ---\")\n",
"print(\"Run these commands in your system terminal (cmd, bash, etc.):\")\n",
"print(\"# Connect to the device (if not already connected):\")\n",
"print(f\"adb connect {ADB_DEVICE_ID}\")\n",
"print(\"-\" * 20)\n",
"\n",
"for ch in CHANNELS_TO_CALIBRATE:\n",
" mod_file_local = os.path.join(DATA_DIR, MODIFIED_HEX_FILES[ch])\n",
" original_filename_remote = ORIGINAL_HEX_FILES[\n",
" ch\n",
" ] # Overwrite the original name on device\n",
" # Assuming the standard Rigol path, adjust if necessary\n",
" remote_path = f\"/rigol/data/{original_filename_remote}\"\n",
"\n",
" # Important: Ensure paths are correctly quoted if they contain spaces\n",
" # Using pathlib helps create OS-agnostic paths for the local file\n",
" mod_file_local_str = str(pathlib.Path(mod_file_local))\n",
"\n",
" print(f\"# Upload Channel {ch}:\")\n",
" # Use quotes around paths, especially the local one if it might have spaces\n",
" print(f'adb -s {ADB_DEVICE_ID} push \"{mod_file_local_str}\" \"{remote_path}\"')\n",
" print(\"\") # Add a newline for clarity\n",
"\n",
"print(\"--- End of ADB Commands ---\")\n",
"print(\n",
" \"\\nAfter uploading, it is recommended to REBOOT the signal generator for the new calibration data to take effect.\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if gen:\n",
" gen.close()\n",
" print(\"Generator connection closed.\")\n",
"if osc:\n",
" osc.close()\n",
" print(\"Oscilloscope connection closed.\")\n",
"\n",
"print(\"\\nCalibration process finished.\")"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -8,7 +8,7 @@ import numpy as np
import matplotlib.pyplot as plt
import os
GEN_CH = 2 # generator chan to cal
GEN_CH = 1 # generator chan to cal
OSC_CH = 1 # scope chan
# VISA resource strs
@@ -263,4 +263,63 @@ input_hex = f"cal_hfflat{GEN_CH}.hex"
output_hex = f"cal_hfflat{GEN_CH}_mod.hex"
generate_final_hex(input_hex, output_hex, meas_data)
# %% [markdown]
# Push the generated `cal_hfflat{gen_ch}_mod.hex` to the device.
#
# I.e. `adb push cal_hfflat1_mod.hex /rigol/data/cal_hfflat1.hex`, reboot, then do the verification sweep if needed.
# %%
def check_final_flatness(controller, gen_ch, osc_ch):
print("=" * 32)
print(f"Verification sweep: Channel {gen_ch}")
print("=" * 32)
new_meas_data = run_calibration_sweeps(controller, gen_ch, osc_ch)
_fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(10, 14))
plot_cfg = [
{"key": "rng1", "title": "Range 1 (Mid: 1.26V)", "freq": FREQ_FULL},
{"key": "rng2", "title": "Range 2 (High: 5.0V)", "freq": FREQ_HIGH_VOLT},
{"key": "rng3", "title": "Range 3 (Low: 0.63V)", "freq": FREQ_FULL},
]
for i, cfg in enumerate(plot_cfg):
key = cfg["key"]
freqs_mhz = cfg["freq"] / 1e6
vals = new_meas_data[key]
# flatness ratio = V_meas / V_ref (at 200kHz)
ref_val = vals[0]
normalized = vals / ref_val
# peak deviation
max_dev = np.max(np.abs(normalized - 1.0)) * 100
ax = axes[i]
ax.plot(freqs_mhz, normalized, "b.-", linewidth=2, label="Measured Response")
ax.axhline(
1.0, color="k", linestyle="-", alpha=0.8, linewidth=1, label="Target (1.0)"
)
ax.axhline(
1.01, color="g", linestyle="--", alpha=0.5, label=r"$\pm 1\%$ Tolerance"
)
ax.axhline(0.99, color="g", linestyle="--", alpha=0.5)
ax.set_title(f"{cfg['title']} - Max Deviation: {max_dev:.2f}%")
ax.set_ylabel("Normalized Gain ($V_{out} / V_{ref}$)")
ax.set_xlabel("Frequency (MHz)")
if max_dev < 5.0:
ax.set_ylim(0.95, 1.05)
ax.legend(loc="upper right")
ax.grid(True, which="both", alpha=0.3)
plt.tight_layout()
plt.show()
check_final_flatness(instr, GEN_CH, OSC_CH)