Skip to main content

Koolertron / MHinstek MHS-5200A series dual-channel DDS signal generator + frequency counter driver (covers KKmoon and other rebrands)

Project description

rf-bench-drivers-koolertron

Status: Tested 2026-06-08 against a real MHS-5225A (raw model code 5225A5040000, CH340 USB-serial) and confirmed working: frequency, amplitude, waveform, duty-cycle, offset, phase, attenuator, and per-channel set/get; master output enable; built-in frequency counter (loopback test confirms ±7 ppm against the unit's commanded frequency); period-mode counter; snapshot of full state. Sweep and arb-upload commands are implemented from documentation but not yet exercised against hardware.

Python driver for the Koolertron / MHinstek MHS-5200A series dual-channel DDS arbitrary-waveform signal generator with built-in frequency counter and sweep generator. Sold under many brand names — Koolertron, MHinstek, KKmoon, and various AliExpress / eBay listings labelled "200MSa/s 12Bit DDS". The hardware and USB protocol are common to every variant; only the upper sine-wave frequency limit changes per model suffix:

Model Sine FS
MHS-5206A 6 MHz
MHS-5212A 12 MHz
MHS-5220A 20 MHz
MHS-5225A 25 MHz

This driver was written from scratch using the public protocol document listed in Protocol reference and credits below. No source code from any other implementation was copied or modified; the wire protocol was verified live against an MHS-5225A on 2026-06-08.

Installation

pip install rf-bench-drivers-koolertron

The only runtime dependency is pyserial.

Quick start

from rf_bench.koolertron import (
    MHS5200A, Waveform, CounterMode, Gate, Atten,
)

with MHS5200A() as gen:                         # auto-detect CH340 / PL2303
    print(gen.identify())                       # 'MHS-5225A (5040000)'

    # Function generator
    gen.set_frequency(1, 1_000_000)             # CH1: 1 MHz
    gen.set_amplitude(1, 1.0)                   # CH1: 1.0 Vpp into 50 Ω
    gen.set_waveform(1, Waveform.SINE)
    gen.set_frequency(2, 100_000)               # CH2: 100 kHz
    gen.set_waveform(2, Waveform.SQUARE)
    gen.set_duty_cycle(2, 25.0)                 # CH2: 25% duty
    gen.output_on()                             # master enable (BOTH ch)

    # Built-in frequency counter (Ext.IN connector on front)
    hz = gen.measure_frequency_hz(gate=Gate.S10)
    print(f"counter sees {hz:.1f} Hz")

    gen.output_off()

Optional calibration

The driver supports optional per-channel amplitude correction and frequency offset correction. With no cal file present, the driver works using the unit's built-in (factory) calibration.

To enable corrections, run the calibration tool at projects/signal-sources/koolertron-cal/ to characterise your specific unit. Results are saved to ~/.koolertron_mhs5200_cal.json and picked up automatically by the driver.

with MHS5200A() as gen:
    print(gen.calibration_info())
    # -> {'loaded': True, 'frequency_ppm_offset': 11.77, ...}

    # Frequency is pre-corrected for the unit's TCXO offset
    gen.set_frequency(1, 1_000_000)             # actually ≈ 999_988 Hz
                                                 # at the wire so output is 1 MHz

    # set_amplitude_dbm is amp-corrected per channel and per frequency
    gen.set_amplitude_dbm(1, 25_000_000, 0.0)   # delivers 0 dBm at 25 MHz
                                                 # (compensates ~4.7 dB rolloff)

To bypass an existing cal file:

gen = MHS5200A(calibration=False)

Hardware

Item Value
Instrument Koolertron / MHinstek MHS-5206A / 5212A / 5220A / 5225A (and rebrands: KKmoon, et al.)
USB chip QinHeng CH340 (1a86:7523) on older firmware, PL2303 (067b:2303) on newer
Serial 57600 8N1, no flow control, CR LF terminators
Per-channel parameters frequency, amplitude, waveform, duty cycle, offset, phase, output attenuator
Master output Global enable (both channels together — hardware quirk)
Counter EXT IN connector on rear; modes: frequency, count, period, +/− pulse width, duty cycle
Sweep Linear or logarithmic, configurable start/stop/time
Memory 10 setup slots (0..9; slot 0 is the power-on default)

API summary

Identification and configuration

Method / attribute Description
MHS5200A(port=None, calibration=None) Connect; auto-detects CH340 / PL2303 if port omitted. Calibration: None = auto-load ~/.koolertron_mhs5200_cal.json if present; False = ignore any cal; str = path to specific JSON; dict = use as-is.
gen.model Friendly model string, e.g. "MHS-5225A"
gen.raw_model Full raw :r0c payload, e.g. "5225A5040000"
gen.identify() Combined string, e.g. "MHS-5225A (5040000)"
gen.calibration_info() Dict describing the active calibration, or {loaded: False}
gen.port Active serial port path
MHS5200A.find_port() Class method returning the first compatible USB-serial port found, or None

Per-channel waveform parameters

Method Description / units
set_frequency(ch, hz) / get_frequency(ch) Hz; pre-corrected by frequency_ppm_offset if cal loaded. Wire register: 0.01 Hz steps.
set_amplitude(ch, vpp) / get_amplitude(ch) Vpp into 50 Ω (matches scope reading at 50 Ω; front panel shows 2× this open-circuit value). Wire register: 5 mV steps. NOT cal-corrected.
set_amplitude_dbm(ch, freq, dbm) Set amplitude as target dBm into 50 Ω at the given frequency. Frequency-aware; uses calibration if loaded.
set_waveform(ch, w) / get_waveform(ch) Waveform enum (SINE/SQUARE/TRIANGLE/UP_SAW/DOWN_SAW/ARB0..ARB15)
set_duty_cycle(ch, %) / get_duty_cycle(ch) Percent (wire: 0.1 % steps)
set_offset(ch, signed) / get_offset(ch) Signed -120..+120 (wire: 0..240 with 120 = no offset)
set_phase(ch, deg) / get_phase(ch) Degrees, 0..359
set_attenuator(ch, a) / get_attenuator(ch) Atten.MINUS_20DB (-20 dB pad) or Atten.ZERO_DB
set_channel_enable(ch, on) / get_channel_enable(ch) Per-channel enable (use master output_on/off for normal use)

Master output (global to both channels)

Method Description
output_on() Enable output (both channels — global)
output_off() Disable output

Frequency counter (Ext.IN connector on front)

Method Description
counter_setup(mode, gate, source_ttl=False) Configure mode + gate window + source
counter_start() / counter_stop() / counter_reset() Run / stop / zero
read_counter() Raw counter value (units depend on mode)
read_counter_hz() FREQ-mode reading scaled to Hz (firmware 5040000)
measure_frequency_hz(gate, ...) One-shot helper: setup + start + poll-until-stable + read + stop. Default gate is Gate.S10 (10 s), reliable from 10 kHz upward.

CounterMode enum: FREQ, COUNT, PULSE_HIGH, PULSE_LOW, PERIOD, DUTY. Gate enum: S1 (1 s), S10 (10 s, default for measure_frequency_hz), S0_1 (100 ms), S0_01 (10 ms).

Counter input range (firmware 5040000):

Gate Reliable input range
Gate.S10 ≥ 10 kHz
Gate.S1 ≥ 10 MHz
Gate.S0_1 / Gate.S0_01 High-frequency only; not characterised

Below the listed minimum, the counter often fails to lock; the driver's measure_frequency_hz returns the last value read, but it may be inaccurate. Caller should sanity-check against the expected input.

Sweep generator (CH1)

Method Description
sweep_setup(start_hz, stop_hz, time_s, log=False) Configure
sweep_start() / sweep_stop() / get_sweep_state() Run / stop / query

Memory slots

Method Description
save_slot(slot=0) Save current full setup to slot 0..9
load_slot(slot=0) Load setup; slot 0 is the power-on default

Power amplifier (units that have it)

Method Description
power_amp(on) / get_power_amp() Enable/disable optional power amp

Other

Method Description
snapshot() Return a dict with the model + per-channel parameters of both channels
close() Close the serial port (automatic via context manager)

Live-test results (2026-06-08)

Confirmed against real MHS-5225A, firmware/hardware code 5040000, USB adapter CH340 (1a86:7523) on /dev/ttyUSB0. CH1 output patched to the rear EXT IN connector to enable closed-loop counter testing.

identify : MHS-5225A (5040000)

TEST 1: CH1 frequency set/get round-trip — 1 Hz to 25 MHz, all OK
TEST 2: CH1 amplitude — 0.5 / 1 / 2.5 / 5 V, all OK
TEST 3: CH1 waveform — SINE/SQUARE/TRIANGLE/UP_SAW/DOWN_SAW, all OK
TEST 4: duty 25 %, offset +30, phase 90°, all OK
TEST 5: CH1=1 MHz / CH2=100 kHz independent — OK
TEST 6: master output on / off — OK (no exception)
TEST 7: frequency counter loopback (CH1 -> EXT IN at 1.234567 MHz):
        measured 1234576.1 Hz   (+7.4 ppm error vs commanded)
TEST 8: counter PERIOD mode at 1.234 MHz: 811 ns (matches 1/1.234 MHz ≈ 810 ns)
TEST 9: snapshot() returns full state for both channels
        0 failures

Protocol reference and credits

This driver's wire protocol is implemented directly from the public reverse- engineering work of the amateur-radio and electronics community. All credit for protocol discovery, reverse-engineering, and the documentation that made this driver possible belongs to those authors. Their work is gratefully acknowledged below; this driver simply exposes their findings through a clean Python API.

The single most useful resource was:

wd5gnr (Al Williams)"Serial Protocol for MHS-5200A", document version 2, dated 9 August 2015. The complete protocol document, including the frequency counter and sweep commands, is included in the MHS5200AProtocol.pdf file of his open-source reference implementation.

Repository: https://github.com/wd5gnr/mhs5200a Document: MHS5200AProtocol.pdf in that repository.

This protocol document is the canonical public reference; without it, only the basic 8 set/get commands from the standard serial probing would be known. The frequency counter, sweep, arbitrary-waveform upload, and memory- slot commands all come from his reverse-engineering work.

Other community sources that helped confirm details:

If you are the author of one of the sources above and feel this credit should be expanded or reworded, please open an issue on the rf-bench repository.

Hardware verified

  • Make/model: Sold to the author as a KKmoon-branded "200MSa/s 12Bit" dual-channel DDS function generator with frequency counter (no model number on the front panel).
  • Identifies as: MHS-5225A, raw model code 5225A5040000.
  • Channel 1 upper sine frequency: 25 MHz (set/get round-trip OK at 25 MHz).
  • USB chip: QinHeng CH340 (1a86:7523).
  • Confirmed working: 2026-06-08, all tests passing against /dev/ttyUSB0.

License

GPL-3.0-or-later.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rf_bench_drivers_koolertron-0.1.0.tar.gz (35.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rf_bench_drivers_koolertron-0.1.0-py3-none-any.whl (32.0 kB view details)

Uploaded Python 3

File details

Details for the file rf_bench_drivers_koolertron-0.1.0.tar.gz.

File metadata

File hashes

Hashes for rf_bench_drivers_koolertron-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0918ec4ce9775f143327631dab580104f9f48a1f7c3de4ba1150da1c02388282
MD5 3c079ee90b95a9c46897dbd770d1e877
BLAKE2b-256 67320efb8af687e542a182b99f5418b7bd6637f779672b59ede30e5b5082c5c5

See more details on using hashes here.

File details

Details for the file rf_bench_drivers_koolertron-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for rf_bench_drivers_koolertron-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4f27985b383d185db788700f34243f2f60f73c975a6961aef1c8466b523715d1
MD5 a6194950ff63f191d83ee3450192b955
BLAKE2b-256 3d286197356812b374bcc6895bb9e6639f784fd0e1bc349b304c222b6b6893e1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page