big refactoring and features added

This commit is contained in:
Ayzen
2026-04-24 16:51:15 +03:00
parent eafc328caa
commit ea1fbb071d
184 changed files with 35336 additions and 75480 deletions

1416
App/Core/app_core.c Normal file

File diff suppressed because it is too large Load Diff

61
App/Core/app_core.h Normal file
View File

@ -0,0 +1,61 @@
/**
* @file app_core.h
* @brief Top-level application runtime and IRQ entry points.
*/
#ifndef APP_CORE_H
#define APP_CORE_H
#include <stdint.h>
#include "app_types.h"
/**
* @brief Initialise the modular application layer after CubeMX peripherals are ready.
*/
void app_init(void);
/**
* @brief Execute one non-blocking iteration of the top-level application state machine.
*/
void app_run_once(void);
/**
* @brief Post a deferred application event from an ISR, button handler, or future UI backend.
*
* The event is latched and consumed inside `app_run_once()` so hardware-facing
* interrupt adapters stay thin and the main runtime keeps ownership of mode
* transitions and safe-stop sequencing.
*
* @param event Event to enqueue for the application core.
*/
void app_post_event(app_event_t event);
/**
* @brief Forward one raw UART byte into the transport-agnostic protocol parser.
*
* @param byte Newly received UART byte.
*/
void app_on_uart_byte(uint8_t byte);
/**
* @brief Notify the application core about a UART framing/parity/overrun error.
*/
void app_on_uart_error(void);
/**
* @brief Notify the application core about the 10 ms housekeeping tick.
*/
void app_on_tim6_tick(void);
/**
* @brief Notify the application core about the 1 ms timebase tick.
*/
void app_on_tim7_tick(void);
/**
* @brief Notify the application core that the USART DMA transmit finished.
*/
void app_on_dma_tx_complete(void);
#endif /* APP_CORE_H */

716
App/Devices/ad9102_device.c Normal file
View File

@ -0,0 +1,716 @@
/**
* @file ad9102_device.c
* @brief AD9102 waveform-generation device driver.
*/
#include "ad9102_device.h"
#include "board_io.h"
#include "main.h"
/* AD9102 register map used by the existing firmware. See ad9102.pdf. */
#define AD9102_REG_SPICONFIG 0x0000u
#define AD9102_REG_POWERCONFIG 0x0001u
#define AD9102_REG_CLOCKCONFIG 0x0002u
#define AD9102_REG_RAMUPDATE 0x001Du
#define AD9102_REG_PAT_STATUS 0x001Eu
#define AD9102_REG_PAT_TYPE 0x001Fu
#define AD9102_REG_WAV_CONFIG 0x0027u
#define AD9102_REG_PAT_TIMEBASE 0x0028u
#define AD9102_REG_PAT_PERIOD 0x0029u
#define AD9102_REG_DAC_PAT 0x002Bu
#define AD9102_REG_SAW_CONFIG 0x0037u
#define AD9102_REG_START_DLY 0x005Cu
#define AD9102_REG_START_ADDR 0x005Du
#define AD9102_REG_STOP_ADDR 0x005Eu
#define AD9102_REG_CFG_ERROR 0x0060u
#define AD9102_REG_SRAM_DATA_BASE 0x6000u
#define AD9102_PAT_STATUS_RUN (1u << 0)
#define AD9102_SAW_TYPE_UP 0u
#define AD9102_SAW_TYPE_TRIANGLE 2u
#define AD9102_SAW_STEP_DEFAULT 1u
#define AD9102_PAT_PERIOD_BASE_DEFAULT 0x2u
#define AD9102_START_DELAY_BASE_DEFAULT 0x1u
#define AD9102_PAT_TIMEBASE_HOLD_DEFAULT 0x1u
#define AD9102_PAT_PERIOD_DEFAULT 0xFFFFu
#define AD9102_EX4_WAV_CONFIG 0x3212u
#define AD9102_EX4_SAW_CONFIG 0x0606u
#define AD9102_EX2_WAV_CONFIG 0x3030u
#define AD9102_EX2_DAC_PAT 0x0101u
#define AD9102_EX2_SAW_CONFIG 0x0200u
#define AD9102_SRAM_PAT_PERIOD_BASE_DEFAULT 0x1u
#define AD9102_SRAM_START_DELAY_BASE_DEFAULT 0x1u
#define AD9102_SRAM_START_DLY_DEFAULT 0x0000u
#define AD9102_SRAM_RAMP_MIN (-8192)
#define AD9102_SRAM_RAMP_MAX (8191)
#define AD9102_REG_COUNT 66u
#define AD9102_WAVE_MAX_CHUNK_SAMPLES 12u
static const uint16_t g_ad9102_reg_addr[AD9102_REG_COUNT] = {
0x0000u, 0x0001u, 0x0002u, 0x0003u, 0x0004u, 0x0005u, 0x0006u, 0x0007u,
0x0008u, 0x0009u, 0x000Au, 0x000Bu, 0x000Cu, 0x000Du, 0x000Eu, 0x001Fu,
0x0020u, 0x0022u, 0x0023u, 0x0024u, 0x0025u, 0x0026u, 0x0027u, 0x0028u,
0x0029u, 0x002Au, 0x002Bu, 0x002Cu, 0x002Du, 0x002Eu, 0x002Fu, 0x0030u,
0x0031u, 0x0032u, 0x0033u, 0x0034u, 0x0035u, 0x0036u, 0x0037u, 0x003Eu,
0x003Fu, 0x0040u, 0x0041u, 0x0042u, 0x0043u, 0x0044u, 0x0045u, 0x0047u,
0x0050u, 0x0051u, 0x0052u, 0x0053u, 0x0054u, 0x0055u, 0x0056u, 0x0057u,
0x0058u, 0x0059u, 0x005Au, 0x005Bu, 0x005Cu, 0x005Du, 0x005Eu, 0x005Fu,
0x001Eu, 0x001Du
};
static const uint16_t g_ad9102_example4_regval[AD9102_REG_COUNT] = {
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x4000u,
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x1F00u, 0x0000u, 0x0000u, 0x0000u,
0x000Eu, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x3212u, 0x0121u,
0xFFFFu, 0x0000u, 0x0101u, 0x0003u, 0x0000u, 0x0000u, 0x0000u, 0x0000u,
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x4000u, 0x0000u, 0x0606u, 0x1999u,
0x9A00u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u,
0x0FA0u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u,
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x16FFu,
0x0001u, 0x0001u
};
static const uint16_t g_ad9102_example2_regval[AD9102_REG_COUNT] = {
0x0000u, 0x0E00u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x4000u,
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x1F00u, 0x0000u, 0x0000u, 0x0000u,
0x000Eu, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x3030u, 0x0111u,
0xFFFFu, 0x0000u, 0x0101u, 0x0003u, 0x0000u, 0x0000u, 0x0000u, 0x0000u,
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x4000u, 0x0000u, 0x0200u, 0x0000u,
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u,
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0000u,
0x0000u, 0x0000u, 0x0000u, 0x0000u, 0x0FA0u, 0x0000u, 0x3FF0u, 0x0100u,
0x0001u, 0x0001u
};
typedef struct ad9102_upload_state_t {
uint8_t active;
uint16_t expected_samples;
uint16_t written_samples;
} ad9102_upload_state_t;
static ad9102_upload_state_t g_upload_state;
static void ad9102_write_reg(uint16_t address, uint16_t value)
{
uint16_t command = (uint16_t)(address & 0x7FFFu);
uint32_t timeout = 0u;
board_io_configure_spi2_mode(LL_SPI_POLARITY_LOW, LL_SPI_PHASE_1EDGE);
HAL_GPIO_WritePin(DAC_LD1_CS_GPIO_Port, DAC_LD1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_TEC1_CS_GPIO_Port, DAC_TEC1_CS_Pin, GPIO_PIN_SET);
if (!LL_SPI_IsEnabled(SPI2))
{
LL_SPI_Enable(SPI2);
}
HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_RESET);
while ((!LL_SPI_IsActiveFlag_TXE(SPI2)) && (timeout++ < 1000u))
{
}
LL_SPI_TransmitData16(SPI2, command);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI2)) && (timeout++ < 1000u))
{
}
(void)SPI2->DR;
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_TXE(SPI2)) && (timeout++ < 1000u))
{
}
LL_SPI_TransmitData16(SPI2, value);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI2)) && (timeout++ < 1000u))
{
}
(void)SPI2->DR;
HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_SET);
}
static uint16_t ad9102_read_reg(uint16_t address)
{
uint16_t command = (uint16_t)(0x8000u | (address & 0x7FFFu));
uint16_t value;
uint32_t timeout = 0u;
board_io_configure_spi2_mode(LL_SPI_POLARITY_LOW, LL_SPI_PHASE_1EDGE);
HAL_GPIO_WritePin(DAC_LD1_CS_GPIO_Port, DAC_LD1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_TEC1_CS_GPIO_Port, DAC_TEC1_CS_Pin, GPIO_PIN_SET);
if (!LL_SPI_IsEnabled(SPI2))
{
LL_SPI_Enable(SPI2);
}
HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_RESET);
while ((!LL_SPI_IsActiveFlag_TXE(SPI2)) && (timeout++ < 1000u))
{
}
LL_SPI_TransmitData16(SPI2, command);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI2)) && (timeout++ < 1000u))
{
}
(void)SPI2->DR;
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_TXE(SPI2)) && (timeout++ < 1000u))
{
}
LL_SPI_TransmitData16(SPI2, 0x0000u);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI2)) && (timeout++ < 1000u))
{
}
value = LL_SPI_ReceiveData16(SPI2);
HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_SET);
return value;
}
static void ad9102_write_reg_table(const uint16_t *values, uint16_t count)
{
uint16_t index;
for (index = 0u; index < count; ++index)
{
ad9102_write_reg(g_ad9102_reg_addr[index], values[index]);
}
}
static void ad9102_reset_upload_state(void)
{
g_upload_state.active = 0u;
g_upload_state.expected_samples = 0u;
g_upload_state.written_samples = 0u;
}
static void ad9102_start_output(void)
{
HAL_GPIO_WritePin(AD9102_TRIG_GPIO_Port, AD9102_TRIG_Pin, GPIO_PIN_SET);
ad9102_write_reg(AD9102_REG_PAT_STATUS, AD9102_PAT_STATUS_RUN);
ad9102_write_reg(AD9102_REG_RAMUPDATE, 0x0001u);
for (volatile uint32_t delay_counter = 0u; delay_counter < 1000u; ++delay_counter)
{
}
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)
{
uint16_t pat_timebase;
uint32_t pat_period;
if (sample_count < 2u)
{
sample_count = 2u;
}
if (sample_count > AD9102_SRAM_MAX_SAMPLE_COUNT)
{
sample_count = AD9102_SRAM_MAX_SAMPLE_COUNT;
}
if (hold_cycles == 0u)
{
hold_cycles = AD9102_SRAM_DEFAULT_HOLD;
}
if (hold_cycles > 0x0Fu)
{
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)
{
pat_period = sample_count;
}
if (pat_period > 0xFFFFu)
{
pat_period = 0xFFFFu;
}
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);
ad9102_write_reg(AD9102_REG_SAW_CONFIG, AD9102_EX2_SAW_CONFIG);
ad9102_write_reg(AD9102_REG_DAC_PAT, AD9102_EX2_DAC_PAT);
ad9102_write_reg(AD9102_REG_PAT_TIMEBASE, pat_timebase);
ad9102_write_reg(AD9102_REG_PAT_PERIOD, (uint16_t)pat_period);
ad9102_write_reg(AD9102_REG_PAT_TYPE, 0x0000u);
ad9102_write_reg(AD9102_REG_START_DLY, AD9102_SRAM_START_DLY_DEFAULT);
ad9102_write_reg(AD9102_REG_START_ADDR, 0x0000u);
ad9102_write_reg(AD9102_REG_STOP_ADDR, (uint16_t)((sample_count - 1u) << 4));
ad9102_write_reg(AD9102_REG_RAMUPDATE, 0x0001u);
}
static void ad9102_load_sram_ramp(uint16_t sample_count, uint8_t triangle_mode, uint16_t amplitude)
{
uint16_t sample_index;
if (sample_count < 2u)
{
sample_count = 2u;
}
if (sample_count > AD9102_SRAM_MAX_SAMPLE_COUNT)
{
sample_count = AD9102_SRAM_MAX_SAMPLE_COUNT;
}
if (amplitude > AD9102_SRAM_DEFAULT_AMPLITUDE)
{
amplitude = AD9102_SRAM_DEFAULT_AMPLITUDE;
}
ad9102_write_reg(AD9102_REG_PAT_STATUS, 0x0004u);
for (sample_index = 0u; sample_index < sample_count; ++sample_index)
{
int32_t value;
int32_t min_value = -(int32_t)amplitude;
int32_t max_value = (int32_t)amplitude;
int32_t span = max_value - min_value;
if (triangle_mode != 0u)
{
uint16_t half = sample_count / 2u;
if (half == 0u)
{
half = 1u;
}
if (sample_index < half)
{
uint16_t denominator = (half > 1u) ? (uint16_t)(half - 1u) : 1u;
value = min_value + (span * (int32_t)sample_index) / (int32_t)denominator;
}
else
{
uint16_t tail = (uint16_t)(sample_count - half);
uint16_t denominator = (tail > 1u) ? (uint16_t)(tail - 1u) : 1u;
value = max_value - (span * (int32_t)(sample_index - half)) / (int32_t)denominator;
}
}
else
{
uint16_t denominator = (sample_count > 1u) ? (uint16_t)(sample_count - 1u) : 1u;
value = min_value + (span * (int32_t)sample_index) / (int32_t)denominator;
}
if (value < AD9102_SRAM_RAMP_MIN)
{
value = AD9102_SRAM_RAMP_MIN;
}
else if (value > AD9102_SRAM_RAMP_MAX)
{
value = AD9102_SRAM_RAMP_MAX;
}
ad9102_write_reg((uint16_t)(AD9102_REG_SRAM_DATA_BASE + sample_index),
(uint16_t)(((uint16_t)((int16_t)value) & 0x3FFFu) << 2));
}
ad9102_write_reg(AD9102_REG_PAT_STATUS, 0x0000u);
}
void ad9102_init(void)
{
HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(AD9102_RESET_GPIO_Port, AD9102_RESET_Pin, GPIO_PIN_RESET);
for (volatile uint32_t delay_counter = 0u; delay_counter < 1000u; ++delay_counter)
{
}
HAL_GPIO_WritePin(AD9102_RESET_GPIO_Port, AD9102_RESET_Pin, GPIO_PIN_SET);
ad9102_write_reg_table(g_ad9102_example4_regval, AD9102_REG_COUNT);
ad9102_write_reg(AD9102_REG_PAT_STATUS, 0x0000u);
ad9102_write_reg(AD9102_REG_RAMUPDATE, 0x0001u);
HAL_GPIO_WritePin(AD9102_TRIG_GPIO_Port, AD9102_TRIG_Pin, GPIO_PIN_SET);
ad9102_reset_upload_state();
}
void ad9102_stop_output(void)
{
ad9102_write_reg(AD9102_REG_PAT_STATUS, 0x0000u);
HAL_GPIO_WritePin(AD9102_TRIG_GPIO_Port, AD9102_TRIG_Pin, GPIO_PIN_SET);
}
uint16_t ad9102_apply_saw(uint8_t saw_type,
uint8_t enabled,
uint8_t saw_step,
uint8_t pat_period_base,
uint16_t pat_period)
{
uint16_t saw_config;
uint16_t pat_timebase;
ad9102_reset_upload_state();
if (enabled == 0u)
{
ad9102_stop_output();
return ad9102_read_reg(AD9102_REG_PAT_STATUS);
}
if (saw_step == 0u)
{
saw_step = AD9102_SAW_STEP_DEFAULT;
}
else if (saw_step > 63u)
{
saw_step = 63u;
}
if (pat_period == 0u)
{
pat_period = AD9102_PAT_PERIOD_DEFAULT;
}
saw_config = (uint16_t)(((uint16_t)(saw_step & 0x3Fu) << 2) | ((uint16_t)(saw_type & 0x3u)));
pat_timebase = (uint16_t)(((AD9102_PAT_TIMEBASE_HOLD_DEFAULT & 0x0Fu) << 8) |
((pat_period_base & 0x0Fu) << 4) |
(AD9102_START_DELAY_BASE_DEFAULT & 0x0Fu));
ad9102_write_reg(AD9102_REG_WAV_CONFIG, AD9102_EX4_WAV_CONFIG);
ad9102_write_reg(AD9102_REG_SAW_CONFIG, saw_config);
ad9102_write_reg(AD9102_REG_PAT_TIMEBASE, pat_timebase);
ad9102_write_reg(AD9102_REG_PAT_PERIOD, pat_period);
ad9102_write_reg(AD9102_REG_PAT_TYPE, 0x0000u);
ad9102_start_output();
return ad9102_read_reg(AD9102_REG_PAT_STATUS);
}
uint16_t ad9102_apply_generated_sram(uint8_t enabled,
uint16_t sample_count,
uint8_t hold_cycles,
uint8_t triangle_mode,
uint16_t amplitude)
{
ad9102_reset_upload_state();
if (sample_count == 0u)
{
sample_count = AD9102_SRAM_DEFAULT_SAMPLE_COUNT;
}
if (sample_count < 2u)
{
sample_count = 2u;
}
if (sample_count > AD9102_SRAM_MAX_SAMPLE_COUNT)
{
sample_count = AD9102_SRAM_MAX_SAMPLE_COUNT;
}
if (hold_cycles == 0u)
{
hold_cycles = AD9102_SRAM_DEFAULT_HOLD;
}
if (hold_cycles > 0x0Fu)
{
hold_cycles = 0x0Fu;
}
if (amplitude > AD9102_SRAM_DEFAULT_AMPLITUDE)
{
amplitude = AD9102_SRAM_DEFAULT_AMPLITUDE;
}
ad9102_configure_sram_playback(sample_count, hold_cycles);
ad9102_load_sram_ramp(sample_count, triangle_mode, amplitude);
if (enabled != 0u)
{
ad9102_start_output();
}
else
{
ad9102_stop_output();
}
return ad9102_read_reg(AD9102_REG_PAT_STATUS);
}
bool ad9102_begin_custom_upload(uint16_t sample_count)
{
if ((sample_count < 2u) || (sample_count > AD9102_SRAM_MAX_SAMPLE_COUNT))
{
return false;
}
ad9102_stop_output();
ad9102_reset_upload_state();
ad9102_configure_sram_playback(sample_count, AD9102_SRAM_DEFAULT_HOLD);
ad9102_write_reg(AD9102_REG_PAT_STATUS, 0x0004u);
g_upload_state.expected_samples = sample_count;
g_upload_state.written_samples = 0u;
g_upload_state.active = 1u;
return true;
}
bool ad9102_write_custom_chunk(const uint16_t *samples, uint16_t chunk_count)
{
uint16_t index;
if ((samples == NULL) || (g_upload_state.active == 0u))
{
return false;
}
if ((chunk_count == 0u) || (chunk_count > AD9102_WAVE_MAX_CHUNK_SAMPLES))
{
return false;
}
if (((uint32_t)g_upload_state.written_samples + (uint32_t)chunk_count) >
(uint32_t)g_upload_state.expected_samples)
{
return false;
}
for (index = 0u; index < chunk_count; ++index)
{
int16_t sample = (int16_t)samples[index];
if ((sample < AD9102_SRAM_RAMP_MIN) || (sample > AD9102_SRAM_RAMP_MAX))
{
return false;
}
ad9102_write_reg((uint16_t)(AD9102_REG_SRAM_DATA_BASE + g_upload_state.written_samples + index),
(uint16_t)(((uint16_t)sample & 0x3FFFu) << 2));
}
g_upload_state.written_samples = (uint16_t)(g_upload_state.written_samples + chunk_count);
return true;
}
uint16_t ad9102_commit_custom_upload(bool *out_ok)
{
uint16_t pat_status;
if (out_ok != NULL)
{
*out_ok = false;
}
if ((g_upload_state.active == 0u) ||
(g_upload_state.expected_samples < 2u) ||
(g_upload_state.written_samples != g_upload_state.expected_samples))
{
ad9102_cancel_custom_upload();
return ad9102_read_reg(AD9102_REG_PAT_STATUS);
}
ad9102_write_reg(AD9102_REG_PAT_STATUS, 0x0000u);
ad9102_write_reg(AD9102_REG_START_ADDR, 0x0000u);
ad9102_write_reg(AD9102_REG_STOP_ADDR, (uint16_t)((g_upload_state.expected_samples - 1u) << 4));
ad9102_write_reg(AD9102_REG_RAMUPDATE, 0x0001u);
ad9102_start_output();
pat_status = ad9102_read_reg(AD9102_REG_PAT_STATUS);
ad9102_reset_upload_state();
if (out_ok != NULL)
{
*out_ok = true;
}
return pat_status;
}
void ad9102_cancel_custom_upload(void)
{
if (g_upload_state.active != 0u)
{
ad9102_stop_output();
}
ad9102_reset_upload_state();
}
uint8_t ad9102_check_saw_configuration(uint16_t pat_status,
uint8_t expect_run,
uint8_t saw_type,
uint8_t saw_step,
uint8_t pat_period_base,
uint16_t pat_period)
{
uint16_t spiconfig = ad9102_read_reg(AD9102_REG_SPICONFIG);
uint16_t power_config = ad9102_read_reg(AD9102_REG_POWERCONFIG);
uint16_t clock_config = ad9102_read_reg(AD9102_REG_CLOCKCONFIG);
uint16_t config_error = ad9102_read_reg(AD9102_REG_CFG_ERROR);
uint16_t pat_timebase = (uint16_t)(((AD9102_PAT_TIMEBASE_HOLD_DEFAULT & 0x0Fu) << 8) |
((pat_period_base & 0x0Fu) << 4) |
(AD9102_START_DELAY_BASE_DEFAULT & 0x0Fu));
uint16_t expected_saw;
uint8_t ok = 1u;
if (saw_step == 0u)
{
saw_step = AD9102_SAW_STEP_DEFAULT;
}
if (saw_step > 63u)
{
saw_step = 63u;
}
if (pat_period == 0u)
{
pat_period = AD9102_PAT_PERIOD_DEFAULT;
}
expected_saw = (uint16_t)(((uint16_t)(saw_step & 0x3Fu) << 2) | ((uint16_t)(saw_type & 0x3u)));
if (spiconfig != 0x0000u)
{
ok = 0u;
}
if (power_config & ((1u << 8) | (1u << 7) | (1u << 6) | (1u << 5) | (1u << 3)))
{
ok = 0u;
}
if (clock_config & ((1u << 11) | (1u << 7) | (1u << 6) | (1u << 5)))
{
ok = 0u;
}
if (config_error & 0x003Fu)
{
ok = 0u;
}
if ((expect_run != 0u) && ((pat_status & AD9102_PAT_STATUS_RUN) == 0u))
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_WAV_CONFIG) != AD9102_EX4_WAV_CONFIG)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_PAT_TIMEBASE) != pat_timebase)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_PAT_PERIOD) != pat_period)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_PAT_TYPE) != 0x0000u)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_SAW_CONFIG) != expected_saw)
{
ok = 0u;
}
return (ok != 0u) ? 0u : 1u;
}
uint8_t ad9102_check_sram_configuration(uint16_t pat_status,
uint8_t expect_run,
uint16_t sample_count,
uint8_t hold_cycles)
{
uint16_t spiconfig = ad9102_read_reg(AD9102_REG_SPICONFIG);
uint16_t power_config = ad9102_read_reg(AD9102_REG_POWERCONFIG);
uint16_t clock_config = ad9102_read_reg(AD9102_REG_CLOCKCONFIG);
uint16_t config_error = ad9102_read_reg(AD9102_REG_CFG_ERROR);
uint16_t pat_timebase;
uint32_t pat_period;
uint16_t stop_address;
uint8_t ok = 1u;
if (sample_count == 0u)
{
sample_count = AD9102_SRAM_DEFAULT_SAMPLE_COUNT;
}
if (sample_count < 2u)
{
sample_count = 2u;
}
if (sample_count > AD9102_SRAM_MAX_SAMPLE_COUNT)
{
sample_count = AD9102_SRAM_MAX_SAMPLE_COUNT;
}
if (hold_cycles == 0u)
{
hold_cycles = AD9102_SRAM_DEFAULT_HOLD;
}
if (hold_cycles > 0x0Fu)
{
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)
{
pat_period = sample_count;
}
if (pat_period > 0xFFFFu)
{
pat_period = 0xFFFFu;
}
stop_address = (uint16_t)((sample_count - 1u) << 4);
if (spiconfig != 0x0000u)
{
ok = 0u;
}
if (power_config & ((1u << 8) | (1u << 7) | (1u << 6) | (1u << 5) | (1u << 3)))
{
ok = 0u;
}
if (clock_config & ((1u << 11) | (1u << 7) | (1u << 6) | (1u << 5)))
{
ok = 0u;
}
if (config_error & 0x003Fu)
{
ok = 0u;
}
if ((expect_run != 0u) && ((pat_status & AD9102_PAT_STATUS_RUN) == 0u))
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_WAV_CONFIG) != AD9102_EX2_WAV_CONFIG)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_PAT_TIMEBASE) != pat_timebase)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_PAT_PERIOD) != (uint16_t)pat_period)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_PAT_TYPE) != 0x0000u)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_START_ADDR) != 0x0000u)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_STOP_ADDR) != stop_address)
{
ok = 0u;
}
if (ad9102_read_reg(AD9102_REG_DAC_PAT) != AD9102_EX2_DAC_PAT)
{
ok = 0u;
}
return (ok != 0u) ? 0u : 1u;
}

