From fd95fe3c56fe5f47bd510d2113c273d44624e2e0 Mon Sep 17 00:00:00 2001 From: Ayzen Date: Tue, 1 Sep 2026 18:08:35 +0300 Subject: [PATCH] pulse modulation added --- App/Core/app_core.c | 13 ++-- App/Devices/ad9102_device.c | 48 ++++++++++-- App/Devices/ad9102_device.h | 3 +- App/Models/app_types.h | 4 + App/Services/profile_repository.c | 5 ++ tools/ad9102_pulse.py | 119 ++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 tools/ad9102_pulse.py diff --git a/App/Core/app_core.c b/App/Core/app_core.c index dd0b086..b3d653a 100644 --- a/App/Core/app_core.c +++ b/App/Core/app_core.c @@ -195,7 +195,7 @@ static bool app_activate_profile(const profile_t *profile, uint16_t index, bool static bool app_handle_profile_boot(void); static bool app_apply_waveform_config(const waveform_config_t *waveform); static void app_apply_profile_auxiliary_outputs(const profile_t *profile); -static bool app_load_custom_waveform_from_file(const char *path); +static bool app_load_custom_waveform_from_file(const char *path, uint16_t pat_period_us); static void app_handle_packet(const app_packet_t *packet); static void app_handle_work_config_packet(const app_packet_t *packet); static void app_handle_profile_save_control_packet(const app_packet_t *packet); @@ -724,7 +724,7 @@ static void app_decode_work_packet(const uint16_t *packet_words) g_app.laser_config[1].current_raw = packet_words[12]; } -static bool app_load_custom_waveform_from_file(const char *path) +static bool app_load_custom_waveform_from_file(const char *path, uint16_t pat_period_us) { char buffer[APP_CUSTOM_WAVE_BUFFER_SIZE]; UINT bytes_read = 0u; @@ -769,7 +769,7 @@ static bool app_load_custom_waveform_from_file(const char *path) cursor = end_ptr; } - if ((sample_count < 2u) || !ad9102_begin_custom_upload(sample_count)) + if ((sample_count < 2u) || !ad9102_begin_custom_upload(sample_count, pat_period_us)) { return false; } @@ -858,7 +858,8 @@ static bool app_apply_waveform_config(const waveform_config_t *waveform) ad9102_stop_output(); return true; } - return app_load_custom_waveform_from_file(waveform->source_path); + return app_load_custom_waveform_from_file(waveform->source_path, + waveform->custom_pat_period_us); default: return false; @@ -1352,7 +1353,9 @@ static void app_handle_ad9102_wave_control_packet(const app_packet_t *packet) switch (opcode) { case APP_AD9102_WAVE_OPCODE_BEGIN: - if ((param1 != 0u) || !ad9102_begin_custom_upload(param0)) + /* param1 = pattern repetition period in microseconds; 0 keeps + * the legacy back-to-back playback. */ + if (!ad9102_begin_custom_upload(param0, param1)) { ad9102_cancel_custom_upload(); app_set_error(APP_STATUS_FLAG_AD9102_ERROR); diff --git a/App/Devices/ad9102_device.c b/App/Devices/ad9102_device.c index 5764a88..3f63fc4 100644 --- a/App/Devices/ad9102_device.c +++ b/App/Devices/ad9102_device.c @@ -44,6 +44,9 @@ #define AD9102_EX2_SAW_CONFIG 0x0200u #define AD9102_SRAM_PAT_PERIOD_BASE_DEFAULT 0x1u #define AD9102_SRAM_START_DELAY_BASE_DEFAULT 0x1u + +/* Board-level oscillator on the AD9102 CLKIN pin. */ +#define AD9102_DAC_CLOCK_MHZ 150u #define AD9102_SRAM_START_DLY_DEFAULT 0x0000u #define AD9102_SRAM_RAMP_MIN (-8192) #define AD9102_SRAM_RAMP_MAX (8191) @@ -207,10 +210,13 @@ static void ad9102_start_output(void) HAL_GPIO_WritePin(AD9102_TRIG_GPIO_Port, AD9102_TRIG_Pin, GPIO_PIN_RESET); } -static void ad9102_configure_sram_playback(uint16_t sample_count, uint8_t hold_cycles) +static void ad9102_configure_sram_playback(uint16_t sample_count, + uint8_t hold_cycles, + uint16_t pat_period_us) { uint16_t pat_timebase; uint32_t pat_period; + uint32_t pat_period_base = AD9102_SRAM_PAT_PERIOD_BASE_DEFAULT; if (sample_count < 2u) { @@ -229,9 +235,6 @@ static void ad9102_configure_sram_playback(uint16_t sample_count, uint8_t hold_c hold_cycles = 0x0Fu; } - pat_timebase = (uint16_t)(((uint16_t)(hold_cycles & 0x0Fu) << 8) | - ((AD9102_SRAM_PAT_PERIOD_BASE_DEFAULT & 0x0Fu) << 4) | - (AD9102_SRAM_START_DELAY_BASE_DEFAULT & 0x0Fu)); pat_period = (uint32_t)sample_count * (uint32_t)(hold_cycles & 0x0Fu); if (pat_period == 0u) { @@ -242,6 +245,37 @@ static void ad9102_configure_sram_playback(uint16_t sample_count, uint8_t hold_c pat_period = 0xFFFFu; } + if (pat_period_us != 0u) + { + /* Repetition period requested in microseconds (0 = back-to-back). + * PAT_PERIOD counts in units of PATTERN_PERIOD_BASE DAC clocks, so + * spread the tick count over the smallest base that fits 16 bits. */ + uint32_t period_clocks = (uint32_t)pat_period_us * AD9102_DAC_CLOCK_MHZ; + uint32_t pattern_clocks = (uint32_t)sample_count * (uint32_t)(hold_cycles & 0x0Fu); + + if (period_clocks < pattern_clocks) + { + /* Shorter than the pattern itself would truncate it and raise a + * configuration error; fall back to back-to-back playback. */ + period_clocks = pattern_clocks; + } + + pat_period_base = 1u + ((period_clocks - 1u) / 0xFFFFu); + if (pat_period_base > 0x0Fu) + { + pat_period_base = 0x0Fu; + } + pat_period = (period_clocks + (pat_period_base / 2u)) / pat_period_base; + if (pat_period > 0xFFFFu) + { + pat_period = 0xFFFFu; + } + } + + pat_timebase = (uint16_t)(((uint16_t)(hold_cycles & 0x0Fu) << 8) | + ((uint16_t)(pat_period_base & 0x0Fu) << 4) | + (AD9102_SRAM_START_DELAY_BASE_DEFAULT & 0x0Fu)); + ad9102_write_reg_table(g_ad9102_example2_regval, AD9102_REG_COUNT); ad9102_stop_output(); ad9102_write_reg(AD9102_REG_WAV_CONFIG, AD9102_EX2_WAV_CONFIG); @@ -426,7 +460,7 @@ uint16_t ad9102_apply_generated_sram(uint8_t enabled, amplitude = AD9102_SRAM_DEFAULT_AMPLITUDE; } - ad9102_configure_sram_playback(sample_count, hold_cycles); + ad9102_configure_sram_playback(sample_count, hold_cycles, 0u); ad9102_load_sram_ramp(sample_count, triangle_mode, amplitude); if (enabled != 0u) @@ -441,7 +475,7 @@ uint16_t ad9102_apply_generated_sram(uint8_t enabled, return ad9102_read_reg(AD9102_REG_PAT_STATUS); } -bool ad9102_begin_custom_upload(uint16_t sample_count) +bool ad9102_begin_custom_upload(uint16_t sample_count, uint16_t pat_period_us) { if ((sample_count < 2u) || (sample_count > AD9102_SRAM_MAX_SAMPLE_COUNT)) { @@ -450,7 +484,7 @@ bool ad9102_begin_custom_upload(uint16_t sample_count) ad9102_stop_output(); ad9102_reset_upload_state(); - ad9102_configure_sram_playback(sample_count, AD9102_SRAM_DEFAULT_HOLD); + ad9102_configure_sram_playback(sample_count, AD9102_SRAM_DEFAULT_HOLD, pat_period_us); ad9102_write_reg(AD9102_REG_PAT_STATUS, 0x0004u); g_upload_state.expected_samples = sample_count; diff --git a/App/Devices/ad9102_device.h b/App/Devices/ad9102_device.h index d84aac6..7aa2180 100644 --- a/App/Devices/ad9102_device.h +++ b/App/Devices/ad9102_device.h @@ -34,7 +34,8 @@ uint16_t ad9102_apply_generated_sram(uint8_t enabled, uint8_t hold_cycles, uint8_t triangle_mode, uint16_t amplitude); -bool ad9102_begin_custom_upload(uint16_t sample_count); +/* pat_period_us: pattern repetition period in microseconds (0 = back-to-back). */ +bool ad9102_begin_custom_upload(uint16_t sample_count, uint16_t pat_period_us); bool ad9102_write_custom_chunk(const uint16_t *samples, uint16_t chunk_count); uint16_t ad9102_commit_custom_upload(bool *out_ok); void ad9102_cancel_custom_upload(void); diff --git a/App/Models/app_types.h b/App/Models/app_types.h index 182f459..d48908d 100644 --- a/App/Models/app_types.h +++ b/App/Models/app_types.h @@ -305,6 +305,10 @@ typedef struct waveform_config_t { uint8_t saw_step; uint8_t pat_period_base; uint16_t pat_period; + /* Repetition period for custom SRAM patterns in microseconds; + * 0 keeps the legacy back-to-back playback. Kept separate from + * pat_period because that field carries the SAW-mode default (0xFFFF). */ + uint16_t custom_pat_period_us; uint16_t sample_count; uint8_t hold_cycles; uint16_t amplitude; diff --git a/App/Services/profile_repository.c b/App/Services/profile_repository.c index 31ad5fa..c57bf17 100644 --- a/App/Services/profile_repository.c +++ b/App/Services/profile_repository.c @@ -103,6 +103,7 @@ static void profile_repository_reset_profile(profile_t *profile) profile->waveform.saw_step = 1u; profile->waveform.pat_period_base = 2u; profile->waveform.pat_period = 0xFFFFu; + profile->waveform.custom_pat_period_us = 0u; profile->waveform.sample_count = AD9102_SRAM_DEFAULT_SAMPLE_COUNT; profile->waveform.hold_cycles = AD9102_SRAM_DEFAULT_HOLD; profile->waveform.amplitude = AD9102_SRAM_DEFAULT_AMPLITUDE; @@ -267,6 +268,10 @@ static void profile_repository_apply_key_value(profile_t *profile, const char *k { profile->waveform.pat_period = profile_repository_parse_u16(value); } + else if (strcmp(key, "waveform_custom_period_us") == 0) + { + profile->waveform.custom_pat_period_us = profile_repository_parse_u16(value); + } else if (strcmp(key, "waveform_sample_count") == 0) { profile->waveform.sample_count = profile_repository_parse_u16(value); diff --git a/tools/ad9102_pulse.py b/tools/ad9102_pulse.py new file mode 100644 index 0000000..1c472ab --- /dev/null +++ b/tools/ad9102_pulse.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Load a periodic pulse into the AD9102 SRAM over the UART protocol. + +Builds the BEGIN (0xCCCC) / DATA (0xDDDD) / COMMIT packet sequence for the +custom-waveform upload path. Pattern = of full-scale top samples +followed by base samples; BEGIN param1 carries the repetition period in +MICROSECONDS (firmware converts it to PAT_PERIOD/PATTERN_PERIOD_BASE for +its 150 MHz DAC clock; 0 = legacy back-to-back playback). + +Example (1 us pulse at 1 kHz): + python3 ad9102_pulse.py --port /dev/ttyUSB0 --width-us 1 --period-us 1000 +Use --dry-run to print the packets without opening a port. +""" + +import argparse +import sys + +HEADER_WAVE_CONTROL = 0xCCCC +HEADER_WAVE_DATA = 0xDDDD +OPCODE_BEGIN = 0x0001 +OPCODE_COMMIT = 0x0002 +OPCODE_CANCEL = 0x0003 + +DAC_CLOCK_HZ = 150_000_000 # board oscillator on AD9102 CLKIN +DATA_WORDS = 14 # count + 12 samples + checksum +MAX_CHUNK_SAMPLES = 12 +MAX_SRAM_SAMPLES = 4096 +MAX_PERIOD_US = 6553 # 65535 ticks x base 15 at 150 MHz +SAMPLE_MIN = -8192 +SAMPLE_MAX = 8191 + + +def xor_checksum(words): + checksum = words[0] + for word in words[1:]: + checksum ^= word + return checksum + + +def pack_packet(header, words): + words = list(words) + [xor_checksum(words)] + raw = bytearray() + raw += header.to_bytes(2, "little") + for word in words: + raw += (word & 0xFFFF).to_bytes(2, "little") + return bytes(raw) + + +def build_packets(samples, period_us): + packets = [pack_packet(HEADER_WAVE_CONTROL, + [OPCODE_BEGIN, len(samples), period_us])] + for offset in range(0, len(samples), MAX_CHUNK_SAMPLES): + chunk = samples[offset:offset + MAX_CHUNK_SAMPLES] + words = [len(chunk)] + [s & 0xFFFF for s in chunk] + words += [0] * (DATA_WORDS - 1 - len(words)) + packets.append(pack_packet(HEADER_WAVE_DATA, words)) + packets.append(pack_packet(HEADER_WAVE_CONTROL, [OPCODE_COMMIT, 0, 0])) + return packets + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--port", help="serial port, e.g. /dev/ttyUSB0 or COM5") + parser.add_argument("--baud", type=int, default=115200) + parser.add_argument("--width-us", type=float, required=True, + help="pulse width, microseconds") + parser.add_argument("--period-us", type=int, required=True, + help="repetition period, integer microseconds") + parser.add_argument("--top", type=int, default=SAMPLE_MAX, + help="pulse top code, -8192..8191 (default max)") + parser.add_argument("--base", type=int, default=SAMPLE_MIN, + help="baseline code, -8192..8191 (default min)") + parser.add_argument("--dry-run", action="store_true", + help="print packets, do not open the port") + args = parser.parse_args() + + width_samples = max(1, round(DAC_CLOCK_HZ * args.width_us * 1e-6)) + tail_samples = max(1, min(8, MAX_SRAM_SAMPLES - width_samples)) + total_samples = width_samples + tail_samples + + if not (SAMPLE_MIN <= args.base <= SAMPLE_MAX + and SAMPLE_MIN <= args.top <= SAMPLE_MAX): + sys.exit("top/base out of DAC range -8192..8191") + if total_samples > MAX_SRAM_SAMPLES: + sys.exit(f"pulse too long: {width_samples} samples > SRAM {MAX_SRAM_SAMPLES}") + if not 0 <= args.period_us <= MAX_PERIOD_US: + sys.exit(f"period must be 0..{MAX_PERIOD_US} us") + pattern_us = total_samples * 1e6 / DAC_CLOCK_HZ + if args.period_us and args.period_us <= pattern_us: + print(f"note: period <= pattern ({pattern_us:.2f} us) -> back-to-back playback") + + samples = [args.top] * width_samples + [args.base] * tail_samples + packets = build_packets(samples, args.period_us) + + print(f"pulse: {width_samples} samples = {width_samples / DAC_CLOCK_HZ * 1e6:.3f} us") + if args.period_us: + print(f"period: {args.period_us} us ({1e6 / args.period_us:.1f} Hz)") + print(f"pattern {total_samples} samples, {len(packets)} packets") + + if args.dry_run or not args.port: + for packet in packets: + print(packet.hex(" ")) + return + + import serial # pyserial + with serial.Serial(args.port, args.baud, timeout=1) as link: + for index, packet in enumerate(packets): + link.write(packet) + status = link.read(2) + if len(status) < 2: + sys.exit(f"packet {index}: no status response") + if status[0] != 0: + sys.exit(f"packet {index}: device error flags 0x{status[0]:02X}") + print(f"packet {index}: ok, status {status.hex(' ')}") + print("committed; RUN bit:", "on" if status[1] & 0x01 else "off") + + +if __name__ == "__main__": + main()