pulse modulation added
This commit is contained in:
+8
-5
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = <width> 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()
|
||||
Reference in New Issue
Block a user