View File

@ -0,0 +1,52 @@
/**
* @file ad9102_device.h
* @brief AD9102 waveform-generation device driver.
*
* Architectural note:
* The driver owns all AD9102 register tables and safe mode switching rules.
* High-level services describe *what* waveform to apply, while this module
* owns the register-level details of *how* to enter, verify, and leave each
* mode without cross-coupling to UART or storage logic.
*/
#ifndef AD9102_DEVICE_H
#define AD9102_DEVICE_H
#include <stdbool.h>
#include <stdint.h>
#include "app_types.h"
#define AD9102_SRAM_DEFAULT_HOLD 1u
#define AD9102_SRAM_DEFAULT_AMPLITUDE 8191u
#define AD9102_SRAM_DEFAULT_SAMPLE_COUNT 16u
#define AD9102_SRAM_MAX_SAMPLE_COUNT 4096u
void ad9102_init(void);
void ad9102_stop_output(void);
uint16_t ad9102_apply_saw(uint8_t saw_type,
uint8_t enabled,
uint8_t saw_step,
uint8_t pat_period_base,
uint16_t pat_period);
uint16_t ad9102_apply_generated_sram(uint8_t enabled,
uint16_t sample_count,
uint8_t hold_cycles,
uint8_t triangle_mode,
uint16_t amplitude);
bool ad9102_begin_custom_upload(uint16_t sample_count);
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);
uint8_t ad9102_check_saw_configuration(uint16_t pat_status,
uint8_t expect_run,
uint8_t saw_type,
uint8_t saw_step,
uint8_t pat_period_base,
uint16_t pat_period);
uint8_t ad9102_check_sram_configuration(uint16_t pat_status,
uint8_t expect_run,
uint16_t sample_count,
uint8_t hold_cycles);
#endif /* AD9102_DEVICE_H */

View File

@ -0,0 +1,64 @@
/**
* @file ad9833_device.c
* @brief AD9833 waveform-generator control helpers.
*/
#include "ad9833_device.h"
#include "board_io.h"
#include "main.h"
static void ad9833_write_word(uint16_t word)
{
uint32_t timeout = 0u;
board_io_configure_spi2_mode(LL_SPI_POLARITY_HIGH, LL_SPI_PHASE_1EDGE);
HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_LD1_CS_GPIO_Port, DAC_LD1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_TEC1_CS_GPIO_Port, DAC_TEC1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(AD9833_CS_GPIO_Port, AD9833_CS_Pin, GPIO_PIN_RESET);
while ((!LL_SPI_IsActiveFlag_TXE(SPI2)) && (timeout++ < 1000u))
{
}
LL_SPI_TransmitData16(SPI2, word);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI2)) && (timeout++ < 1000u))
{
}
(void)SPI2->DR;
HAL_GPIO_WritePin(AD9833_CS_GPIO_Port, AD9833_CS_Pin, GPIO_PIN_SET);
}
void ad9833_apply(uint8_t enabled, uint8_t triangle_mode, uint32_t frequency_word)
{
uint16_t control = 0x2000u;
uint16_t lsw;
uint16_t msw;
if (triangle_mode != 0u)
{
control |= 0x0002u;
}
control |= 0x0100u;
frequency_word &= 0x0FFFFFFFu;
lsw = (uint16_t)(0x4000u | (frequency_word & 0x3FFFu));
msw = (uint16_t)(0x4000u | ((frequency_word >> 14) & 0x3FFFu));
ad9833_write_word(control);
ad9833_write_word(lsw);
ad9833_write_word(msw);
ad9833_write_word(0xC000u);
if (enabled != 0u)
{
control &= (uint16_t)(~0x0100u);
}
ad9833_write_word(control);
}

View File

@ -0,0 +1,13 @@
/**
* @file ad9833_device.h
* @brief AD9833 waveform generator control helpers.
*/
#ifndef AD9833_DEVICE_H
#define AD9833_DEVICE_H
#include <stdint.h>
void ad9833_apply(uint8_t enabled, uint8_t triangle_mode, uint32_t frequency_word);
#endif /* AD9833_DEVICE_H */

135
App/Devices/adc_mux.c Normal file
View File

@ -0,0 +1,135 @@
/**
* @file adc_mux.c
* @brief External photodiode ADC and internal STM32 ADC helpers.
*/
#include "adc_mux.h"
#include "board_handles.h"
#include "main.h"
uint16_t adc_mux_read_external_channel(uint8_t channel_index)
{
uint16_t result = 0u;
uint32_t delay_counter;
HAL_GPIO_WritePin(SPI4_CNV_GPIO_Port, SPI4_CNV_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(SPI5_CNV_GPIO_Port, SPI5_CNV_Pin, GPIO_PIN_RESET);
for (delay_counter = 0u; delay_counter < 500u; ++delay_counter)
{
}
HAL_GPIO_WritePin(SPI4_CNV_GPIO_Port, SPI4_CNV_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(SPI5_CNV_GPIO_Port, SPI5_CNV_Pin, GPIO_PIN_SET);
for (delay_counter = 0u; delay_counter < 500u; ++delay_counter)
{
}
if (channel_index == 1u)
{
HAL_GPIO_WritePin(ADC_ThrLD1_CS_GPIO_Port, ADC_ThrLD1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(ADC_MPD1_CS_GPIO_Port, ADC_MPD1_CS_Pin, GPIO_PIN_RESET);
for (delay_counter = 0u; delay_counter < 500u; ++delay_counter)
{
}
LL_SPI_Enable(SPI4);
delay_counter = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI4)) && (delay_counter <= 1000u))
{
++delay_counter;
}
LL_SPI_Disable(SPI4);
HAL_GPIO_WritePin(ADC_MPD1_CS_GPIO_Port, ADC_MPD1_CS_Pin, GPIO_PIN_SET);
result = LL_SPI_ReceiveData16(SPI4);
}
else if (channel_index == 2u)
{
HAL_GPIO_WritePin(ADC_ThrLD2_CS_GPIO_Port, ADC_ThrLD2_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(ADC_MPD2_CS_GPIO_Port, ADC_MPD2_CS_Pin, GPIO_PIN_RESET);
for (delay_counter = 0u; delay_counter < 500u; ++delay_counter)
{
}
LL_SPI_Enable(SPI5);
delay_counter = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI5)) && (delay_counter <= 1000u))
{
++delay_counter;
}
LL_SPI_Disable(SPI5);
HAL_GPIO_WritePin(ADC_MPD2_CS_GPIO_Port, ADC_MPD2_CS_Pin, GPIO_PIN_SET);
result = LL_SPI_ReceiveData16(SPI5);
}
else if (channel_index == 3u)
{
HAL_GPIO_WritePin(ADC_MPD1_CS_GPIO_Port, ADC_MPD1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(ADC_ThrLD1_CS_GPIO_Port, ADC_ThrLD1_CS_Pin, GPIO_PIN_RESET);
for (delay_counter = 0u; delay_counter < 500u; ++delay_counter)
{
}
LL_SPI_Enable(SPI4);
delay_counter = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI4)) && (delay_counter <= 1000u))
{
++delay_counter;
}
LL_SPI_Disable(SPI4);
HAL_GPIO_WritePin(ADC_ThrLD1_CS_GPIO_Port, ADC_ThrLD1_CS_Pin, GPIO_PIN_SET);
result = LL_SPI_ReceiveData16(SPI4);
}
else if (channel_index == 4u)
{
HAL_GPIO_WritePin(ADC_MPD2_CS_GPIO_Port, ADC_MPD2_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(ADC_ThrLD2_CS_GPIO_Port, ADC_ThrLD2_CS_Pin, GPIO_PIN_RESET);
for (delay_counter = 0u; delay_counter < 500u; ++delay_counter)
{
}
LL_SPI_Enable(SPI5);
delay_counter = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI5)) && (delay_counter <= 1000u))
{
++delay_counter;
}
LL_SPI_Disable(SPI5);
HAL_GPIO_WritePin(ADC_ThrLD2_CS_GPIO_Port, ADC_ThrLD2_CS_Pin, GPIO_PIN_SET);
result = LL_SPI_ReceiveData16(SPI5);
}
return result;
}
uint16_t adc_mux_process_internal_adc_step(uint8_t step)
{
uint16_t output = 0u;
switch (step)
{
case 0u:
HAL_ADC_Start(&hadc1);
break;
case 1u:
HAL_ADC_PollForConversion(&hadc1, 100u);
output = HAL_ADC_GetValue(&hadc1);
break;
case 2u:
HAL_ADC_Stop(&hadc1);
break;
case 3u:
HAL_ADC_Start(&hadc3);
break;
case 4u:
HAL_ADC_PollForConversion(&hadc3, 100u);
output = HAL_ADC_GetValue(&hadc3);
break;
case 5u:
HAL_ADC_Stop(&hadc3);
break;
default:
break;
}
return output;
}

