diff --git a/App/Core/app_core.c b/App/Core/app_core.c index 3da9196..728fb48 100644 --- a/App/Core/app_core.c +++ b/App/Core/app_core.c @@ -202,6 +202,7 @@ static void app_write_tec_modulation_sample(void); static void app_set_stm32_dac(uint16_t code, uint8_t enabled); static void app_update_sweep_timer(void); static void app_stop_sweeps(void); +static bool app_apply_sweep_config(sweep_t *sweep, const sweep_config_t *config); static void app_prepare_status_response(void); static void app_capture_live_frame(bool update_temperature_loops, bool drive_laser_currents); static void app_decode_work_packet(const uint16_t *packet_words); @@ -594,6 +595,34 @@ static void app_stop_sweeps(void) app_update_sweep_timer(); } +/** + * @brief Stop @p sweep and restart it with @p config when enabled. + * + * @return false if an enabled configuration could not be started. + */ +static bool app_apply_sweep_config(sweep_t *sweep, const sweep_config_t *config) +{ + const bool dac_target = (sweep == &g_app.stm32_dac_sweep); + bool started = true; + + sweep_stop(sweep); + if (config->enabled != 0u) + { + /* The laser current is only driven in WORK mode; idle entry stops the sweep again. */ + started = dac_target ? (config->max_code <= STM32_DAC_MAX_CODE) : (g_app.mode == APP_MODE_WORK); + __disable_irq(); + started = started && sweep_start(sweep, config, APP_SWEEP_TICK_US); + __enable_irq(); + } + + if (dac_target && (sweep->enabled == 0u)) + { + stm32_dac_output_set(g_app.stm32_dac.code, g_app.stm32_dac.enabled); + } + app_update_sweep_timer(); + return started; +} + static void app_apply_profile_auxiliary_outputs(const profile_t *profile) { if (profile == NULL) @@ -603,6 +632,10 @@ static void app_apply_profile_auxiliary_outputs(const profile_t *profile) ad9833_apply(profile->ad9833.enabled, profile->ad9833.triangle, profile->ad9833.frequency_word); app_set_stm32_dac(profile->stm32_dac.code, profile->stm32_dac.enabled); + if (!app_apply_sweep_config(&g_app.stm32_dac_sweep, &profile->stm32_dac_sweep)) + { + app_set_error(APP_STATUS_FLAG_SD_ERROR); + } } static void app_prepare_status_response(void) @@ -676,6 +709,10 @@ static bool app_activate_profile(const profile_t *profile, uint16_t index, bool app_apply_work_config_to_board(); app_set_mode(APP_MODE_WORK); g_app.resume_mode = APP_MODE_WORK; + if (!app_apply_sweep_config(&g_app.laser2_current_sweep, &profile->laser2_current_sweep)) + { + app_set_error(APP_STATUS_FLAG_SD_ERROR); + } } if (profile->ds1809.apply_position) @@ -1386,8 +1423,7 @@ static void app_handle_sweep_control_packet(const app_packet_t *packet) { sweep_config_t config; sweep_t *sweep; - bool dac_target; - bool started = true; + uint16_t flags; if ((packet == NULL) || !packet->checksum_valid || ((packet->words[0] & ~APP_SWEEP_ALLOWED_FLAGS) != 0u)) @@ -1397,32 +1433,16 @@ static void app_handle_sweep_control_packet(const app_packet_t *packet) return; } - dac_target = (packet->words[0] & APP_SWEEP_FLAG_TARGET_STM32_DAC) != 0u; - sweep = dac_target ? &g_app.stm32_dac_sweep : &g_app.laser2_current_sweep; - sweep_stop(sweep); - - if ((packet->words[0] & APP_SWEEP_FLAG_ENABLE) != 0u) - { - config.min_code = packet->words[1]; - config.max_code = packet->words[2]; - config.step_code = packet->words[3]; - config.step_period_us = packet->words[4]; - config.pause_ms = packet->words[5]; - - /* The laser current is only driven in WORK mode; idle entry stops the sweep again. */ - started = dac_target ? (config.max_code <= STM32_DAC_MAX_CODE) : (g_app.mode == APP_MODE_WORK); - __disable_irq(); - started = started && sweep_start(sweep, &config, APP_SWEEP_TICK_US); - __enable_irq(); - } - - if (dac_target && (sweep->enabled == 0u)) - { - stm32_dac_output_set(g_app.stm32_dac.code, g_app.stm32_dac.enabled); - } - app_update_sweep_timer(); - - if (!started) + flags = packet->words[0]; + sweep = ((flags & APP_SWEEP_FLAG_TARGET_STM32_DAC) != 0u) ? &g_app.stm32_dac_sweep + : &g_app.laser2_current_sweep; + config.enabled = ((flags & APP_SWEEP_FLAG_ENABLE) != 0u) ? 1u : 0u; + config.min_code = packet->words[1]; + config.max_code = packet->words[2]; + config.step_code = packet->words[3]; + config.step_period_us = packet->words[4]; + config.pause_ms = packet->words[5]; + if (!app_apply_sweep_config(sweep, &config)) { app_set_error(APP_STATUS_FLAG_UART_DECODE_ERROR); } diff --git a/App/Models/app_types.h b/App/Models/app_types.h index 9434f0f..9d5652c 100644 --- a/App/Models/app_types.h +++ b/App/Models/app_types.h @@ -347,6 +347,22 @@ typedef struct tec_modulation_config_t { uint16_t amplitude_code; } tec_modulation_config_t; +/** + * @brief Periodic staircase sweep of one output code. + * + * The output steps from `min_code` up to `max_code` (inclusive) every + * `step_period_us`, then holds `min_code` for an extra `pause_ms` before the + * next sweep starts. + */ +typedef struct sweep_config_t { + uint8_t enabled; + uint16_t min_code; + uint16_t max_code; + uint16_t step_code; + uint16_t step_period_us; + uint16_t pause_ms; +} sweep_config_t; + /** * @brief Absolute DS1809 target expressed as a number of steps above the minimum tap. */ @@ -371,6 +387,8 @@ typedef struct profile_t { stm32_dac_config_t stm32_dac; tec_modulation_config_t tec_modulation; ds1809_config_t ds1809; + sweep_config_t laser2_current_sweep; + sweep_config_t stm32_dac_sweep; } profile_t; /** diff --git a/App/Services/profile_repository.c b/App/Services/profile_repository.c index c57bf17..baac8a7 100644 --- a/App/Services/profile_repository.c +++ b/App/Services/profile_repository.c @@ -13,7 +13,8 @@ #include "ad9102_device.h" #include "storage_sd.h" -#define PROFILE_REPOSITORY_BUFFER_SIZE 1024u +#define PROFILE_REPOSITORY_INDEX_BUFFER_SIZE 1024u +#define PROFILE_REPOSITORY_INI_BUFFER_SIZE 4096u static char *profile_repository_trim(char *text) { @@ -114,6 +115,52 @@ static void profile_repository_reset_profile(profile_t *profile) profile->ds1809.apply_position = false; } +/** + * @brief Apply a `=value` sweep key. + * + * @return true when @p key carries @p prefix, i.e. belongs to this sweep. + */ +static bool profile_repository_apply_sweep_key(sweep_config_t *sweep, + const char *prefix, + const char *key, + const char *value) +{ + const size_t prefix_length = strlen(prefix); + const char *field; + + if (strncmp(key, prefix, prefix_length) != 0) + { + return false; + } + + field = key + prefix_length; + if (strcmp(field, "enable") == 0) + { + sweep->enabled = (uint8_t)profile_repository_parse_u16(value); + } + else if (strcmp(field, "min_code") == 0) + { + sweep->min_code = profile_repository_parse_u16(value); + } + else if (strcmp(field, "max_code") == 0) + { + sweep->max_code = profile_repository_parse_u16(value); + } + else if (strcmp(field, "step_code") == 0) + { + sweep->step_code = profile_repository_parse_u16(value); + } + else if (strcmp(field, "step_period_us") == 0) + { + sweep->step_period_us = profile_repository_parse_u16(value); + } + else if (strcmp(field, "pause_ms") == 0) + { + sweep->pause_ms = profile_repository_parse_u16(value); + } + return true; +} + static void profile_repository_apply_key_value(profile_t *profile, const char *key, const char *value) { bool bool_value; @@ -123,6 +170,12 @@ static void profile_repository_apply_key_value(profile_t *profile, const char *k return; } + if (profile_repository_apply_sweep_key(&profile->laser2_current_sweep, "laser2_current_sweep_", key, value) || + profile_repository_apply_sweep_key(&profile->stm32_dac_sweep, "stm32_dac_sweep_", key, value)) + { + return; + } + if (strcmp(key, "profile_name") == 0) { profile_repository_copy_string(profile->display_name, sizeof(profile->display_name), value); @@ -351,13 +404,15 @@ static void profile_repository_apply_key_value(profile_t *profile, const char *k static bool profile_repository_load_ini_file(const char *profile_path, profile_t *profile) { - char buffer[PROFILE_REPOSITORY_BUFFER_SIZE]; + /* Static: profiles are parsed only from the main loop, and the stack already holds FatFs' FIL. */ + static char buffer[PROFILE_REPOSITORY_INI_BUFFER_SIZE]; UINT bytes_read = 0u; char *cursor; FRESULT result; - result = storage_sd_read_bytes(profile_path, 0u, buffer, sizeof(buffer) - 1u, &bytes_read); - if (result != FR_OK) + result = storage_sd_read_bytes(profile_path, 0u, buffer, sizeof(buffer), &bytes_read); + /* A file that fills the whole buffer leaves no room for the terminator: reject it rather than drop keys. */ + if ((result != FR_OK) || (bytes_read >= sizeof(buffer))) { return false; } @@ -412,7 +467,7 @@ static bool profile_repository_load_by_valid_line_index(uint16_t target_index, profile_t *out_profile, uint16_t *out_total_valid_lines) { - char buffer[PROFILE_REPOSITORY_BUFFER_SIZE]; + char buffer[PROFILE_REPOSITORY_INDEX_BUFFER_SIZE]; UINT bytes_read = 0u; char *cursor; uint16_t valid_line_index = 0u; diff --git a/App/Services/sweep.h b/App/Services/sweep.h index 6ed64c7..be44bbe 100644 --- a/App/Services/sweep.h +++ b/App/Services/sweep.h @@ -1,10 +1,6 @@ /** * @file sweep.h - * @brief Hardware-agnostic periodic staircase sweep between two output codes. - * - * The output steps from `min_code` up to `max_code` (inclusive) every step - * period, then returns to `min_code` and holds it for an extra pause before - * the next sweep starts. + * @brief Hardware-agnostic periodic staircase sweep (see sweep_config_t). */ #ifndef SWEEP_H @@ -13,16 +9,7 @@ #include #include -/** - * @brief Host-supplied sweep parameters. - */ -typedef struct sweep_config_t { - uint16_t min_code; - uint16_t max_code; - uint16_t step_code; - uint16_t step_period_us; - uint16_t pause_ms; -} sweep_config_t; +#include "app_types.h" /** * @brief Sweep state advanced from the periodic tick interrupt. @@ -39,7 +26,9 @@ typedef struct sweep_t { } sweep_t; /** - * @brief Validate the configuration and start sweeping from `min_code` on the next tick. + * @brief Validate the sweep parameters and start from `min_code` on the next tick. + * + * The `enabled` field of @p config is not consulted: whether to start is the caller's decision. * * @return false (sweep left stopped) if bounds, step or period are invalid. */ diff --git a/Examples/SD_Card/README.md b/Examples/SD_Card/README.md index edc797c..e556989 100644 --- a/Examples/SD_Card/README.md +++ b/Examples/SD_Card/README.md @@ -35,6 +35,7 @@ Profile `.ini` format: - Empty lines are ignored. - Lines starting with `#` or `;` are ignored. - Unknown keys are ignored. +- The whole file must be smaller than 4096 bytes; a larger profile fails to load instead of being truncated. Additional profile-controlled devices: @@ -44,12 +45,18 @@ Additional profile-controlled devices: - TEC drive modulation is stored through `tec_modulation_enable`, `tec_modulation_laser`, `tec_modulation_frequency_hz`, and `tec_modulation_amplitude_code`. - `DS1809` is stored as an absolute target above the minimum tap using `ds1809_apply` and `ds1809_position_from_min`. +- Laser 2 current and STM32 DAC sweeps are stored through the `laser2_current_sweep_*` and `stm32_dac_sweep_*` keys: + `_enable`, `_min_code`, `_max_code`, `_step_code`, `_step_period_us`, and `_pause_ms`. Notes: - `ad9833_frequency_word` uses the same raw 28-bit tuning word that the old serial command accepted. - `tec_modulation_laser` is `1` or `2`; accepted TEC modulation frequency is `50..2000 Hz`, - and peak amplitude is `0..4096` external TEC DAC codes. + and peak amplitude is `0..27900` external TEC DAC codes (further limited by the headroom around the PID output). +- A sweep steps from `_min_code` up to `_max_code` (inclusive) by `_step_code` every `_step_period_us` + (at least `10`, rounded down to a multiple of 10 us), then holds `_min_code` for `_pause_ms` before repeating. +- Sweep codes are raw DAC codes: the laser current DAC uses the same units as `laser2_current`, the STM32 DAC `0..4095`. +- The laser 2 current sweep only runs in `auto_run=true` profiles, because it needs the laser to be driven. - `ds1809_position_from_min` is clamped to the valid `0..63` range by the firmware. - When `ds1809_apply=true`, the firmware first drives the potentiometer fully down and then steps it up to the requested absolute position. diff --git a/Examples/SD_Card/profiles/PROF1.INI b/Examples/SD_Card/profiles/PROF1.INI index 5b651da..3838ddc 100644 --- a/Examples/SD_Card/profiles/PROF1.INI +++ b/Examples/SD_Card/profiles/PROF1.INI @@ -50,3 +50,17 @@ tec_modulation_amplitude_code=0 ds1809_apply=true ds1809_position_from_min=0 + +laser2_current_sweep_enable=0 +laser2_current_sweep_min_code=29490 +laser2_current_sweep_max_code=39321 +laser2_current_sweep_step_code=49 +laser2_current_sweep_step_period_us=50 +laser2_current_sweep_pause_ms=5 + +stm32_dac_sweep_enable=0 +stm32_dac_sweep_min_code=0 +stm32_dac_sweep_max_code=4095 +stm32_dac_sweep_step_code=16 +stm32_dac_sweep_step_period_us=50 +stm32_dac_sweep_pause_ms=5 diff --git a/Examples/SD_Card/profiles/PROF2.INI b/Examples/SD_Card/profiles/PROF2.INI index edc8a42..458038b 100644 --- a/Examples/SD_Card/profiles/PROF2.INI +++ b/Examples/SD_Card/profiles/PROF2.INI @@ -54,3 +54,17 @@ tec_modulation_amplitude_code=0 ds1809_apply=true ds1809_position_from_min=18 + +laser2_current_sweep_enable=0 +laser2_current_sweep_min_code=29490 +laser2_current_sweep_max_code=39321 +laser2_current_sweep_step_code=49 +laser2_current_sweep_step_period_us=50 +laser2_current_sweep_pause_ms=5 + +stm32_dac_sweep_enable=0 +stm32_dac_sweep_min_code=0 +stm32_dac_sweep_max_code=4095 +stm32_dac_sweep_step_code=16 +stm32_dac_sweep_step_period_us=50 +stm32_dac_sweep_pause_ms=5