14
App/Devices/adc_mux.h Normal file
View File

@ -0,0 +1,14 @@
/**
* @file adc_mux.h
* @brief Helpers for external photodiode ADCs and internal STM32 ADC channels.
*/
#ifndef ADC_MUX_H
#define ADC_MUX_H
#include <stdint.h>
uint16_t adc_mux_read_external_channel(uint8_t channel_index);
uint16_t adc_mux_process_internal_adc_step(uint8_t step);
#endif /* ADC_MUX_H */

View File

@ -0,0 +1,22 @@
/**
* @file board_handles.h
* @brief Extern declarations for CubeMX-generated peripheral handles.
*
* Architectural note:
* Device and service modules depend on hardware handles owned by `Src/main.c`.
* This header isolates those extern declarations so the App layer never has to
* pull application types back into the CubeMX-generated file.
*/
#ifndef BOARD_HANDLES_H
#define BOARD_HANDLES_H
#include "main.h"
extern ADC_HandleTypeDef hadc1;
extern ADC_HandleTypeDef hadc3;
extern SD_HandleTypeDef hsd1;
extern TIM_HandleTypeDef htim1;
extern UART_HandleTypeDef huart8;
#endif /* BOARD_HANDLES_H */

257
App/Devices/board_io.c Normal file
View File

@ -0,0 +1,257 @@
/**
* @file board_io.c
* @brief Board-specific GPIO and shared low-level control helpers.
*/
#include "board_io.h"
#define UI_LCD_CONTRAST_PWM_FREQUENCY_HZ 20000u
#define UI_LCD_CONTRAST_PWM_PERIOD_COUNTS 1000u
#define UI_LCD_CONTRAST_PWM_DUTY_PERMILLE 300u
static TIM_HandleTypeDef g_ui_lcd_contrast_pwm_timer;
static uint32_t board_io_get_apb1_timer_clock_hz(void);
static void board_io_init_lcd_contrast_pwm(void);
void board_io_enable_uart_rx_irq(void)
{
LL_USART_EnableIT_PE(USART1);
LL_USART_EnableIT_RXNE(USART1);
LL_USART_EnableIT_ERROR(USART1);
NVIC_SetPriority(USART1_IRQn, 0);
NVIC_EnableIRQ(USART1_IRQn);
}
void board_io_init_standalone_ui(void)
{
GPIO_InitTypeDef gpio_init = {0};
__HAL_RCC_GPIOG_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
HAL_GPIO_WritePin(UI_LCD_RS_GPIO_Port,
UI_LCD_RS_Pin | UI_LCD_E_Pin | UI_LCD_D4_Pin | UI_LCD_D5_Pin | UI_LCD_D6_Pin | UI_LCD_D7_Pin,
GPIO_PIN_RESET);
/*
* A fixed contrast pin is only a compromise until a potentiometer is
* added. Driving V0 low is the safest default for HD44780/SPLC780D-style
* modules because it makes characters visible instead of appearing blank.
*/
HAL_GPIO_WritePin(UI_LCD_V0_REF_GPIO_Port, UI_LCD_V0_REF_Pin, GPIO_PIN_RESET);
gpio_init.Pin = UI_LCD_RS_Pin | UI_LCD_E_Pin | UI_LCD_D4_Pin | UI_LCD_D5_Pin | UI_LCD_D6_Pin | UI_LCD_D7_Pin;
gpio_init.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init.Pull = GPIO_NOPULL;
gpio_init.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOG, &gpio_init);
gpio_init.Pin = UI_LCD_V0_REF_Pin;
gpio_init.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init.Pull = GPIO_NOPULL;
gpio_init.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(UI_LCD_V0_REF_GPIO_Port, &gpio_init);
gpio_init.Pin = UI_BUTTON_Pin;
gpio_init.Mode = GPIO_MODE_INPUT;
gpio_init.Pull = GPIO_PULLUP;
HAL_GPIO_Init(UI_BUTTON_GPIO_Port, &gpio_init);
board_io_init_lcd_contrast_pwm();
}
static uint32_t board_io_get_apb1_timer_clock_hz(void)
{
uint32_t pclk1_hz = HAL_RCC_GetPCLK1Freq();
if ((RCC->CFGR & RCC_CFGR_PPRE1) == RCC_CFGR_PPRE1_DIV1)
{
return pclk1_hz;
}
return pclk1_hz * 2u;
}
static void board_io_init_lcd_contrast_pwm(void)
{
GPIO_InitTypeDef gpio_init = {0};
TIM_OC_InitTypeDef pwm_channel = {0};
uint32_t timer_clock_hz = board_io_get_apb1_timer_clock_hz();
uint32_t prescaler_divisor = timer_clock_hz / (UI_LCD_CONTRAST_PWM_FREQUENCY_HZ * UI_LCD_CONTRAST_PWM_PERIOD_COUNTS);
uint32_t pulse_counts = ((UI_LCD_CONTRAST_PWM_PERIOD_COUNTS * UI_LCD_CONTRAST_PWM_DUTY_PERMILLE) + 999u) / 1000u;
if (prescaler_divisor == 0u)
{
prescaler_divisor = 1u;
}
if (pulse_counts >= UI_LCD_CONTRAST_PWM_PERIOD_COUNTS)
{
pulse_counts = UI_LCD_CONTRAST_PWM_PERIOD_COUNTS - 1u;
}
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_TIM4_CLK_ENABLE();
/*
* This PWM output is intended for the LCD contrast input (V0) through a
* simple RC low-pass filter. Keeping it local to board_io.c avoids
* spreading board wiring assumptions into higher-level UI code.
*/
gpio_init.Pin = UI_LCD_CONTRAST_PWM_Pin;
gpio_init.Mode = GPIO_MODE_AF_PP;
gpio_init.Pull = GPIO_NOPULL;
gpio_init.Speed = GPIO_SPEED_FREQ_LOW;
gpio_init.Alternate = GPIO_AF2_TIM4;
HAL_GPIO_Init(UI_LCD_CONTRAST_PWM_GPIO_Port, &gpio_init);
g_ui_lcd_contrast_pwm_timer.Instance = TIM4;
g_ui_lcd_contrast_pwm_timer.Init.Prescaler = (uint32_t)(prescaler_divisor - 1u);
g_ui_lcd_contrast_pwm_timer.Init.CounterMode = TIM_COUNTERMODE_UP;
g_ui_lcd_contrast_pwm_timer.Init.Period = UI_LCD_CONTRAST_PWM_PERIOD_COUNTS - 1u;
g_ui_lcd_contrast_pwm_timer.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
g_ui_lcd_contrast_pwm_timer.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_PWM_Init(&g_ui_lcd_contrast_pwm_timer) != HAL_OK)
{
return;
}
pwm_channel.OCMode = TIM_OCMODE_PWM1;
pwm_channel.Pulse = pulse_counts;
pwm_channel.OCPolarity = TIM_OCPOLARITY_HIGH;
pwm_channel.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&g_ui_lcd_contrast_pwm_timer, &pwm_channel, TIM_CHANNEL_3) != HAL_OK)
{
return;
}
(void)HAL_TIM_PWM_Start(&g_ui_lcd_contrast_pwm_timer, TIM_CHANNEL_3);
}
void board_io_reset_runtime_outputs(void)
{
HAL_GPIO_WritePin(EN_5V1_GPIO_Port, EN_5V1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(EN_5V2_GPIO_Port, EN_5V2_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LD1_EN_GPIO_Port, LD1_EN_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LD2_EN_GPIO_Port, LD2_EN_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(REF0_EN_GPIO_Port, REF0_EN_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(REF2_ON_GPIO_Port, REF2_ON_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(TECEN1_GPIO_Port, TECEN1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(TECEN2_GPIO_Port, TECEN2_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(TEC1_PD_GPIO_Port, TEC1_PD_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(TEC2_PD_GPIO_Port, TEC2_PD_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(ADC_MPD1_CS_GPIO_Port, ADC_MPD1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(ADC_MPD2_CS_GPIO_Port, ADC_MPD2_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(SPI4_CNV_GPIO_Port, SPI4_CNV_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(SPI5_CNV_GPIO_Port, SPI5_CNV_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_LD1_CS_GPIO_Port, DAC_LD1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_LD2_CS_GPIO_Port, DAC_LD2_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_TEC1_CS_GPIO_Port, DAC_TEC1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_TEC2_CS_GPIO_Port, DAC_TEC2_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(AD9833_CS_GPIO_Port, AD9833_CS_Pin, GPIO_PIN_SET);
}
void board_io_configure_spi2_mode(uint32_t polarity, uint32_t phase)
{
if (LL_SPI_IsEnabled(SPI2))
{
LL_SPI_Disable(SPI2);
}
LL_SPI_SetClockPolarity(SPI2, polarity);
LL_SPI_SetClockPhase(SPI2, phase);
if (!LL_SPI_IsEnabled(SPI2))
{
LL_SPI_Enable(SPI2);
}
}
bool board_io_is_usb_connected(void)
{
return HAL_GPIO_ReadPin(USB_FLAG_GPIO_Port, USB_FLAG_Pin) == GPIO_PIN_SET;
}
bool board_io_is_sd_card_present(void)
{
return HAL_GPIO_ReadPin(SDMMC1_EN_GPIO_Port, SDMMC1_EN_Pin) == GPIO_PIN_RESET;
}
bool board_io_is_standalone_ui_button_pressed(void)
{
return HAL_GPIO_ReadPin(UI_BUTTON_GPIO_Port, UI_BUTTON_Pin) == GPIO_PIN_RESET;
}
void board_io_set_supply_enabled(uint8_t supply_index, bool enabled)
{
GPIO_PinState pin_state = enabled ? GPIO_PIN_SET : GPIO_PIN_RESET;
if (supply_index == 1u)
{
HAL_GPIO_WritePin(EN_5V1_GPIO_Port, EN_5V1_Pin, pin_state);
}
else if (supply_index == 2u)
{
HAL_GPIO_WritePin(EN_5V2_GPIO_Port, EN_5V2_Pin, pin_state);
}
}
void board_io_set_laser_enabled(uint8_t laser_index, bool enabled)
{
GPIO_PinState pin_state = enabled ? GPIO_PIN_SET : GPIO_PIN_RESET;
if (laser_index == 1u)
{
HAL_GPIO_WritePin(LD1_EN_GPIO_Port, LD1_EN_Pin, pin_state);
}
else if (laser_index == 2u)
{
HAL_GPIO_WritePin(LD2_EN_GPIO_Port, LD2_EN_Pin, pin_state);
}
}
void board_io_set_reference_enabled(uint8_t reference_index, bool enabled)
{
GPIO_PinState pin_state = enabled ? GPIO_PIN_SET : GPIO_PIN_RESET;
if (reference_index == 1u)
{
HAL_GPIO_WritePin(REF0_EN_GPIO_Port, REF0_EN_Pin, pin_state);
}
else if (reference_index == 2u)
{
HAL_GPIO_WritePin(REF2_ON_GPIO_Port, REF2_ON_Pin, pin_state);
}
}
void board_io_set_tec_channel_enabled(uint8_t tec_index, bool enabled)
{
GPIO_PinState pin_state = enabled ? GPIO_PIN_SET : GPIO_PIN_RESET;
if (tec_index == 1u)
{
HAL_GPIO_WritePin(TEC1_PD_GPIO_Port, TEC1_PD_Pin, pin_state);
HAL_GPIO_WritePin(TECEN1_GPIO_Port, TECEN1_Pin, pin_state);
}
else if (tec_index == 2u)
{
HAL_GPIO_WritePin(TEC2_PD_GPIO_Port, TEC2_PD_Pin, pin_state);
HAL_GPIO_WritePin(TECEN2_GPIO_Port, TECEN2_Pin, pin_state);
}
}
void board_io_write_signal(GPIO_TypeDef *port, uint16_t pin, bool level_high)
{
HAL_GPIO_WritePin(port, pin, level_high ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
void board_io_toggle_debug_pin(void)
{
HAL_GPIO_TogglePin(TEST_01_GPIO_Port, TEST_01_Pin);
}

33
App/Devices/board_io.h Normal file
View File

@ -0,0 +1,33 @@
/**
* @file board_io.h
* @brief Board-specific GPIO and low-level control helpers.
*
* Architectural note:
* This module groups direct GPIO manipulations that are shared across several
* devices and services. Doing so keeps register-driving code localised and
* prevents high-level services from depending on scattered pin knowledge.
*/
#ifndef BOARD_IO_H
#define BOARD_IO_H
#include <stdbool.h>
#include <stdint.h>
#include "main.h"
void board_io_enable_uart_rx_irq(void);
void board_io_init_standalone_ui(void);
void board_io_reset_runtime_outputs(void);
void board_io_configure_spi2_mode(uint32_t polarity, uint32_t phase);
bool board_io_is_usb_connected(void);
bool board_io_is_sd_card_present(void);
bool board_io_is_standalone_ui_button_pressed(void);
void board_io_set_supply_enabled(uint8_t supply_index, bool enabled);
void board_io_set_laser_enabled(uint8_t laser_index, bool enabled);
void board_io_set_reference_enabled(uint8_t reference_index, bool enabled);
void board_io_set_tec_channel_enabled(uint8_t tec_index, bool enabled);
void board_io_toggle_debug_pin(void);
void board_io_write_signal(GPIO_TypeDef *port, uint16_t pin, bool level_high);
#endif /* BOARD_IO_H */

View File

@ -0,0 +1,83 @@
/**
* @file ds1809_device.c
* @brief DS1809 pulse-based control helper.
*/
#include "ds1809_device.h"
#include "main.h"
#define DS1809_POSITION_COUNT 64u
#define DS1809_CPU_PULSE_WIDTH_MS 2u
#define DS1809_CPU_INTER_PULSE_HIGH_MS 2u
#define DS1809_CONTROL_PORT_READY_DELAY_MS 10u
static void ds1809_drive_idle_high(void)
{
HAL_GPIO_WritePin(DS1809_UC_GPIO_Port, DS1809_UC_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DS1809_DC_GPIO_Port, DS1809_DC_Pin, GPIO_PIN_SET);
}
static void ds1809_pulse_direction(uint8_t increment,
uint8_t decrement,
uint16_t count,
uint16_t low_time_ms,
uint16_t high_time_ms)
{
uint16_t pulse_index;
if ((count == 0u) || ((increment == 0u) && (decrement == 0u)))
{
ds1809_drive_idle_high();
return;
}
ds1809_drive_idle_high();
for (pulse_index = 0u; pulse_index < count; ++pulse_index)
{
if (increment != 0u)
{
HAL_GPIO_WritePin(DS1809_UC_GPIO_Port, DS1809_UC_Pin, GPIO_PIN_RESET);
}
if (decrement != 0u)
{
HAL_GPIO_WritePin(DS1809_DC_GPIO_Port, DS1809_DC_Pin, GPIO_PIN_RESET);
}
HAL_Delay(low_time_ms);
ds1809_drive_idle_high();
HAL_Delay(high_time_ms);
}
}
void ds1809_pulse(uint8_t increment, uint8_t decrement, uint16_t count, uint16_t pulse_ms)
{
if (pulse_ms < DS1809_CPU_PULSE_WIDTH_MS)
{
pulse_ms = DS1809_CPU_PULSE_WIDTH_MS;
}
ds1809_pulse_direction(increment, decrement, count, pulse_ms, pulse_ms);
}
void ds1809_apply_position_from_min(uint8_t position_from_min)
{
/*
* DS1809 CPU-driven timing requirements from the local datasheet:
* - a pulse must stay low for longer than 1 ms to count as a step;
* - repetitive pulses need at least 1 ms of high time between steps;
* - the control inputs are locked out for at least 10 ms after power-up.
*
* Use 2 ms low/high timing for margin, always drive to the RL end first,
* then walk back up to the requested absolute position.
*/
if (position_from_min >= DS1809_POSITION_COUNT)
{
position_from_min = (uint8_t)(DS1809_POSITION_COUNT - 1u);
}
HAL_Delay(DS1809_CONTROL_PORT_READY_DELAY_MS);
ds1809_pulse_direction(0u, 1u, DS1809_POSITION_COUNT, DS1809_CPU_PULSE_WIDTH_MS, DS1809_CPU_INTER_PULSE_HIGH_MS);
ds1809_pulse_direction(1u, 0u, position_from_min, DS1809_CPU_PULSE_WIDTH_MS, DS1809_CPU_INTER_PULSE_HIGH_MS);
}

View File

@ -0,0 +1,32 @@
/**
* @file ds1809_device.h
* @brief DS1809 pulse-based control helper.
*/
#ifndef DS1809_DEVICE_H
#define DS1809_DEVICE_H
#include <stdint.h>
/**
* @brief Send relative UC/DC pulses using the legacy serial-command semantics.
*
* @param increment Non-zero to pulse the UC pin.
* @param decrement Non-zero to pulse the DC pin.
* @param count Number of pulses to issue.
* @param pulse_ms Low time and inter-pulse high time, in milliseconds.
*/
void ds1809_pulse(uint8_t increment, uint8_t decrement, uint16_t count, uint16_t pulse_ms);
/**
* @brief Force the DS1809 to a known absolute position above the minimum tap.
*
* The helper first overdrives the wiper all the way down to the RL terminal and
* then steps back up to the requested tap. This makes the standalone profile
* path deterministic even though the DS1809 serial command itself is relative.
*
* @param position_from_min Number of upward steps above the minimum tap.
*/
void ds1809_apply_position_from_min(uint8_t position_from_min);
#endif /* DS1809_DEVICE_H */

103
App/Devices/laser_dac.c Normal file
View File

@ -0,0 +1,103 @@
/**
* @file laser_dac.c
* @brief External DAC access for laser-current and TEC channels.
*/
#include "laser_dac.h"
#include "board_io.h"
#include "main.h"
void laser_dac_write_channel(uint8_t channel, uint16_t value)
{
uint32_t timeout;
if ((channel == 1u) || (channel == 3u))
{
board_io_configure_spi2_mode(LL_SPI_POLARITY_HIGH, LL_SPI_PHASE_2EDGE);
HAL_GPIO_WritePin(AD9102_CS_GPIO_Port, AD9102_CS_Pin, GPIO_PIN_SET);
}
switch (channel)
{
case 1u:
HAL_GPIO_WritePin(DAC_LD1_CS_GPIO_Port, DAC_LD1_CS_Pin, GPIO_PIN_RESET);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_TXE(SPI2)) && (timeout <= 500u))
{
++timeout;
}
LL_SPI_TransmitData16(SPI2, value);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI2)) && (timeout <= 500u))
{
++timeout;
}
(void)SPI2->DR;
break;
case 2u:
if (!LL_SPI_IsEnabled(SPI6))
{
LL_SPI_Enable(SPI6);
}
HAL_GPIO_WritePin(DAC_LD2_CS_GPIO_Port, DAC_LD2_CS_Pin, GPIO_PIN_RESET);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_TXE(SPI6)) && (timeout <= 500u))
{
++timeout;
}
LL_SPI_TransmitData16(SPI6, value);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI6)) && (timeout <= 500u))
{
++timeout;
}
(void)SPI6->DR;
break;
case 3u:
HAL_GPIO_WritePin(DAC_TEC1_CS_GPIO_Port, DAC_TEC1_CS_Pin, GPIO_PIN_RESET);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_TXE(SPI2)) && (timeout <= 500u))
{
++timeout;
}
LL_SPI_TransmitData16(SPI2, value);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI2)) && (timeout <= 500u))
{
++timeout;
}
(void)SPI2->DR;
break;
case 4u:
if (!LL_SPI_IsEnabled(SPI6))
{
LL_SPI_Enable(SPI6);
}
HAL_GPIO_WritePin(DAC_TEC2_CS_GPIO_Port, DAC_TEC2_CS_Pin, GPIO_PIN_RESET);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_TXE(SPI6)) && (timeout <= 500u))
{
++timeout;
}
LL_SPI_TransmitData16(SPI6, value);
timeout = 0u;
while ((!LL_SPI_IsActiveFlag_RXNE(SPI6)) && (timeout <= 500u))
{
++timeout;
}
(void)SPI6->DR;
break;
default:
break;
}
HAL_GPIO_WritePin(DAC_LD1_CS_GPIO_Port, DAC_LD1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_LD2_CS_GPIO_Port, DAC_LD2_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_TEC1_CS_GPIO_Port, DAC_TEC1_CS_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(DAC_TEC2_CS_GPIO_Port, DAC_TEC2_CS_Pin, GPIO_PIN_SET);
}

13
App/Devices/laser_dac.h Normal file
View File

@ -0,0 +1,13 @@
/**
* @file laser_dac.h
* @brief Drivers for the external laser-current and TEC DAC channels.
*/
#ifndef LASER_DAC_H
#define LASER_DAC_H
#include <stdint.h>
void laser_dac_write_channel(uint8_t channel, uint16_t value);
#endif /* LASER_DAC_H */

View File

@ -0,0 +1,199 @@
/**
* @file lcd1602_display.c
* @brief Minimal SPLC780D/HD44780-compatible LCD1602 driver in 4-bit mode.
*
* Architectural note:
* The chosen wiring keeps the whole standalone UI on GPIOG. This driver uses
* write-only transfers with fixed delays, which keeps the implementation
* compact and avoids a bidirectional R/W pin or extra state.
*/
#include "lcd1602_display.h"
#include <stdbool.h>
#include <stddef.h>
#include "main.h"
#define LCD1602_COLUMNS 16u
#define LCD1602_LINE_1_ADDRESS 0x00u
#define LCD1602_LINE_2_ADDRESS 0x40u
#define LCD1602_COMMAND_DELAY_US 50u
#define LCD1602_CLEAR_DELAY_US 2000u
#define LCD1602_ENABLE_PULSE_US 1u
#define LCD1602_POWER_ON_DELAY_US 50000u
#define LCD1602_DATA_MASK (UI_LCD_D4_Pin | UI_LCD_D5_Pin | UI_LCD_D6_Pin | UI_LCD_D7_Pin)
static uint8_t g_cycle_counter_available = 0u;
static void lcd1602_enable_cycle_counter(void);
static void lcd1602_delay_us(uint32_t delay_us);
static void lcd1602_write_nibble(uint8_t nibble);
static void lcd1602_pulse_enable(void);
static void lcd1602_write_byte(bool is_data, uint8_t value);
static void lcd1602_write_command(uint8_t command);
static void lcd1602_write_data(uint8_t value);
static void lcd1602_set_cursor(uint8_t address);
static void lcd1602_write_padded_line(const char *text);
void lcd1602_display_init(void)
{
lcd1602_enable_cycle_counter();
HAL_GPIO_WritePin(UI_LCD_RS_GPIO_Port, UI_LCD_RS_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(UI_LCD_E_GPIO_Port, UI_LCD_E_Pin, GPIO_PIN_RESET);
UI_LCD_D4_GPIO_Port->BSRR = ((uint32_t)LCD1602_DATA_MASK) << 16u;
lcd1602_delay_us(LCD1602_POWER_ON_DELAY_US);
lcd1602_write_nibble(0x03u);
lcd1602_delay_us(5000u);
lcd1602_write_nibble(0x03u);
lcd1602_delay_us(150u);
lcd1602_write_nibble(0x03u);
lcd1602_delay_us(150u);
lcd1602_write_nibble(0x02u);
lcd1602_delay_us(LCD1602_COMMAND_DELAY_US);
lcd1602_write_command(0x28u);
lcd1602_write_command(0x08u);
lcd1602_write_command(0x01u);
lcd1602_write_command(0x06u);
lcd1602_write_command(0x0Cu);
}
void lcd1602_display_set_lines(const char *line1, const char *line2)
{
lcd1602_set_cursor(LCD1602_LINE_1_ADDRESS);
lcd1602_write_padded_line(line1);
lcd1602_set_cursor(LCD1602_LINE_2_ADDRESS);
lcd1602_write_padded_line(line2);
}
static void lcd1602_enable_cycle_counter(void)
{
uint32_t start_cycles;
uint32_t spin;
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0u;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
start_cycles = DWT->CYCCNT;
for (spin = 0u; spin < 64u; ++spin)
{
__NOP();
}
/*
* DWT-based microsecond delays are convenient, but they are not
* guaranteed to be available in every production boot/debug state.
* Falling back to HAL_Delay() keeps the LCD optional rather than
* letting the standalone UI block the whole firmware startup.
*/
g_cycle_counter_available = (DWT->CYCCNT != start_cycles) ? 1u : 0u;
}
static void lcd1602_delay_us(uint32_t delay_us)
{
if (delay_us == 0u)
{
return;
}
if (g_cycle_counter_available != 0u)
{
uint32_t start_cycles = DWT->CYCCNT;
uint32_t target_cycles = delay_us * (SystemCoreClock / 1000000u);
while ((DWT->CYCCNT - start_cycles) < target_cycles)
{
}
return;
}
while (delay_us > 1000u)
{
HAL_Delay(1u);
delay_us -= 1000u;
}
HAL_Delay(1u);
}
static void lcd1602_write_nibble(uint8_t nibble)
{
uint32_t bsrr = ((uint32_t)LCD1602_DATA_MASK) << 16u;
if ((nibble & 0x01u) != 0u)
{
bsrr |= UI_LCD_D4_Pin;
}
if ((nibble & 0x02u) != 0u)
{
bsrr |= UI_LCD_D5_Pin;
}
if ((nibble & 0x04u) != 0u)
{
bsrr |= UI_LCD_D6_Pin;
}
if ((nibble & 0x08u) != 0u)
{
bsrr |= UI_LCD_D7_Pin;
}
UI_LCD_D4_GPIO_Port->BSRR = bsrr;
lcd1602_pulse_enable();
}
static void lcd1602_pulse_enable(void)
{
HAL_GPIO_WritePin(UI_LCD_E_GPIO_Port, UI_LCD_E_Pin, GPIO_PIN_SET);
lcd1602_delay_us(LCD1602_ENABLE_PULSE_US);
HAL_GPIO_WritePin(UI_LCD_E_GPIO_Port, UI_LCD_E_Pin, GPIO_PIN_RESET);
lcd1602_delay_us(LCD1602_COMMAND_DELAY_US);
}
static void lcd1602_write_byte(bool is_data, uint8_t value)
{
HAL_GPIO_WritePin(UI_LCD_RS_GPIO_Port, UI_LCD_RS_Pin, is_data ? GPIO_PIN_SET : GPIO_PIN_RESET);
lcd1602_write_nibble((uint8_t)(value >> 4u));
lcd1602_write_nibble((uint8_t)(value & 0x0Fu));
}
static void lcd1602_write_command(uint8_t command)
{
lcd1602_write_byte(false, command);
if ((command == 0x01u) || (command == 0x02u))
{
lcd1602_delay_us(LCD1602_CLEAR_DELAY_US);
}
}
static void lcd1602_write_data(uint8_t value)
{
lcd1602_write_byte(true, value);
}
static void lcd1602_set_cursor(uint8_t address)
{
lcd1602_write_command((uint8_t)(0x80u | address));
}
static void lcd1602_write_padded_line(const char *text)
{
uint8_t index;
uint8_t value;
for (index = 0u; index < LCD1602_COLUMNS; ++index)
{
value = ' ';
if ((text != NULL) && (text[index] != '\0'))
{
value = (uint8_t)text[index];
}
lcd1602_write_data(value);
}
}

View File

@ -0,0 +1,19 @@
/**
* @file lcd1602_display.h
* @brief Minimal SPLC780D/HD44780-compatible LCD1602 driver in 4-bit mode.
*
* Architectural note:
* This module owns the LCD bus timing and character writes for the standalone
* front panel. High-level services format text elsewhere and call this driver
* only with already-prepared 16-character lines.
*/
#ifndef LCD1602_DISPLAY_H
#define LCD1602_DISPLAY_H
#include <stdint.h>
void lcd1602_display_init(void);
void lcd1602_display_set_lines(const char *line1, const char *line2);
#endif /* LCD1602_DISPLAY_H */

View File

@ -0,0 +1,45 @@
/**
* @file stm32_dac_output.c
* @brief STM32 internal DAC helper for the PA4 analogue output.
*/
#include "stm32_dac_output.h"
#include "main.h"
#define STM32_DAC_MAX_CODE 4095u
void stm32_dac_output_init(void)
{
GPIO_InitTypeDef gpio_init = {0};
__HAL_RCC_DAC_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
gpio_init.Pin = GPIO_PIN_4;
gpio_init.Mode = GPIO_MODE_ANALOG;
gpio_init.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &gpio_init);
DAC->CR &= ~(DAC_CR_EN1 | DAC_CR_TEN1 | DAC_CR_DMAEN1);
DAC->DHR12R1 = 0u;
}
void stm32_dac_output_set(uint16_t dac_code, uint8_t enabled)
{
if (dac_code > STM32_DAC_MAX_CODE)
{
dac_code = STM32_DAC_MAX_CODE;
}
DAC->DHR12R1 = dac_code;
if (enabled != 0u)
{
DAC->CR |= DAC_CR_EN1;
}
else
{
DAC->CR &= ~DAC_CR_EN1;
}
}

View File

@ -0,0 +1,14 @@
/**
* @file stm32_dac_output.h
* @brief STM32 internal DAC helper for PA4 output generation.
*/
#ifndef STM32_DAC_OUTPUT_H
#define STM32_DAC_OUTPUT_H
#include <stdint.h>
void stm32_dac_output_init(void);
void stm32_dac_output_set(uint16_t dac_code, uint8_t enabled);
#endif /* STM32_DAC_OUTPUT_H */

View File

@ -0,0 +1,68 @@
/**
* @file uart_transport.c
* @brief USART1 transmit helpers used by the application core.
*/
#include "uart_transport.h"
#include "main.h"
static volatile uint8_t g_dma_busy = 0u;
void uart_transport_init(void)
{
LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_7);
LL_DMA_ClearFlag_TC7(DMA2);
LL_DMA_ClearFlag_TE7(DMA2);
LL_USART_EnableDMAReq_TX(USART1);
LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_7);
LL_DMA_EnableIT_TE(DMA2, LL_DMA_STREAM_7);
g_dma_busy = 0u;
}
void uart_transport_reset(void)
{
g_dma_busy = 0u;
}
void uart_transport_send_blocking(const uint8_t *data, uint16_t size)
{
uint16_t index;
for (index = 0u; index < size; ++index)
{
while (!LL_USART_IsActiveFlag_TXE(USART1))
{
}
LL_USART_TransmitData8(USART1, data[index]);
}
}
void uart_transport_send_dma(const uint8_t *data, uint16_t size)
{
while (g_dma_busy != 0u)
{
}
LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_7);
LL_DMA_ClearFlag_TC7(DMA2);
LL_DMA_ClearFlag_TE7(DMA2);
LL_DMA_ConfigAddresses(DMA2,
LL_DMA_STREAM_7,
(uint32_t)data,
LL_USART_DMA_GetRegAddr(USART1, LL_USART_DMA_REG_DATA_TRANSMIT),
LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_7));
LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_7, size);
g_dma_busy = 1u;
LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_7);
}
void uart_transport_mark_dma_complete(void)
{
g_dma_busy = 0u;
}
bool uart_transport_is_dma_busy(void)
{
return g_dma_busy != 0u;
}

View File

@ -0,0 +1,19 @@
/**
* @file uart_transport.h
* @brief USART1 transmit helpers used by the application core.
*/
#ifndef UART_TRANSPORT_H
#define UART_TRANSPORT_H
#include <stdbool.h>
#include <stdint.h>
void uart_transport_init(void);
void uart_transport_reset(void);
void uart_transport_send_blocking(const uint8_t *data, uint16_t size);
void uart_transport_send_dma(const uint8_t *data, uint16_t size);
void uart_transport_mark_dma_complete(void);
bool uart_transport_is_dma_busy(void);
#endif /* UART_TRANSPORT_H */

344
App/Models/app_types.h Normal file
View File

@ -0,0 +1,344 @@
/**
* @file app_types.h
* @brief Shared application-level types, constants, and protocol identifiers.
*
* Architectural note:
* This header owns the firmware vocabulary used by the modular App layer.
* CubeMX-generated headers keep only board-level definitions, while every
* application module includes these types to communicate through explicit
* interfaces instead of ad-hoc globals and scattered macros.
*/
#ifndef APP_TYPES_H
#define APP_TYPES_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/** @brief Total number of 16-bit words in a telemetry frame, including header. */
#define APP_TELEMETRY_WORD_COUNT 15u
/** @brief Total number of bytes in a telemetry frame. */
#define APP_TELEMETRY_FRAME_BYTES 30u
/** @brief Maximum number of payload words stored by the UART protocol parser. */
#define APP_PROTOCOL_MAX_PAYLOAD_WORDS 15u
/** @brief Maximum packet size handled by the UART protocol parser. */
#define APP_PROTOCOL_MAX_PACKET_BYTES 32u
/** @brief Maximum number of visible characters allowed in a saved profile name. */
#define APP_PROFILE_NAME_MAX_CHARACTERS 16u
/** @brief Maximum profile display-name length, including the terminating NUL. */
#define APP_PROFILE_NAME_LENGTH (APP_PROFILE_NAME_MAX_CHARACTERS + 1u)
/** @brief Maximum stored path length for profile and waveform files. */
#define APP_PROFILE_PATH_LENGTH 96u
/** @brief Future profile list file for SD-card based standalone operation. */
#define APP_STORAGE_PROFILE_INDEX_FILE "profiles.csv"
/** @brief UART header for the host work-configuration packet. */
#define APP_PACKET_HEADER_WORK_CONFIG 0x1111u
/** @brief UART header for the "return to defaults" command. */
#define APP_PACKET_HEADER_DEFAULTS 0x2222u
/** @brief UART header for the "transmit live telemetry" command. */
#define APP_PACKET_HEADER_TX_CURRENT 0x4444u
/** @brief UART header for the "query state" command. */
#define APP_PACKET_HEADER_QUERY_STATE 0x6666u
/** @brief UART header for profile-save control packets. */
#define APP_PACKET_HEADER_PROFILE_SAVE_CONTROL 0x7777u
/** @brief UART header for the AD9102 control packet. */
#define APP_PACKET_HEADER_AD9102_CONTROL 0x8888u
/** @brief UART header for the AD9833 control packet. */
#define APP_PACKET_HEADER_AD9833_CONTROL 0x9999u
/** @brief UART header for the DS1809 control packet. */
#define APP_PACKET_HEADER_DS1809_CONTROL 0xAAAAu
/** @brief UART header for the STM32 internal DAC control packet. */
#define APP_PACKET_HEADER_STM32_DAC_CONTROL 0xBBBBu
/** @brief UART header for the AD9102 waveform upload control packet. */
#define APP_PACKET_HEADER_AD9102_WAVE_CONTROL 0xCCCCu
/** @brief UART header for the AD9102 waveform upload data packet. */
#define APP_PACKET_HEADER_AD9102_WAVE_DATA 0xDDDDu
/** @brief UART header for profile-save data packets. */
#define APP_PACKET_HEADER_PROFILE_SAVE_DATA 0xEEEEu
/** @brief Packet size of the host work-configuration message, in bytes. */
#define APP_PACKET_BYTES_WORK_CONFIG 30u
/** @brief Packet size of short control messages, in bytes. */
#define APP_PACKET_BYTES_SHORT_CONTROL 10u
/** @brief Packet size of AD9102 waveform-data messages, in bytes. */
#define APP_PACKET_BYTES_AD9102_WAVE_DATA 30u
/** @brief Packet size of profile-save control messages, in bytes. */
#define APP_PACKET_BYTES_PROFILE_SAVE_CONTROL 30u
/** @brief Packet size of profile-save data messages, in bytes. */
#define APP_PACKET_BYTES_PROFILE_SAVE_DATA 30u
/** @brief Number of payload words carried by the host work packet. */
#define APP_PACKET_WORDS_WORK_CONFIG 14u
/** @brief Number of payload words carried by short control packets. */
#define APP_PACKET_WORDS_SHORT_CONTROL 4u
/** @brief Number of payload words carried by AD9102 waveform-data packets. */
#define APP_PACKET_WORDS_AD9102_WAVE_DATA 14u
/** @brief Number of payload words carried by profile-save control packets. */
#define APP_PACKET_WORDS_PROFILE_SAVE_CONTROL 14u
/** @brief Number of payload words carried by profile-save data packets. */
#define APP_PACKET_WORDS_PROFILE_SAVE_DATA 14u
/** @brief First status byte flag for SD-card related errors. */
#define APP_STATUS_FLAG_SD_ERROR 0x01u
/** @brief First status byte flag for UART framing/header errors. */
#define APP_STATUS_FLAG_UART_ERROR 0x02u
/** @brief First status byte flag for checksum or payload validation errors. */
#define APP_STATUS_FLAG_UART_DECODE_ERROR 0x04u
/** @brief First status byte flag for TEC1-related runtime errors. */
#define APP_STATUS_FLAG_TEC1_ERROR 0x08u
/** @brief First status byte flag for TEC2-related runtime errors. */
#define APP_STATUS_FLAG_TEC2_ERROR 0x10u
/** @brief First status byte flag for default/reset handling errors. */
#define APP_STATUS_FLAG_DEFAULT_ERROR 0x20u
/** @brief First status byte flag for AD9102 configuration/upload errors. */
#define APP_STATUS_FLAG_AD9102_ERROR 0x80u
/** @brief AD9102 control flag: enable or disable output. */
#define APP_AD9102_FLAG_ENABLE 0x0001u
/** @brief AD9102 control flag: select triangle instead of saw ramp. */
#define APP_AD9102_FLAG_TRIANGLE 0x0002u
/** @brief AD9102 control flag: use SRAM-backed waveform path. */
#define APP_AD9102_FLAG_SRAM 0x0004u
/** @brief AD9102 control flag: use amplitude/sample-count short format. */
#define APP_AD9102_FLAG_SRAM_FORMAT_ALT 0x0008u
/** @brief AD9833 control flag: enable or disable output. */
#define APP_AD9833_FLAG_ENABLE 0x0001u
/** @brief AD9833 control flag: select triangle mode. */
#define APP_AD9833_FLAG_TRIANGLE 0x0002u
/** @brief DS1809 control flag: pulse the "up" control line. */
#define APP_DS1809_FLAG_INCREMENT 0x0001u
/** @brief DS1809 control flag: pulse the "down" control line. */
#define APP_DS1809_FLAG_DECREMENT 0x0002u
/** @brief STM32 DAC control flag: enable output buffer/channel. */
#define APP_STM32_DAC_FLAG_ENABLE 0x0001u
/** @brief Op-code used to begin a custom AD9102 waveform upload. */
#define APP_AD9102_WAVE_OPCODE_BEGIN 0x0001u
/** @brief Op-code used to commit a custom AD9102 waveform upload. */
#define APP_AD9102_WAVE_OPCODE_COMMIT 0x0002u
/** @brief Op-code used to cancel a custom AD9102 waveform upload. */
#define APP_AD9102_WAVE_OPCODE_CANCEL 0x0003u
/** @brief Op-code used to begin a streamed profile-save session. */
#define APP_PROFILE_SAVE_OPCODE_BEGIN 0x0001u
/** @brief Op-code used to commit a streamed profile-save session. */
#define APP_PROFILE_SAVE_OPCODE_COMMIT 0x0002u
/** @brief Op-code used to cancel a streamed profile-save session. */
#define APP_PROFILE_SAVE_OPCODE_CANCEL 0x0003u
/** @brief Section id used for profile INI text chunks during profile save. */
#define APP_PROFILE_SAVE_SECTION_PROFILE_TEXT 0x0001u
/** @brief Section id used for optional waveform CSV chunks during profile save. */
#define APP_PROFILE_SAVE_SECTION_WAVEFORM_TEXT 0x0002u
/** @brief Maximum number of raw data bytes carried by one profile-save data packet. */
#define APP_PROFILE_SAVE_MAX_DATA_BYTES_PER_PACKET 22u
/**
* @brief High-level runtime modes executed by the application core.
*/
typedef enum app_mode_t {
APP_MODE_IDLE = 0,
APP_MODE_WORK,
APP_MODE_TX_CURRENT,
APP_MODE_ERROR
} app_mode_t;
/**
* @brief Deferred application events injected by future UI/GPIO frontends.
*/
typedef enum app_event_t {
APP_EVENT_NONE = 0,
APP_EVENT_PROFILE_NEXT
} app_event_t;
/**
* @brief Internal transmission slots queued by the application core.
*/
typedef enum app_tx_request_t {
APP_TX_REQUEST_NONE = 0,
APP_TX_REQUEST_STATUS,
APP_TX_REQUEST_CURRENT_FRAME
} app_tx_request_t;
/**
* @brief Packet kinds emitted by the UART protocol parser.
*/
typedef enum app_packet_kind_t {
APP_PACKET_KIND_NONE = 0,
APP_PACKET_KIND_WORK_CONFIG,
APP_PACKET_KIND_DEFAULTS,
APP_PACKET_KIND_TX_CURRENT,
APP_PACKET_KIND_QUERY_STATE,
APP_PACKET_KIND_PROFILE_SAVE_CONTROL,
APP_PACKET_KIND_AD9102_CONTROL,
APP_PACKET_KIND_AD9833_CONTROL,
APP_PACKET_KIND_DS1809_CONTROL,
APP_PACKET_KIND_STM32_DAC_CONTROL,
APP_PACKET_KIND_AD9102_WAVE_CONTROL,
APP_PACKET_KIND_AD9102_WAVE_DATA,
APP_PACKET_KIND_PROFILE_SAVE_DATA
} app_packet_kind_t;
/**
* @brief User-visible signal-generation mode for the AD9102 path.
*/
typedef enum waveform_mode_t {
WAVEFORM_MODE_SAW = 0,
WAVEFORM_MODE_SRAM_GENERATED,
WAVEFORM_MODE_SRAM_CUSTOM
} waveform_mode_t;
/**
* @brief Packed enable flags and misc runtime settings for the board.
*/
typedef struct work_config_t {
uint8_t work_enabled;
uint8_t supply_5v1_enabled;
uint8_t supply_5v2_enabled;
uint8_t laser1_enabled;
uint8_t laser2_enabled;
uint8_t reference1_enabled;
uint8_t reference2_enabled;
uint8_t tec1_enabled;
uint8_t tec2_enabled;
uint8_t temp_sensor1_enabled;
uint8_t temp_sensor2_enabled;
uint8_t pid1_from_host;
uint8_t pid2_from_host;
uint16_t averages;
uint16_t message_id;
} work_config_t;
/**
* @brief Runtime configuration of one laser channel.
*/
typedef struct laser_channel_config_t {
uint16_t target_temperature_raw;
float pid_p;
float pid_i;
uint16_t current_raw;
} laser_channel_config_t;
/**
* @brief Mutable per-channel runtime state derived from sensor feedback.
*/
typedef struct laser_runtime_t {
uint16_t current_temperature_raw;
float integral_error;
uint16_t power_raw;
} laser_runtime_t;
/**
* @brief Normalised configuration for AD9102 waveform control.
*/
typedef struct waveform_config_t {
waveform_mode_t mode;
uint8_t enabled;
uint8_t triangle;
uint8_t saw_step;
uint8_t pat_period_base;
uint16_t pat_period;
uint16_t sample_count;
uint8_t hold_cycles;
uint16_t amplitude;
char source_path[APP_PROFILE_PATH_LENGTH];
} waveform_config_t;
/**
* @brief AD9833 generator configuration mirrored from the serial-control path.
*/
typedef struct ad9833_config_t {
uint8_t enabled;
uint8_t triangle;
uint32_t frequency_word;
} ad9833_config_t;
/**
* @brief STM32 internal DAC configuration mirrored from the serial-control path.
*/
typedef struct stm32_dac_config_t {
uint8_t enabled;
uint16_t code;
} stm32_dac_config_t;
/**
* @brief Absolute DS1809 target expressed as a number of steps above the minimum tap.
*/
typedef struct ds1809_config_t {
bool apply_position;
uint8_t position_from_min;
} ds1809_config_t;
/**
* @brief Standalone profile model loaded from SD-card configuration.
*/
typedef struct profile_t {
char display_name[APP_PROFILE_NAME_LENGTH];
char profile_path[APP_PROFILE_PATH_LENGTH];
char waveform_path[APP_PROFILE_PATH_LENGTH];
bool auto_run;
bool boot_enabled;
work_config_t work_config;
laser_channel_config_t laser_channels[2];
waveform_config_t waveform;
ad9833_config_t ad9833;
stm32_dac_config_t stm32_dac;
ds1809_config_t ds1809;
} profile_t;
/**
* @brief Telemetry frame sent over UART.
*/
typedef struct telemetry_frame_t {
uint16_t words[APP_TELEMETRY_WORD_COUNT];
} telemetry_frame_t;
#endif /* APP_TYPES_H */

View File

@ -0,0 +1,162 @@
/**
* @file app_uart_protocol.c
* @brief Table-driven incremental UART packet parser and checksum helpers.
*/
#include "app_uart_protocol.h"
#include <string.h>
static const app_packet_descriptor_t g_packet_descriptors[] = {
{APP_PACKET_HEADER_WORK_CONFIG, APP_PACKET_BYTES_WORK_CONFIG, APP_PACKET_WORDS_WORK_CONFIG, 13, APP_PACKET_KIND_WORK_CONFIG},
{APP_PACKET_HEADER_DEFAULTS, 2u, 0u, -1, APP_PACKET_KIND_DEFAULTS},
{APP_PACKET_HEADER_TX_CURRENT, 2u, 0u, -1, APP_PACKET_KIND_TX_CURRENT},
{APP_PACKET_HEADER_QUERY_STATE, 2u, 0u, -1, APP_PACKET_KIND_QUERY_STATE},
{APP_PACKET_HEADER_PROFILE_SAVE_CONTROL, APP_PACKET_BYTES_PROFILE_SAVE_CONTROL, APP_PACKET_WORDS_PROFILE_SAVE_CONTROL, 13, APP_PACKET_KIND_PROFILE_SAVE_CONTROL},
{APP_PACKET_HEADER_AD9102_CONTROL, APP_PACKET_BYTES_SHORT_CONTROL, APP_PACKET_WORDS_SHORT_CONTROL, 3, APP_PACKET_KIND_AD9102_CONTROL},
{APP_PACKET_HEADER_AD9833_CONTROL, APP_PACKET_BYTES_SHORT_CONTROL, APP_PACKET_WORDS_SHORT_CONTROL, 3, APP_PACKET_KIND_AD9833_CONTROL},
{APP_PACKET_HEADER_DS1809_CONTROL, APP_PACKET_BYTES_SHORT_CONTROL, APP_PACKET_WORDS_SHORT_CONTROL, 3, APP_PACKET_KIND_DS1809_CONTROL},
{APP_PACKET_HEADER_STM32_DAC_CONTROL, APP_PACKET_BYTES_SHORT_CONTROL, APP_PACKET_WORDS_SHORT_CONTROL, 3, APP_PACKET_KIND_STM32_DAC_CONTROL},
{APP_PACKET_HEADER_AD9102_WAVE_CONTROL, APP_PACKET_BYTES_SHORT_CONTROL, APP_PACKET_WORDS_SHORT_CONTROL, 3, APP_PACKET_KIND_AD9102_WAVE_CONTROL},
{APP_PACKET_HEADER_AD9102_WAVE_DATA, APP_PACKET_BYTES_AD9102_WAVE_DATA, APP_PACKET_WORDS_AD9102_WAVE_DATA, 13, APP_PACKET_KIND_AD9102_WAVE_DATA},
{APP_PACKET_HEADER_PROFILE_SAVE_DATA, APP_PACKET_BYTES_PROFILE_SAVE_DATA, APP_PACKET_WORDS_PROFILE_SAVE_DATA, 13, APP_PACKET_KIND_PROFILE_SAVE_DATA}
};
static const app_packet_descriptor_t *app_uart_protocol_find_descriptor(uint16_t header)
{
size_t index;
for (index = 0u; index < (sizeof(g_packet_descriptors) / sizeof(g_packet_descriptors[0])); ++index)
{
if (g_packet_descriptors[index].header == header)
{
return &g_packet_descriptors[index];
}
}
return NULL;
}
uint16_t app_protocol_calculate_checksum(const uint16_t *words, uint16_t word_count)
{
uint16_t checksum;
uint16_t index;
if ((words == NULL) || (word_count == 0u))
{
return 0u;
}
checksum = words[0];
for (index = 1u; index < word_count; ++index)
{
checksum ^= words[index];
}
return checksum;
}
void app_uart_protocol_init(app_uart_protocol_parser_t *parser)
{
if (parser != NULL)
{
memset(parser, 0, sizeof(*parser));
}
}
void app_uart_protocol_reset(app_uart_protocol_parser_t *parser)
{
app_uart_protocol_init(parser);
}
app_protocol_feed_result_t app_uart_protocol_feed_byte(app_uart_protocol_parser_t *parser,
uint8_t byte,
app_packet_t *out_packet)
{
uint8_t word_index;
if ((parser == NULL) || (out_packet == NULL))
{
return APP_PROTOCOL_FEED_IN_PROGRESS;
}
if (parser->bytes_received == 0u)
{
parser->buffer[0] = byte;
parser->partial_header = byte;
parser->bytes_received = 1u;
return APP_PROTOCOL_FEED_IN_PROGRESS;
}
if (parser->bytes_received == 1u)
{
parser->buffer[1] = byte;
parser->partial_header = (uint16_t)(parser->partial_header | ((uint16_t)byte << 8));
parser->descriptor = app_uart_protocol_find_descriptor(parser->partial_header);
if (parser->descriptor == NULL)
{
app_uart_protocol_reset(parser);
return APP_PROTOCOL_FEED_INVALID_HEADER;
}
parser->bytes_received = 2u;
if (parser->descriptor->total_bytes == 2u)
{
memset(out_packet, 0, sizeof(*out_packet));
out_packet->kind = parser->descriptor->kind;
out_packet->header = parser->descriptor->header;
out_packet->checksum_valid = true;
app_uart_protocol_reset(parser);
return APP_PROTOCOL_FEED_PACKET_READY;
}
return APP_PROTOCOL_FEED_IN_PROGRESS;
}
if (parser->descriptor == NULL)
{
app_uart_protocol_reset(parser);
return APP_PROTOCOL_FEED_INVALID_HEADER;
}
if (parser->bytes_received >= parser->descriptor->total_bytes)
{
app_uart_protocol_reset(parser);
return APP_PROTOCOL_FEED_INVALID_HEADER;
}
parser->buffer[parser->bytes_received] = byte;
++parser->bytes_received;
if (parser->bytes_received < parser->descriptor->total_bytes)
{
return APP_PROTOCOL_FEED_IN_PROGRESS;
}
memset(out_packet, 0, sizeof(*out_packet));
out_packet->kind = parser->descriptor->kind;
out_packet->header = parser->descriptor->header;
out_packet->payload_words = parser->descriptor->payload_words;
for (word_index = 0u; word_index < parser->descriptor->payload_words; ++word_index)
{
uint8_t byte_offset = (uint8_t)(2u + (word_index * 2u));
out_packet->words[word_index] = (uint16_t)(parser->buffer[byte_offset] |
((uint16_t)parser->buffer[byte_offset + 1u] << 8));
}
if (parser->descriptor->checksum_word_index >= 0)
{
uint8_t checksum_index = (uint8_t)parser->descriptor->checksum_word_index;
out_packet->checksum_valid =
app_protocol_calculate_checksum(out_packet->words, checksum_index) == out_packet->words[checksum_index];
}
else
{
out_packet->checksum_valid = true;
}
app_uart_protocol_reset(parser);
return APP_PROTOCOL_FEED_PACKET_READY;
}

View File

@ -0,0 +1,68 @@
/**
* @file app_uart_protocol.h
* @brief Table-driven incremental UART packet parser and checksum helpers.
*
* Architectural note:
* The parser is intentionally transport-agnostic. It receives one byte at a
* time from the IRQ adapter, reconstructs packets according to a descriptor
* table, validates checksums when applicable, and emits fully decoded packet
* objects for the application core.
*/
#ifndef APP_UART_PROTOCOL_H
#define APP_UART_PROTOCOL_H
#include <stdbool.h>
#include <stdint.h>
#include "app_types.h"
/**
* @brief Descriptor describing one supported UART packet shape.
*/
typedef struct app_packet_descriptor_t {
uint16_t header;
uint8_t total_bytes;
uint8_t payload_words;
int8_t checksum_word_index;
app_packet_kind_t kind;
} app_packet_descriptor_t;
/**
* @brief Parsed UART packet emitted by the protocol layer.
*/
typedef struct app_packet_t {
app_packet_kind_t kind;
uint16_t header;
uint8_t payload_words;
bool checksum_valid;
uint16_t words[APP_PROTOCOL_MAX_PAYLOAD_WORDS];
} app_packet_t;
/**
* @brief Mutable state of the incremental UART parser.
*/
typedef struct app_uart_protocol_parser_t {
const app_packet_descriptor_t *descriptor;
uint16_t partial_header;
uint8_t bytes_received;
uint8_t buffer[APP_PROTOCOL_MAX_PACKET_BYTES];
} app_uart_protocol_parser_t;
/**
* @brief Result returned after feeding one byte into the parser.
*/
typedef enum app_protocol_feed_result_t {
APP_PROTOCOL_FEED_IN_PROGRESS = 0,
APP_PROTOCOL_FEED_PACKET_READY,
APP_PROTOCOL_FEED_INVALID_HEADER
} app_protocol_feed_result_t;
void app_uart_protocol_init(app_uart_protocol_parser_t *parser);
void app_uart_protocol_reset(app_uart_protocol_parser_t *parser);
app_protocol_feed_result_t app_uart_protocol_feed_byte(app_uart_protocol_parser_t *parser,
uint8_t byte,
app_packet_t *out_packet);
uint16_t app_protocol_calculate_checksum(const uint16_t *words, uint16_t word_count);
#endif /* APP_UART_PROTOCOL_H */

View File

@ -0,0 +1,525 @@
/**
* @file profile_repository.c
* @brief Future SD-card profile repository with minimal standalone parsing.
*/
#include "profile_repository.h"
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "ad9102_device.h"
#include "storage_sd.h"
#define PROFILE_REPOSITORY_BUFFER_SIZE 1024u
static char *profile_repository_trim(char *text)
{
char *end;
while ((*text != '\0') && isspace((unsigned char)*text))
{
++text;
}
if (*text == '\0')
{
return text;
}
end = text + strlen(text) - 1u;
while ((end > text) && isspace((unsigned char)*end))
{
*end = '\0';
--end;
}
return text;
}
static void profile_repository_copy_string(char *destination, size_t destination_size, const char *source)
{
if ((destination == NULL) || (destination_size == 0u))
{
return;
}
if (source == NULL)
{
destination[0] = '\0';
return;
}
strncpy(destination, source, destination_size - 1u);
destination[destination_size - 1u] = '\0';
}
static bool profile_repository_parse_bool(const char *value, bool *out_value)
{
if ((value == NULL) || (out_value == NULL))
{
return false;
}
if ((strcmp(value, "1") == 0) || (strcasecmp(value, "true") == 0) || (strcasecmp(value, "yes") == 0))
{
*out_value = true;
return true;
}
if ((strcmp(value, "0") == 0) || (strcasecmp(value, "false") == 0) || (strcasecmp(value, "no") == 0))
{
*out_value = false;
return true;
}
return false;
}
static uint16_t profile_repository_parse_u16(const char *value)
{
return (uint16_t)strtoul(value, NULL, 0);
}
static uint32_t profile_repository_parse_u32(const char *value)
{
return (uint32_t)strtoul(value, NULL, 0);
}
static float profile_repository_parse_float(const char *value)
{
return strtof(value, NULL);
}
static void profile_repository_reset_profile(profile_t *profile)
{
memset(profile, 0, sizeof(*profile));
profile->auto_run = true;
profile->boot_enabled = true;
profile->waveform.mode = WAVEFORM_MODE_SAW;
profile->waveform.enabled = 1u;
profile->waveform.saw_step = 1u;
profile->waveform.pat_period_base = 2u;
profile->waveform.pat_period = 0xFFFFu;
profile->waveform.sample_count = AD9102_SRAM_DEFAULT_SAMPLE_COUNT;
profile->waveform.hold_cycles = AD9102_SRAM_DEFAULT_HOLD;
profile->waveform.amplitude = AD9102_SRAM_DEFAULT_AMPLITUDE;
profile->ds1809.apply_position = false;
}
static void profile_repository_apply_key_value(profile_t *profile, const char *key, const char *value)
{
bool bool_value;
if ((profile == NULL) || (key == NULL) || (value == NULL))
{
return;
}
if (strcmp(key, "profile_name") == 0)
{
profile_repository_copy_string(profile->display_name, sizeof(profile->display_name), value);
}
else if (strcmp(key, "auto_run") == 0)
{
if (profile_repository_parse_bool(value, &bool_value))
{
profile->auto_run = bool_value;
}
}
else if (strcmp(key, "boot_enabled") == 0)
{
if (profile_repository_parse_bool(value, &bool_value))
{
profile->boot_enabled = bool_value;
}
}
else if (strcmp(key, "work_enable") == 0)
{
profile->work_config.work_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "u5v1_enable") == 0)
{
profile->work_config.supply_5v1_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "u5v2_enable") == 0)
{
profile->work_config.supply_5v2_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "ld1_enable") == 0)
{
profile->work_config.laser1_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "ld2_enable") == 0)
{
profile->work_config.laser2_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "ref1_enable") == 0)
{
profile->work_config.reference1_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "ref2_enable") == 0)
{
profile->work_config.reference2_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "tec1_enable") == 0)
{
profile->work_config.tec1_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "tec2_enable") == 0)
{
profile->work_config.tec2_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "ts1_enable") == 0)
{
profile->work_config.temp_sensor1_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "ts2_enable") == 0)
{
profile->work_config.temp_sensor2_enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "pid1_from_host") == 0)
{
profile->work_config.pid1_from_host = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "pid2_from_host") == 0)
{
profile->work_config.pid2_from_host = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "averages") == 0)
{
profile->work_config.averages = profile_repository_parse_u16(value);
}
else if (strcmp(key, "message_id") == 0)
{
profile->work_config.message_id = profile_repository_parse_u16(value);
}
else if (strcmp(key, "laser1_target_temp") == 0)
{
profile->laser_channels[0].target_temperature_raw = profile_repository_parse_u16(value);
}
else if (strcmp(key, "laser2_target_temp") == 0)
{
profile->laser_channels[1].target_temperature_raw = profile_repository_parse_u16(value);
}
else if (strcmp(key, "laser1_current") == 0)
{
profile->laser_channels[0].current_raw = profile_repository_parse_u16(value);
}
else if (strcmp(key, "laser2_current") == 0)
{
profile->laser_channels[1].current_raw = profile_repository_parse_u16(value);
}
else if (strcmp(key, "laser1_pid_p") == 0)
{
profile->laser_channels[0].pid_p = profile_repository_parse_float(value);
}
else if (strcmp(key, "laser1_pid_i") == 0)
{
profile->laser_channels[0].pid_i = profile_repository_parse_float(value);
}
else if (strcmp(key, "laser2_pid_p") == 0)
{
profile->laser_channels[1].pid_p = profile_repository_parse_float(value);
}
else if (strcmp(key, "laser2_pid_i") == 0)
{
profile->laser_channels[1].pid_i = profile_repository_parse_float(value);
}
else if (strcmp(key, "waveform_mode") == 0)
{
if (strcasecmp(value, "saw") == 0)
{
profile->waveform.mode = WAVEFORM_MODE_SAW;
}
else if (strcasecmp(value, "generated_sram") == 0)
{
profile->waveform.mode = WAVEFORM_MODE_SRAM_GENERATED;
}
else if (strcasecmp(value, "custom_sram") == 0)
{
profile->waveform.mode = WAVEFORM_MODE_SRAM_CUSTOM;
}
}
else if (strcmp(key, "waveform_enable") == 0)
{
profile->waveform.enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "waveform_triangle") == 0)
{
profile->waveform.triangle = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "waveform_saw_step") == 0)
{
profile->waveform.saw_step = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "waveform_pat_base") == 0)
{
profile->waveform.pat_period_base = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "waveform_pat_period") == 0)
{
profile->waveform.pat_period = profile_repository_parse_u16(value);
}
else if (strcmp(key, "waveform_sample_count") == 0)
{
profile->waveform.sample_count = profile_repository_parse_u16(value);
}
else if (strcmp(key, "waveform_hold_cycles") == 0)
{
profile->waveform.hold_cycles = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "waveform_amplitude") == 0)
{
profile->waveform.amplitude = profile_repository_parse_u16(value);
}
else if (strcmp(key, "waveform_source") == 0)
{
profile_repository_copy_string(profile->waveform.source_path, sizeof(profile->waveform.source_path), value);
}
else if (strcmp(key, "ad9833_enable") == 0)
{
profile->ad9833.enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "ad9833_triangle") == 0)
{
profile->ad9833.triangle = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "ad9833_frequency_word") == 0)
{
profile->ad9833.frequency_word = profile_repository_parse_u32(value);
}
else if (strcmp(key, "stm32_dac_enable") == 0)
{
profile->stm32_dac.enabled = (uint8_t)profile_repository_parse_u16(value);
}
else if (strcmp(key, "stm32_dac_code") == 0)
{
profile->stm32_dac.code = profile_repository_parse_u16(value);
}
else if (strcmp(key, "ds1809_apply") == 0)
{
if (profile_repository_parse_bool(value, &bool_value))
{
profile->ds1809.apply_position = bool_value;
}
}
else if (strcmp(key, "ds1809_position_from_min") == 0)
{
profile->ds1809.position_from_min = (uint8_t)profile_repository_parse_u16(value);
}
}
static bool profile_repository_load_ini_file(const char *profile_path, profile_t *profile)
{
char buffer[PROFILE_REPOSITORY_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)
{
return false;
}
buffer[bytes_read] = '\0';
cursor = buffer;
while (*cursor != '\0')
{
char *line_start = cursor;
char *line_end = strpbrk(cursor, "\r\n");
char *separator;
char *key;
char *value;
if (line_end != NULL)
{
*line_end = '\0';
cursor = line_end + 1;
while ((*cursor == '\r') || (*cursor == '\n'))
{
++cursor;
}
}
else
{
cursor += strlen(cursor);
}
line_start = profile_repository_trim(line_start);
if ((*line_start == '\0') || (*line_start == '#') || (*line_start == ';'))
{
continue;
}
separator = strchr(line_start, '=');
if (separator == NULL)
{
continue;
}
*separator = '\0';
key = profile_repository_trim(line_start);
value = profile_repository_trim(separator + 1);
profile_repository_apply_key_value(profile, key, value);
}
return true;
}
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];
UINT bytes_read = 0u;
char *cursor;
uint16_t valid_line_index = 0u;
FRESULT result;
if ((out_profile == NULL) || !storage_sd_file_exists(APP_STORAGE_PROFILE_INDEX_FILE))
{
return false;
}
result = storage_sd_read_bytes(APP_STORAGE_PROFILE_INDEX_FILE, 0u, buffer, sizeof(buffer) - 1u, &bytes_read);
if (result != FR_OK)
{
return false;
}
buffer[bytes_read] = '\0';
cursor = buffer;
while (*cursor != '\0')
{
char *line_start = cursor;
char *line_end = strpbrk(cursor, "\r\n");
char *display_name;
char *profile_path;
char *waveform_path;
char *separator_1;
char *separator_2;
if (line_end != NULL)
{
*line_end = '\0';
cursor = line_end + 1;
while ((*cursor == '\r') || (*cursor == '\n'))
{
++cursor;
}
}
else
{
cursor += strlen(cursor);
}
line_start = profile_repository_trim(line_start);
if ((*line_start == '\0') || (*line_start == '#') || (*line_start == ';'))
{
continue;
}
separator_1 = strchr(line_start, ',');
if (separator_1 == NULL)
{
continue;
}
*separator_1 = '\0';
separator_2 = strchr(separator_1 + 1, ',');
if (separator_2 != NULL)
{
*separator_2 = '\0';
}
display_name = profile_repository_trim(line_start);
profile_path = profile_repository_trim(separator_1 + 1);
waveform_path = (separator_2 != NULL) ? profile_repository_trim(separator_2 + 1) : "";
if (*profile_path == '\0')
{
continue;
}
if (valid_line_index == target_index)
{
profile_repository_reset_profile(out_profile);
profile_repository_copy_string(out_profile->display_name, sizeof(out_profile->display_name), display_name);
profile_repository_copy_string(out_profile->profile_path, sizeof(out_profile->profile_path), profile_path);
profile_repository_copy_string(out_profile->waveform_path, sizeof(out_profile->waveform_path), waveform_path);
profile_repository_copy_string(out_profile->waveform.source_path,
sizeof(out_profile->waveform.source_path),
waveform_path);
if (!profile_repository_load_ini_file(profile_path, out_profile))
{
return false;
}
if (out_total_valid_lines != NULL)
{
*out_total_valid_lines = (uint16_t)(valid_line_index + 1u);
}
return true;
}
++valid_line_index;
}
if (out_total_valid_lines != NULL)
{
*out_total_valid_lines = valid_line_index;
}
return false;
}
bool profile_repository_load_first(profile_t *out_profile, uint16_t *out_index)
{
uint16_t total_lines = 0u;
if (!profile_repository_load_by_valid_line_index(0u, out_profile, &total_lines))
{
return false;
}
if (out_index != NULL)
{
*out_index = 0u;
}
return true;
}
bool profile_repository_load_next(uint16_t *in_out_index, profile_t *out_profile)
{
uint16_t requested_index;
uint16_t total_lines = 0u;
if ((in_out_index == NULL) || (out_profile == NULL))
{
return false;
}
requested_index = (uint16_t)(*in_out_index + 1u);
if (profile_repository_load_by_valid_line_index(requested_index, out_profile, &total_lines))
{
*in_out_index = requested_index;
return true;
}
if ((total_lines == 0u) || !profile_repository_load_by_valid_line_index(0u, out_profile, NULL))
{
return false;
}
*in_out_index = 0u;
return true;
}

View File

@ -0,0 +1,36 @@
/**
* @file profile_repository.h
* @brief Future SD-card profile repository with minimal standalone parsing.
*/
#ifndef PROFILE_REPOSITORY_H
#define PROFILE_REPOSITORY_H
#include <stdbool.h>
#include <stdint.h>
#include "app_types.h"
/**
* @brief Load the first valid profile listed in `profiles.csv`.
*
* @param out_profile Destination profile object.
* @param out_index Receives the zero-based valid profile index.
*
* @retval true A valid profile was loaded.
* @retval false No valid profile entry was available.
*/
bool profile_repository_load_first(profile_t *out_profile, uint16_t *out_index);
/**
* @brief Load the next valid profile, wrapping around to the first entry.
*
* @param in_out_index Current profile index on input, next index on success.
* @param out_profile Destination profile object.
*
* @retval true The next valid profile was loaded.
* @retval false No valid profile entry was available.
*/
bool profile_repository_load_next(uint16_t *in_out_index, profile_t *out_profile);
#endif /* PROFILE_REPOSITORY_H */

View File

@ -0,0 +1,497 @@
/**
* @file profile_storage.c
* @brief Streamed SD-card writer for GUI-initiated profile saves.
*/
#include "profile_storage.h"
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include "app_types.h"
#include "storage_sd.h"
#define PROFILE_STORAGE_PROFILES_DIR "profiles"
#define PROFILE_STORAGE_WAVES_DIR "waves"
#define PROFILE_STORAGE_PROFILE_FILE_TEMPLATE "profiles/PROF%04u.INI"
#define PROFILE_STORAGE_WAVE_FILE_TEMPLATE "waves/WAVE%04u.CSV"
#define PROFILE_STORAGE_MAX_FILE_INDEX 9999u
#define PROFILE_STORAGE_INDEX_LINE_BUFFER_SIZE 192u
typedef struct profile_storage_context_t {
bool active;
bool profile_file_open;
bool waveform_file_open;
char display_name[APP_PROFILE_NAME_LENGTH];
char profile_path[APP_PROFILE_PATH_LENGTH];
char waveform_path[APP_PROFILE_PATH_LENGTH];
uint16_t profile_expected_bytes;
uint16_t waveform_expected_bytes;
uint16_t profile_received_bytes;
uint16_t waveform_received_bytes;
FIL profile_file;
FIL waveform_file;
profile_storage_status_t last_status;
} profile_storage_context_t;
static profile_storage_context_t g_profile_storage;
static profile_storage_status_t profile_storage_set_status(profile_storage_status_t status)
{
g_profile_storage.last_status = status;
return status;
}
static void profile_storage_reset_context(void)
{
memset(&g_profile_storage, 0, sizeof(g_profile_storage));
g_profile_storage.last_status = PROFILE_STORAGE_STATUS_OK;
}
static bool profile_storage_is_name_character_allowed(char character)
{
return (isalnum((unsigned char)character) != 0) ||
(character == ' ') ||
(character == '-') ||
(character == '_');
}
static bool profile_storage_copy_validated_name(char *destination, size_t destination_size, const char *source)
{
const char *start;
const char *end;
size_t length;
size_t index;
if ((destination == NULL) || (destination_size < APP_PROFILE_NAME_LENGTH) || (source == NULL))
{
return false;
}
start = source;
while ((*start != '\0') && isspace((unsigned char)*start))
{
++start;
}
end = start + strlen(start);
while ((end > start) && isspace((unsigned char)end[-1]))
{
--end;
}
length = (size_t)(end - start);
if ((length == 0u) || (length > APP_PROFILE_NAME_MAX_CHARACTERS))
{
return false;
}
for (index = 0u; index < length; ++index)
{
if (!profile_storage_is_name_character_allowed(start[index]))
{
return false;
}
}
memcpy(destination, start, length);
destination[length] = '\0';
return true;
}
static void profile_storage_clear_session_paths(void)
{
g_profile_storage.display_name[0] = '\0';
g_profile_storage.profile_path[0] = '\0';
g_profile_storage.waveform_path[0] = '\0';
}
static void profile_storage_close_open_files(void)
{
if (g_profile_storage.waveform_file_open)
{
(void)storage_sd_close_file(&g_profile_storage.waveform_file);
g_profile_storage.waveform_file_open = false;
}
if (g_profile_storage.profile_file_open)
{
(void)storage_sd_close_file(&g_profile_storage.profile_file);
g_profile_storage.profile_file_open = false;
}
}
static void profile_storage_remove_partial_files(void)
{
if (g_profile_storage.waveform_path[0] != '\0')
{
(void)storage_sd_remove_file(g_profile_storage.waveform_path);
}
if (g_profile_storage.profile_path[0] != '\0')
{
(void)storage_sd_remove_file(g_profile_storage.profile_path);
}
}
static void profile_storage_abort_session(void)
{
profile_storage_close_open_files();
profile_storage_remove_partial_files();
g_profile_storage.active = false;
g_profile_storage.profile_expected_bytes = 0u;
g_profile_storage.waveform_expected_bytes = 0u;
g_profile_storage.profile_received_bytes = 0u;
g_profile_storage.waveform_received_bytes = 0u;
profile_storage_clear_session_paths();
}
static profile_storage_status_t profile_storage_format_target_path(char *destination,
size_t destination_size,
const char *format_string,
uint16_t file_index)
{
int written;
written = snprintf(destination, destination_size, format_string, (unsigned int)file_index);
if ((written <= 0) || ((size_t)written >= destination_size))
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_FILE_NAME_EXHAUSTED);
}
return PROFILE_STORAGE_STATUS_OK;
}
static profile_storage_status_t profile_storage_find_free_paths(uint16_t waveform_text_bytes)
{
uint16_t file_index;
for (file_index = 1u; file_index <= PROFILE_STORAGE_MAX_FILE_INDEX; ++file_index)
{
FILINFO file_info;
FRESULT result;
profile_storage_status_t status;
status = profile_storage_format_target_path(g_profile_storage.profile_path,
sizeof(g_profile_storage.profile_path),
PROFILE_STORAGE_PROFILE_FILE_TEMPLATE,
file_index);
if (status != PROFILE_STORAGE_STATUS_OK)
{
return status;
}
result = storage_sd_stat(g_profile_storage.profile_path, &file_info);
if (result == FR_OK)
{
continue;
}
if ((result != FR_NO_FILE) && (result != FR_NO_PATH))
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_STORAGE_UNAVAILABLE);
}
if (waveform_text_bytes > 0u)
{
status = profile_storage_format_target_path(g_profile_storage.waveform_path,
sizeof(g_profile_storage.waveform_path),
PROFILE_STORAGE_WAVE_FILE_TEMPLATE,
file_index);
if (status != PROFILE_STORAGE_STATUS_OK)
{
return status;
}
result = storage_sd_stat(g_profile_storage.waveform_path, &file_info);
if (result == FR_OK)
{
continue;
}
if ((result != FR_NO_FILE) && (result != FR_NO_PATH))
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_STORAGE_UNAVAILABLE);
}
}
else
{
g_profile_storage.waveform_path[0] = '\0';
}
return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK);
}
return profile_storage_set_status(PROFILE_STORAGE_STATUS_FILE_NAME_EXHAUSTED);
}
static profile_storage_status_t profile_storage_open_target_files(void)
{
FRESULT result;
result = storage_sd_make_directory(PROFILE_STORAGE_PROFILES_DIR);
if (result != FR_OK)
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_DIRECTORY_ERROR);
}
if (g_profile_storage.waveform_expected_bytes > 0u)
{
result = storage_sd_make_directory(PROFILE_STORAGE_WAVES_DIR);
if (result != FR_OK)
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_DIRECTORY_ERROR);
}
}
result = storage_sd_open_file(&g_profile_storage.profile_file,
g_profile_storage.profile_path,
FA_CREATE_ALWAYS | FA_WRITE);
if (result != FR_OK)
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_FILE_OPEN_ERROR);
}
g_profile_storage.profile_file_open = true;
if (g_profile_storage.waveform_expected_bytes > 0u)
{
result = storage_sd_open_file(&g_profile_storage.waveform_file,
g_profile_storage.waveform_path,
FA_CREATE_ALWAYS | FA_WRITE);
if (result != FR_OK)
{
profile_storage_abort_session();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_FILE_OPEN_ERROR);
}
g_profile_storage.waveform_file_open = true;
}
return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK);
}
static profile_storage_status_t profile_storage_write_to_file(FIL *file,
uint16_t *received_bytes,
uint16_t expected_bytes,
const uint8_t *data,
uint16_t data_size)
{
UINT bytes_written = 0u;
FRESULT result;
if ((file == NULL) || (received_bytes == NULL) || (data == NULL) || (data_size == 0u))
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_INVALID_ARGUMENT);
}
if ((uint32_t)(*received_bytes) + (uint32_t)data_size > (uint32_t)expected_bytes)
{
profile_storage_abort_session();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_SIZE_MISMATCH);
}
result = f_write(file, data, data_size, &bytes_written);
if ((result != FR_OK) || (bytes_written != data_size))
{
profile_storage_abort_session();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_WRITE_ERROR);
}
*received_bytes = (uint16_t)(*received_bytes + data_size);
return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK);
}
static profile_storage_status_t profile_storage_append_index_entry(void)
{
char line_buffer[PROFILE_STORAGE_INDEX_LINE_BUFFER_SIZE];
FIL index_file;
UINT bytes_written = 0u;
FRESULT result;
int line_length;
line_length = snprintf(line_buffer,
sizeof(line_buffer),
"%s,%s,%s\r\n",
g_profile_storage.display_name,
g_profile_storage.profile_path,
g_profile_storage.waveform_path);
if ((line_length <= 0) || ((size_t)line_length >= sizeof(line_buffer)))
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_INDEX_UPDATE_ERROR);
}
result = storage_sd_open_file(&index_file,
APP_STORAGE_PROFILE_INDEX_FILE,
FA_OPEN_ALWAYS | FA_WRITE);
if (result != FR_OK)
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_INDEX_UPDATE_ERROR);
}
result = f_lseek(&index_file, f_size(&index_file));
if (result == FR_OK)
{
result = f_write(&index_file, line_buffer, (UINT)line_length, &bytes_written);
}
if (result == FR_OK)
{
result = f_sync(&index_file);
}
(void)storage_sd_close_file(&index_file);
if ((result != FR_OK) || (bytes_written != (UINT)line_length))
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_INDEX_UPDATE_ERROR);
}
return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK);
}
void profile_storage_init(void)
{
profile_storage_reset_context();
}
bool profile_storage_is_active(void)
{
return g_profile_storage.active;
}
profile_storage_status_t profile_storage_get_last_status(void)
{
return g_profile_storage.last_status;
}
profile_storage_status_t profile_storage_begin(const char *display_name,
uint16_t profile_text_bytes,
uint16_t waveform_text_bytes)
{
if (profile_storage_is_active())
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_SESSION_ACTIVE);
}
profile_storage_reset_context();
if (!storage_sd_is_available())
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_STORAGE_UNAVAILABLE);
}
if ((profile_text_bytes == 0u) || !profile_storage_copy_validated_name(g_profile_storage.display_name,
sizeof(g_profile_storage.display_name),
display_name))
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_NAME_INVALID);
}
g_profile_storage.profile_expected_bytes = profile_text_bytes;
g_profile_storage.waveform_expected_bytes = waveform_text_bytes;
g_profile_storage.active = true;
if (profile_storage_find_free_paths(waveform_text_bytes) != PROFILE_STORAGE_STATUS_OK)
{
g_profile_storage.active = false;
return g_profile_storage.last_status;
}
if (profile_storage_open_target_files() != PROFILE_STORAGE_STATUS_OK)
{
g_profile_storage.active = false;
profile_storage_clear_session_paths();
return g_profile_storage.last_status;
}
return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK);
}
profile_storage_status_t profile_storage_write_chunk(uint16_t section_id,
const uint8_t *data,
uint16_t data_size)
{
if (!g_profile_storage.active)
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_NO_ACTIVE_SESSION);
}
if ((data == NULL) || (data_size == 0u) || (data_size > APP_PROFILE_SAVE_MAX_DATA_BYTES_PER_PACKET))
{
profile_storage_abort_session();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_INVALID_ARGUMENT);
}
if (section_id == APP_PROFILE_SAVE_SECTION_PROFILE_TEXT)
{
return profile_storage_write_to_file(&g_profile_storage.profile_file,
&g_profile_storage.profile_received_bytes,
g_profile_storage.profile_expected_bytes,
data,
data_size);
}
if ((section_id == APP_PROFILE_SAVE_SECTION_WAVEFORM_TEXT) && g_profile_storage.waveform_file_open)
{
return profile_storage_write_to_file(&g_profile_storage.waveform_file,
&g_profile_storage.waveform_received_bytes,
g_profile_storage.waveform_expected_bytes,
data,
data_size);
}
profile_storage_abort_session();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_SECTION_ERROR);
}
profile_storage_status_t profile_storage_commit(void)
{
if (!g_profile_storage.active)
{
return profile_storage_set_status(PROFILE_STORAGE_STATUS_NO_ACTIVE_SESSION);
}
if ((g_profile_storage.profile_received_bytes != g_profile_storage.profile_expected_bytes) ||
(g_profile_storage.waveform_received_bytes != g_profile_storage.waveform_expected_bytes))
{
profile_storage_abort_session();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_SIZE_MISMATCH);
}
if (g_profile_storage.profile_file_open && (f_sync(&g_profile_storage.profile_file) != FR_OK))
{
profile_storage_abort_session();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_WRITE_ERROR);
}
if (g_profile_storage.waveform_file_open && (f_sync(&g_profile_storage.waveform_file) != FR_OK))
{
profile_storage_abort_session();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_WRITE_ERROR);
}
profile_storage_close_open_files();
if (profile_storage_append_index_entry() != PROFILE_STORAGE_STATUS_OK)
{
profile_storage_remove_partial_files();
g_profile_storage.active = false;
profile_storage_clear_session_paths();
return g_profile_storage.last_status;
}
g_profile_storage.active = false;
g_profile_storage.profile_expected_bytes = 0u;
g_profile_storage.waveform_expected_bytes = 0u;
g_profile_storage.profile_received_bytes = 0u;
g_profile_storage.waveform_received_bytes = 0u;
profile_storage_clear_session_paths();
return profile_storage_set_status(PROFILE_STORAGE_STATUS_OK);
}
void profile_storage_cancel(void)
{
if (g_profile_storage.active)
{
profile_storage_abort_session();
}
g_profile_storage.last_status = PROFILE_STORAGE_STATUS_OK;
}

View File

@ -0,0 +1,50 @@
/**
* @file profile_storage.h
* @brief Streamed SD-card writer for GUI-initiated profile saves.
*
* Architectural note:
* The desktop GUI already knows the current human-facing configuration values.
* It serialises them into the same INI/CSV text format that standalone boot
* understands, while this service owns the SD-card side: validating the
* profile name, generating 8.3-compatible file names, streaming incoming text
* into files, and updating `profiles.csv` only after the full upload succeeds.
*/
#ifndef PROFILE_STORAGE_H
#define PROFILE_STORAGE_H
#include <stdbool.h>
#include <stdint.h>
/**
* @brief Result codes returned by the streamed profile-save service.
*/
typedef enum profile_storage_status_t {
PROFILE_STORAGE_STATUS_OK = 0,
PROFILE_STORAGE_STATUS_INVALID_ARGUMENT,
PROFILE_STORAGE_STATUS_NAME_INVALID,
PROFILE_STORAGE_STATUS_STORAGE_UNAVAILABLE,
PROFILE_STORAGE_STATUS_SESSION_ACTIVE,
PROFILE_STORAGE_STATUS_NO_ACTIVE_SESSION,
PROFILE_STORAGE_STATUS_DIRECTORY_ERROR,
PROFILE_STORAGE_STATUS_FILE_NAME_EXHAUSTED,
PROFILE_STORAGE_STATUS_FILE_OPEN_ERROR,
PROFILE_STORAGE_STATUS_WRITE_ERROR,
PROFILE_STORAGE_STATUS_INDEX_UPDATE_ERROR,
PROFILE_STORAGE_STATUS_SIZE_MISMATCH,
PROFILE_STORAGE_STATUS_SECTION_ERROR
} profile_storage_status_t;
void profile_storage_init(void);
bool profile_storage_is_active(void);
profile_storage_status_t profile_storage_get_last_status(void);
profile_storage_status_t profile_storage_begin(const char *display_name,
uint16_t profile_text_bytes,
uint16_t waveform_text_bytes);
profile_storage_status_t profile_storage_write_chunk(uint16_t section_id,
const uint8_t *data,
uint16_t data_size);
profile_storage_status_t profile_storage_commit(void);
void profile_storage_cancel(void);
#endif /* PROFILE_STORAGE_H */

214
App/Services/storage_sd.c Normal file
View File

@ -0,0 +1,214 @@
/**
* @file storage_sd.c
* @brief Narrow SD-card storage wrapper built directly on FatFs.
*/
#include "storage_sd.h"
#include "fatfs.h"
#include "board_io.h"
static uint8_t g_storage_mounted = 0u;
static uint16_t g_storage_mount_depth = 0u;
bool storage_sd_is_available(void)
{
return board_io_is_sd_card_present();
}
FRESULT storage_sd_begin(void)
{
FRESULT result;
if (!storage_sd_is_available())
{
return FR_NOT_READY;
}
if (g_storage_mount_depth > 0u)
{
++g_storage_mount_depth;
return FR_OK;
}
result = f_mount(&SDFatFS, SDPath, 1u);
if (result == FR_OK)
{
g_storage_mounted = 1u;
g_storage_mount_depth = 1u;
}
return result;
}
FRESULT storage_sd_end(void)
{
FRESULT result;
if (g_storage_mount_depth == 0u)
{
return FR_OK;
}
--g_storage_mount_depth;
if (g_storage_mount_depth > 0u)
{
return FR_OK;
}
result = f_mount(NULL, SDPath, 1u);
if (result == FR_OK)
{
g_storage_mounted = 0u;
}
else
{
g_storage_mount_depth = 1u;
}
return result;
}
FRESULT storage_sd_open_file(FIL *file, const char *path, BYTE mode)
{
FRESULT result;
if ((file == NULL) || (path == NULL))
{
return FR_INVALID_PARAMETER;
}
result = storage_sd_begin();
if (result != FR_OK)
{
return result;
}
result = f_open(file, path, mode);
if (result != FR_OK)
{
(void)storage_sd_end();
}
return result;
}
FRESULT storage_sd_close_file(FIL *file)
{
FRESULT close_result = FR_OK;
FRESULT end_result;
if (file != NULL)
{
close_result = f_close(file);
}
end_result = storage_sd_end();
if (close_result != FR_OK)
{
return close_result;
}
return end_result;
}
FRESULT storage_sd_read_bytes(const char *path, FSIZE_t offset, void *data, UINT size, UINT *bytes_read)
{
FIL file;
UINT local_bytes_read = 0u;
FRESULT result = storage_sd_open_file(&file, path, FA_READ);
if (result != FR_OK)
{
return result;
}
result = f_lseek(&file, offset);
if (result == FR_OK)
{
result = f_read(&file, data, size, &local_bytes_read);
}
(void)storage_sd_close_file(&file);
if (bytes_read != NULL)
{
*bytes_read = local_bytes_read;
}
return result;
}
FRESULT storage_sd_stat(const char *path, FILINFO *out_info)
{
FRESULT result;
if (path == NULL)
{
return FR_INVALID_PARAMETER;
}
result = storage_sd_begin();
if (result != FR_OK)
{
return result;
}
result = f_stat(path, out_info);
(void)storage_sd_end();
return result;
}
bool storage_sd_file_exists(const char *path)
{
FILINFO info;
return storage_sd_stat(path, &info) == FR_OK;
}
FRESULT storage_sd_make_directory(const char *path)
{
FRESULT result;
if (path == NULL)
{
return FR_INVALID_PARAMETER;
}
result = storage_sd_begin();
if (result != FR_OK)
{
return result;
}
result = f_mkdir(path);
if (result == FR_EXIST)
{
result = FR_OK;
}
(void)storage_sd_end();
return result;
}
FRESULT storage_sd_remove_file(const char *path)
{
FRESULT result;
if (path == NULL)
{
return FR_INVALID_PARAMETER;
}
result = storage_sd_begin();
if (result != FR_OK)
{
return result;
}
result = f_unlink(path);
if ((result == FR_NO_FILE) || (result == FR_NO_PATH))
{
result = FR_OK;
}
(void)storage_sd_end();
return result;
}

30
App/Services/storage_sd.h Normal file
View File

@ -0,0 +1,30 @@
/**
* @file storage_sd.h
* @brief Narrow SD-card storage wrapper built directly on FatFs.
*
* Architectural note:
* An earlier storage layer mixed allocation-heavy tutorial helpers with
* application-specific policies. This service now exposes only the minimal
* file-oriented operations needed by profile loading and streamed profile
* saving, while keeping FatFs details out of higher-level services.
*/
#ifndef STORAGE_SD_H
#define STORAGE_SD_H
#include <stdbool.h>
#include "ff.h"
bool storage_sd_is_available(void);
FRESULT storage_sd_begin(void);
FRESULT storage_sd_end(void);
FRESULT storage_sd_read_bytes(const char *path, FSIZE_t offset, void *data, UINT size, UINT *bytes_read);
FRESULT storage_sd_open_file(FIL *file, const char *path, BYTE mode);
FRESULT storage_sd_close_file(FIL *file);
FRESULT storage_sd_stat(const char *path, FILINFO *out_info);
bool storage_sd_file_exists(const char *path);
FRESULT storage_sd_make_directory(const char *path);
FRESULT storage_sd_remove_file(const char *path);
#endif /* STORAGE_SD_H */

87
App/Services/telemetry.c Normal file
View File

@ -0,0 +1,87 @@
/**
* @file telemetry.c
* @brief Telemetry-frame creation, checksum finalisation, and serialisation.
*/
#include "telemetry.h"
#include <string.h>
#include "app_uart_protocol.h"
void telemetry_reset(telemetry_frame_t *frame)
{
if (frame == NULL)
{
return;
}
memset(frame->words, 0, sizeof(frame->words));
frame->words[0] = APP_PACKET_HEADER_WORK_CONFIG;
}
void telemetry_set_message_id(telemetry_frame_t *frame, uint16_t message_id)
{
if (frame != NULL)
{
frame->words[13] = message_id;
}
}
void telemetry_set_live_data(telemetry_frame_t *frame,
uint16_t laser1_power,
uint16_t laser2_power,
uint32_t tick_10ms,
uint16_t laser1_temperature,
uint16_t laser2_temperature,
uint16_t adc_slot_7,
uint16_t adc_slot_8,
uint16_t adc_slot_9,
uint16_t adc_slot_10,
uint16_t adc_slot_11,
uint16_t adc_slot_12)
{
if (frame == NULL)
{
return;
}
frame->words[1] = laser1_power;
frame->words[2] = laser2_power;
frame->words[3] = (uint16_t)(tick_10ms & 0xFFFFu);
frame->words[4] = (uint16_t)((tick_10ms >> 16) & 0xFFFFu);
frame->words[5] = laser1_temperature;
frame->words[6] = laser2_temperature;
frame->words[7] = adc_slot_7;
frame->words[8] = adc_slot_8;
frame->words[9] = adc_slot_9;
frame->words[10] = adc_slot_10;
frame->words[11] = adc_slot_11;
frame->words[12] = adc_slot_12;
}
void telemetry_finalize(telemetry_frame_t *frame)
{
if (frame == NULL)
{
return;
}
frame->words[14] = app_protocol_calculate_checksum(&frame->words[1], 13u);
}
void telemetry_to_bytes(const telemetry_frame_t *frame, uint8_t *out_bytes)
{
uint8_t index;
if ((frame == NULL) || (out_bytes == NULL))
{
return;
}
for (index = 0u; index < APP_TELEMETRY_WORD_COUNT; ++index)
{
out_bytes[index * 2u] = (uint8_t)(frame->words[index] & 0x00FFu);
out_bytes[(index * 2u) + 1u] = (uint8_t)((frame->words[index] >> 8) & 0x00FFu);
}
}

30
App/Services/telemetry.h Normal file
View File

@ -0,0 +1,30 @@
/**
* @file telemetry.h
* @brief Telemetry-frame creation, checksum finalisation, and serialisation.
*/
#ifndef TELEMETRY_H
#define TELEMETRY_H
#include <stdint.h>
#include "app_types.h"
void telemetry_reset(telemetry_frame_t *frame);
void telemetry_set_message_id(telemetry_frame_t *frame, uint16_t message_id);
void telemetry_set_live_data(telemetry_frame_t *frame,
uint16_t laser1_power,
uint16_t laser2_power,
uint32_t tick_10ms,
uint16_t laser1_temperature,
uint16_t laser2_temperature,
uint16_t adc_slot_7,
uint16_t adc_slot_8,
uint16_t adc_slot_9,
uint16_t adc_slot_10,
uint16_t adc_slot_11,
uint16_t adc_slot_12);
void telemetry_finalize(telemetry_frame_t *frame);
void telemetry_to_bytes(const telemetry_frame_t *frame, uint8_t *out_bytes);
#endif /* TELEMETRY_H */

View File

@ -0,0 +1,67 @@
/**
* @file temperature_control.c
* @brief Temperature-control services for the laser TEC loops.
*/
#include "temperature_control.h"
uint16_t temperature_control_compute_pid(const laser_channel_config_t *channel_config,
laser_runtime_t *runtime_state,
uint8_t channel_index,
uint32_t current_tick_1ms,
uint32_t *shared_pid_reference_tick)
{
int32_t error;
float proportional_gain;
float integral_term;
int32_t output;
if ((channel_config == NULL) || (runtime_state == NULL) || (shared_pid_reference_tick == NULL))
{
return 32768u;
}
error = (int32_t)runtime_state->current_temperature_raw - (int32_t)channel_config->target_temperature_raw;
integral_term = runtime_state->integral_error;
if ((error < 3000) && (error > -3000))
{
integral_term += channel_config->pid_i *
(float)error *
(float)(current_tick_1ms - *shared_pid_reference_tick) /
100.0f;
}
proportional_gain = channel_config->pid_p;
if (integral_term > 32000.0f)
{
integral_term = 32000.0f;
}
else if (integral_term < -32000.0f)
{
integral_term = -32000.0f;
}
runtime_state->integral_error = integral_term;
output = 32768 + (int32_t)(proportional_gain * (float)error) + (int32_t)integral_term;
if (output < 1000)
{
output = 8800;
}
else if (output > 56800)
{
output = 56800;
}
/* Both PID channels use a shared timing reference updated after the
* second TEC computation. This preserves the original controller timing. */
if (channel_index == 2u)
{
*shared_pid_reference_tick = current_tick_1ms;
}
return (uint16_t)output;
}

View File

@ -0,0 +1,19 @@
/**
* @file temperature_control.h
* @brief Temperature-control services for the laser TEC loops.
*/
#ifndef TEMPERATURE_CONTROL_H
#define TEMPERATURE_CONTROL_H
#include <stdint.h>
#include "app_types.h"
uint16_t temperature_control_compute_pid(const laser_channel_config_t *channel_config,
laser_runtime_t *runtime_state,
uint8_t channel_index,
uint32_t current_tick_1ms,
uint32_t *shared_pid_reference_tick);
#endif /* TEMPERATURE_CONTROL_H */

237
App/Services/ui_status.c Normal file
View File

@ -0,0 +1,237 @@
/**
* @file ui_status.c
* @brief Standalone LCD/button frontend for profile-driven operation.
*
* Architectural note:
* This service owns all user-facing status formatting and button debouncing.
* The application core pushes state into this module, while this module
* returns high-level events such as "select next profile".
*/
#include "ui_status.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "board_io.h"
#include "lcd1602_display.h"
#define UI_STATUS_LCD_COLUMNS 16u
#define UI_STATUS_BUTTON_DEBOUNCE_TICKS_10MS 3u
typedef struct ui_button_state_t {
bool raw_pressed;
bool stable_pressed;
uint32_t last_change_tick_10ms;
} ui_button_state_t;
static char g_profile_name[APP_PROFILE_NAME_LENGTH];
static app_mode_t g_mode = APP_MODE_IDLE;
static uint8_t g_error_flags = 0u;
static uint8_t g_initialised = 0u;
static uint8_t g_display_dirty = 0u;
static ui_button_state_t g_button = {false, false, 0u};
static void ui_status_mark_dirty(void);
static void ui_status_copy_padded(char *destination, const char *source);
static const char *ui_status_mode_name(app_mode_t mode);
static void ui_status_build_line_1(char *line_buffer);
static void ui_status_build_line_2(char *line_buffer);
static void ui_status_refresh_display(void);
void ui_status_init(void)
{
bool raw_pressed;
memset(g_profile_name, 0, sizeof(g_profile_name));
g_mode = APP_MODE_IDLE;
g_error_flags = 0u;
board_io_init_standalone_ui();
raw_pressed = board_io_is_standalone_ui_button_pressed();
/*
* Synchronise the debounce state with the physical input level at boot.
* This prevents a stuck-low or already-held button from generating a
* synthetic "next profile" event immediately after startup.
*/
g_button.raw_pressed = raw_pressed;
g_button.stable_pressed = raw_pressed;
g_button.last_change_tick_10ms = 0u;
lcd1602_display_init();
g_initialised = 1u;
g_display_dirty = 1u;
ui_status_refresh_display();
}
void ui_status_set_profile_name(const char *profile_name)
{
char next_name[APP_PROFILE_NAME_LENGTH] = {0};
if (profile_name != NULL)
{
strncpy(next_name, profile_name, sizeof(next_name) - 1u);
}
if (strncmp(g_profile_name, next_name, sizeof(g_profile_name)) != 0)
{
memcpy(g_profile_name, next_name, sizeof(g_profile_name));
ui_status_mark_dirty();
}
}
void ui_status_set_mode(app_mode_t mode)
{
/*
* TX_CURRENT is a transient transport state rather than a user-visible
* operating mode. Ignoring it prevents needless LCD churn during polling.
*/
if (mode == APP_MODE_TX_CURRENT)
{
return;
}
if (g_mode != mode)
{
g_mode = mode;
ui_status_mark_dirty();
}
}
void ui_status_set_error(uint8_t error_flags)
{
if (g_error_flags != error_flags)
{
g_error_flags = error_flags;
ui_status_mark_dirty();
}
}
app_event_t ui_status_poll_event(uint32_t tick_10ms)
{
bool raw_pressed = board_io_is_standalone_ui_button_pressed();
app_event_t event = APP_EVENT_NONE;
if (raw_pressed != g_button.raw_pressed)
{
g_button.raw_pressed = raw_pressed;
g_button.last_change_tick_10ms = tick_10ms;
}
else if ((tick_10ms - g_button.last_change_tick_10ms) >= UI_STATUS_BUTTON_DEBOUNCE_TICKS_10MS)
{
if (g_button.stable_pressed != g_button.raw_pressed)
{
g_button.stable_pressed = g_button.raw_pressed;
if (g_button.stable_pressed)
{
event = APP_EVENT_PROFILE_NEXT;
}
}
}
if (g_display_dirty != 0u)
{
ui_status_refresh_display();
}
return event;
}
static void ui_status_mark_dirty(void)
{
g_display_dirty = 1u;
}
static void ui_status_copy_padded(char *destination, const char *source)
{
size_t index;
for (index = 0u; index < UI_STATUS_LCD_COLUMNS; ++index)
{
destination[index] = ' ';
}
destination[UI_STATUS_LCD_COLUMNS] = '\0';
if (source == NULL)
{
return;
}
for (index = 0u; (index < UI_STATUS_LCD_COLUMNS) && (source[index] != '\0'); ++index)
{
destination[index] = source[index];
}
}
static const char *ui_status_mode_name(app_mode_t mode)
{
switch (mode)
{
case APP_MODE_WORK:
return "WORK";
case APP_MODE_ERROR:
return "ERROR";
case APP_MODE_IDLE:
default:
return "IDLE";
}
}
static void ui_status_build_line_1(char *line_buffer)
{
const char *label = g_profile_name;
if (line_buffer == NULL)
{
return;
}
if (label[0] == '\0')
{
label = (g_mode == APP_MODE_WORK) ? "Manual control" : "No profile";
}
ui_status_copy_padded(line_buffer, label);
}
static void ui_status_build_line_2(char *line_buffer)
{
char status_text[UI_STATUS_LCD_COLUMNS + 1u];
if (line_buffer == NULL)
{
return;
}
if (g_error_flags == 0u)
{
(void)snprintf(status_text, sizeof(status_text), "%s OK", ui_status_mode_name(g_mode));
}
else
{
(void)snprintf(status_text, sizeof(status_text), "%s ERR %02X", ui_status_mode_name(g_mode), g_error_flags);
}
ui_status_copy_padded(line_buffer, status_text);
}
static void ui_status_refresh_display(void)
{
char line_1[UI_STATUS_LCD_COLUMNS + 1u];
char line_2[UI_STATUS_LCD_COLUMNS + 1u];
if (g_initialised == 0u)
{
return;
}
ui_status_build_line_1(line_1);
ui_status_build_line_2(line_2);
lcd1602_display_set_lines(line_1, line_2);
g_display_dirty = 0u;
}

19
App/Services/ui_status.h Normal file
View File

@ -0,0 +1,19 @@
/**
* @file ui_status.h
* @brief Future LCD/UI abstraction used by standalone profile mode.
*/
#ifndef UI_STATUS_H
#define UI_STATUS_H
#include <stdint.h>
#include "app_types.h"
void ui_status_init(void);
void ui_status_set_profile_name(const char *profile_name);
void ui_status_set_mode(app_mode_t mode);
void ui_status_set_error(uint8_t error_flags);
app_event_t ui_status_poll_event(uint32_t tick_10ms);
#endif /* UI_STATUS_H */