summaryrefslogtreecommitdiff
path: root/firmware/src
diff options
context:
space:
mode:
Diffstat (limited to 'firmware/src')
-rw-r--r--firmware/src/bsp.c38
-rw-r--r--firmware/src/cmd.c27
-rw-r--r--firmware/src/driver/f1956.c61
-rw-r--r--firmware/src/driver/mcp3202.c129
-rw-r--r--firmware/src/driver/mcp4725.c101
-rw-r--r--firmware/src/driver/si4468.c201
-rw-r--r--firmware/src/main.c343
-rw-r--r--firmware/src/module/drain.c123
-rw-r--r--firmware/src/module/gate.c78
-rw-r--r--firmware/src/peripheral/ring.c115
-rw-r--r--firmware/src/peripheral/spi.c106
-rw-r--r--firmware/src/peripheral/timer.c57
-rw-r--r--firmware/src/peripheral/twi.c114
-rw-r--r--firmware/src/peripheral/uart.c239
-rw-r--r--firmware/src/status.c46
-rw-r--r--firmware/src/util.c152
16 files changed, 1930 insertions, 0 deletions
diff --git a/firmware/src/bsp.c b/firmware/src/bsp.c
new file mode 100644
index 0000000..fdb8a39
--- /dev/null
+++ b/firmware/src/bsp.c
@@ -0,0 +1,38 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+#include "bsp.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <avr/io.h>
+#include <util/delay.h>
+#include <avr/interrupt.h>
+#include <avr/wdt.h>
+
+void bsp_soft_reset(void)
+{
+ cli();
+
+ MCUSR &= ~(1 << WDRF);
+
+ WDTCSR = (1 << WDCE) | (1 << WDE);
+ WDTCSR = (1 << WDP1);
+ WDTCSR = WDTCSR | 0x40;
+
+ MCUSR &= ~(1 << WDRF);
+
+ sei();
+
+ while(1){};
+}
diff --git a/firmware/src/cmd.c b/firmware/src/cmd.c
new file mode 100644
index 0000000..f55ed80
--- /dev/null
+++ b/firmware/src/cmd.c
@@ -0,0 +1,27 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "cmd.h"
+
+#include <avr/pgmspace.h>
+
+const char* const cmd_lut[CMD_SIZE][CMD_ENTRY_SIZE] PROGMEM = {
+ {CMD_POWER_GET_STR, CMD_POWER_GET_DESC},
+ {CMD_POWER_SET_STR, CMD_POWER_SET_DESC},
+ {CMD_TX_STR, CMD_TX_DESC},
+ {CMD_RX_STR, CMD_RX_DESC},
+ {CMD_DRAIN_SET_STR, CMD_DRAIN_SET_DESC},
+ {CMD_PROBE_STR, CMD_PROBE_DESC},
+ {CMD_RESET_STR, CMD_RESET_DESC},
+ {CMD_REPORT_STR, CMD_REPORT_DESC},
+ {CMD_HELP_STR, CMD_HELP_DESC}
+ };
diff --git a/firmware/src/driver/f1956.c b/firmware/src/driver/f1956.c
new file mode 100644
index 0000000..3af5fba
--- /dev/null
+++ b/firmware/src/driver/f1956.c
@@ -0,0 +1,61 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/spi.h"
+#include "driver/f1956.h"
+
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <util/delay.h>
+
+void att_init(uint8_t init_value)
+{
+ F1956_DEV_0_DDR |= (1 << F1956_DEV_0_SS);
+ F1956_DEV_0_PORT |= (1 << F1956_DEV_0_SS);
+
+ att_set(F1956_ID_INPUT_ATT, init_value);
+ _delay_ms(10);
+}
+
+void att_set(
+ uint8_t id,
+ uint8_t value
+)
+{
+ uint8_t dev_id = 0x0;
+ uint8_t dev_ss = 0x0;
+ uint8_t* dev_port = 0x0;
+ uint8_t cmd_buf[2] = {0x0, 0x0};
+
+ switch (id)
+ {
+ case F1956_ID_INPUT_ATT:
+ dev_id = F1956_DEV_INPUT_ATT;
+ break;
+ }
+
+ switch (dev_id)
+ {
+ case F1956_DEV_INPUT_ATT:
+ dev_ss = F1956_DEV_0_SS;
+ dev_port = (uint8_t*)&F1956_DEV_0_PORT;
+ break;
+ }
+
+ cmd_buf[0] = value & 0x7F;
+ cmd_buf[1] = F1956_DEV_INPUT_ATT_ADDR;
+
+ spi_start(cmd_buf, cmd_buf, sizeof(cmd_buf), dev_port, dev_ss);
+ spi_flush();
+}
diff --git a/firmware/src/driver/mcp3202.c b/firmware/src/driver/mcp3202.c
new file mode 100644
index 0000000..07f84db
--- /dev/null
+++ b/firmware/src/driver/mcp3202.c
@@ -0,0 +1,129 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/spi.h"
+#include "driver/mcp3202.h"
+
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <util/delay.h>
+
+void adc_init(void)
+{
+ MCP3202_DEV_0_DDR |= (1 << MCP3202_DEV_0_SS);
+ MCP3202_DEV_1_DDR |= (1 << MCP3202_DEV_1_SS);
+
+ MCP3202_DEV_0_PORT &= ~(1 << MCP3202_DEV_0_SS);
+ MCP3202_DEV_1_PORT &= ~(1 << MCP3202_DEV_1_SS);
+
+ _delay_ms(TRANSACT_DELAY_MS);
+
+ MCP3202_DEV_0_PORT |= (1 << MCP3202_DEV_0_SS);
+ MCP3202_DEV_1_PORT |= (1 << MCP3202_DEV_1_SS);
+
+ _delay_ms(TRANSACT_DELAY_MS);
+}
+
+uint16_t adc_read(
+ uint8_t id,
+ uint8_t conv_bypass
+)
+{
+ uint8_t dev_id = 0x0;
+ uint8_t dev_ch = 0x0;
+ uint8_t dev_ss = 0x0;
+ uint8_t* dev_port = 0x0;
+ uint8_t cmd_buf[3] = {0x0, 0x0, 0x0};
+ uint16_t adc_daq = 0x0;
+
+ switch(id)
+ {
+ case MCP3202_ID_FW_POWER:
+ dev_id = MCP3202_DEV_FW_POWER;
+ dev_ch = MCP3202_CHN_FW_POWER;
+ break;
+
+ case MCP3202_ID_REV_POWER:
+ dev_id = MCP3202_DEV_REV_POWER;
+ dev_ch = MCP3202_CHN_REV_POWER;
+ break;
+
+ case MCP3202_ID_DRAIN_VOLT:
+ dev_id = MCP3202_DEV_DRAIN_VOLT;
+ dev_ch = MCP3202_CHN_DRAIN_VOLT;
+ break;
+
+ case MCP3202_ID_DRAIN_AMP:
+ dev_id = MCP3202_DEV_DRAIN_AMP;
+ dev_ch = MCP3202_CHN_DRAIN_AMP;
+ break;
+ }
+
+ switch(dev_id)
+ {
+ case MCP3202_DEV_0:
+ dev_port = (uint8_t*)&MCP3202_DEV_0_PORT;
+ dev_ss = MCP3202_DEV_0_SS;
+ break;
+
+ case MCP3202_DEV_1:
+ dev_port = (uint8_t*)&MCP3202_DEV_1_PORT;
+ dev_ss = MCP3202_DEV_1_SS;
+ break;
+ }
+
+ cmd_buf[0] = MCP3202_REQ_START;
+ cmd_buf[1] = (dev_ch == MCP3202_CHN_0) ? MCP3202_REQ_CH0 : MCP3202_REQ_CH1;
+ cmd_buf[2] = MCP3202_REQ_PAD;
+
+ spi_start(cmd_buf, cmd_buf, sizeof(cmd_buf), dev_port, dev_ss);
+ spi_flush();
+
+ adc_daq = (uint16_t)(cmd_buf[MCP3202_DAQ_MSB]);
+ adc_daq |= ((uint16_t)(cmd_buf[MCP3202_DAQ_LSB] & 0xF) << 8);
+ adc_daq = adc_daq & 0xFFF;
+
+ if (conv_bypass == 0)
+ {
+ switch(id)
+ {
+ case MCP3202_ID_DRAIN_VOLT:
+ adc_daq = (uint32_t)(((uint32_t)adc_daq * MCP3202_DRAIN_VOLT_GAIN)/10);
+ break;
+ case MCP3202_ID_DRAIN_AMP:
+ adc_daq = (uint32_t)(((uint32_t)adc_daq * 10)/MCP3202_DRAIN_AMP_GAIN);
+ break;
+ }
+ }
+
+ return (uint16_t)adc_daq;
+}
+
+uint16_t adc_read_n(
+ uint8_t id,
+ uint8_t n
+)
+{
+ uint32_t x = 0;
+ uint8_t i = 0;
+
+ for (i = 0; i < n; i++)
+ {
+ x += adc_read(id, 0);
+ }
+
+ x /= n;
+
+ return (uint16_t)x;
+}
diff --git a/firmware/src/driver/mcp4725.c b/firmware/src/driver/mcp4725.c
new file mode 100644
index 0000000..4cb9bd6
--- /dev/null
+++ b/firmware/src/driver/mcp4725.c
@@ -0,0 +1,101 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/twi.h"
+#include "driver/mcp4725.h"
+
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <util/delay.h>
+
+void dac_init(uint16_t init_value)
+{
+ dac_write(MCP4725_ID_GATE_VOLT, init_value, 0);
+ dac_write(MCP4725_ID_DRAIN_VOLT, init_value, 0);
+
+ _delay_ms(TRANSACT_DELAY_MS);
+}
+
+void dac_write(
+ uint8_t id,
+ uint16_t value,
+ uint8_t conv_bypass
+)
+{
+ uint8_t dev_addr = 0x0;
+ uint8_t cmd_buf[2] = {0x0, 0x0};
+ uint8_t drain_mode = 0x0;
+
+ switch(id)
+ {
+ case MCP4725_ID_GATE_VOLT:
+ dev_addr = TWI_ADDRESS_W(MCP4725_DEV_GATE_VOLT_ADDR);
+ break;
+ case MCP4725_ID_DRAIN_VOLT:
+ dev_addr = TWI_ADDRESS_W(MCP4725_DEV_DRAIN_VOLT_ADDR);
+ drain_mode = 1;
+ break;
+ }
+
+ if (conv_bypass == 0)
+ {
+ if (drain_mode == 1)
+ {
+ value = (uint16_t)(((uint32_t)value * 10)/MCP4725_DRAIN_GAIN);
+ }
+ }
+
+ cmd_buf[0] = ((value >> 8) & 0xF) | MCP4725_BIT_PD0(0) | MCP4725_BIT_PD1(0);
+ cmd_buf[1] = value & 0xFF;
+
+ twi_start(dev_addr, cmd_buf, sizeof(cmd_buf));
+ twi_flush();
+}
+
+uint16_t dac_read(
+ uint8_t id,
+ uint8_t conv_bypass
+)
+{
+ uint8_t dev_addr = 0x0;
+ uint8_t cmd_buf[3] = {0x0, 0x0, 0x0};
+ uint16_t value = 0x0;
+ uint8_t drain_mode = 0x0;
+
+ switch(id)
+ {
+ case MCP4725_ID_GATE_VOLT:
+ dev_addr = TWI_ADDRESS_R(MCP4725_DEV_GATE_VOLT_ADDR);
+ break;
+ case MCP4725_ID_DRAIN_VOLT:
+ dev_addr = TWI_ADDRESS_R(MCP4725_DEV_DRAIN_VOLT_ADDR);
+ drain_mode = 1;
+ break;
+ }
+
+ twi_start(dev_addr, cmd_buf, sizeof(cmd_buf));
+ twi_flush();
+
+ value = (((uint16_t)cmd_buf[1]) << 4) | (cmd_buf[2] >> 4);
+
+ if (conv_bypass == 0)
+ {
+ if (drain_mode)
+ {
+ value = (uint16_t)(((uint32_t)value * MCP4725_DRAIN_GAIN)/10);
+ }
+ }
+
+ return (value & 0xFFF);
+}
diff --git a/firmware/src/driver/si4468.c b/firmware/src/driver/si4468.c
new file mode 100644
index 0000000..17dd77d
--- /dev/null
+++ b/firmware/src/driver/si4468.c
@@ -0,0 +1,201 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/spi.h"
+
+#include "driver/si4468.h"
+#include "driver/radio_config.h"
+#include "driver/device.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <util/delay.h>
+#include <util/atomic.h>
+#include <avr/pgmspace.h>
+
+
+static const uint8_t config[] PROGMEM = RADIO_CONFIGURATION_DATA_ARRAY;
+
+void si4468_init(void)
+{
+ SI4468_DEV_0_DDR |= (1 << SI4468_DEV_0_SS);
+ SI4468_DEV_0_PORT |= (1 << SI4468_DEV_0_SS);
+}
+
+void si44468_get_device(
+ int id,
+ t_dev* device
+)
+{
+ uint8_t dev_id = 0x0;
+ uint8_t dev_ss = 0x0;
+ uint8_t* dev_port = 0x0;
+
+ device->dev_id = 0;
+ device->dev_ss = 0;
+ device->dev_port = 0;
+
+ switch (id)
+ {
+ case SI4468_ID_RADIO:
+ dev_id = SI4468_DEV_RADIO;
+ break;
+ }
+
+ switch (dev_id)
+ {
+ case SI4468_DEV_RADIO:
+ dev_ss = SI4468_DEV_0_SS;
+ dev_port = (uint8_t*)&SI4468_DEV_0_PORT;
+ break;
+ }
+
+ device->dev_id = dev_id;
+ device->dev_ss = dev_ss;
+ device->dev_port = dev_port;
+}
+
+static uint8_t si4468_get_response(
+ void* buf,
+ uint8_t len
+)
+{
+ t_dev device;
+ uint8_t cts = 0x0;
+ uint8_t* cmd_buf = 0x0;
+ uint16_t cmd_size = 0x0;
+ uint8_t i = 0;
+
+ si44468_get_device(SI4468_ID_RADIO, &device);
+
+ cmd_size = len + SI4468_GET_RESP_LEN;
+ cmd_buf = (uint8_t*)malloc((cmd_size));
+
+ cmd_buf[0] = SI4468_CMD_READ_CMD_BUFF;
+ cmd_buf[1] = SI4468_CTS;
+
+ spi_start(cmd_buf, cmd_buf, SI4468_GET_RESP_LEN, device.dev_port, device.dev_ss);
+ spi_flush();
+
+ cts = (cmd_buf[0] == SI4468_CTS);
+
+ if(cts)
+ {
+ if (len > 0)
+ {
+ for(i = 0x0; i < cmd_size; i++)
+ {
+ cmd_buf[i] = SI4468_CTS;
+ }
+
+ cmd_buf[0] = SI4468_CMD_READ_CMD_BUFF;
+
+ spi_start(cmd_buf, cmd_buf, cmd_size, (uint8_t*)&SI4468_DEV_0_PORT, SI4468_DEV_0_SS);
+ spi_flush();
+
+ for(i = SI4468_GET_RESP_LEN; i < cmd_size; i++)
+ {
+ ((uint8_t*)buf)[i - SI4468_GET_RESP_LEN] = cmd_buf[i];
+ }
+
+ }
+ }
+
+ free(cmd_buf);
+
+ return cts;
+}
+
+static uint8_t si4468_wait_for_response(
+ void* buf,
+ uint8_t buf_len
+)
+{
+ while(!si4468_get_response(buf, buf_len))
+ {
+ _delay_ms(10);
+ }
+
+ return 1;
+}
+
+static void si4468_exec_api(
+ void* in,
+ size_t in_len,
+ void* out,
+ uint8_t out_len
+)
+{
+ t_dev device;
+ si44468_get_device(SI4468_ID_RADIO, &device);
+
+ if (si4468_wait_for_response(0, 0))
+ {
+ spi_start(in, 0, in_len, device.dev_port, device.dev_ss);
+ spi_flush();
+
+ if (out != 0)
+ {
+ si4468_wait_for_response(out, out_len);
+ }
+ }
+}
+
+void si4468_apply_startup_config(void)
+{
+ uint16_t i = 0;
+ uint8_t buf[100];
+
+ for(i = 0; i < sizeof(config); i++)
+ {
+ memcpy_P(buf, &config[i], sizeof(buf));
+ uint8_t tmp = buf[0];
+ si4468_exec_api(&buf[1], tmp, 0, 0);
+ i += tmp;
+ }
+}
+
+void si4468_get_info(
+ si4468_info_t* info
+)
+{
+ uint8_t data[8] = {
+ SI4468_CMD_PART_INFO
+ };
+
+ si4468_exec_api(data, 1, data, 8);
+
+ info->chipRev = data[0];
+ info->part = (data[1]<<8) | data[2];
+ info->partBuild = data[3];
+ info->id = (data[4]<<8) | data[5];
+ info->customer = data[6];
+ info->romId = data[7];
+}
+
+void si4468_tx_mode(void)
+{
+ uint8_t packet[] = {SI4468_CMD_START_TX, 0,0,0, 0,0,0 };
+ si4468_exec_api(packet, sizeof(packet), 0, 0);
+}
+
+void si4468_rx_mode(void)
+{
+ uint8_t packet[] = {SI4468_CMD_CHANGE_STATE, 0x3};
+ si4468_exec_api(packet, sizeof(packet), 0, 0);
+}
diff --git a/firmware/src/main.c b/firmware/src/main.c
new file mode 100644
index 0000000..02d103d
--- /dev/null
+++ b/firmware/src/main.c
@@ -0,0 +1,343 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/timer.h"
+#include "peripheral/uart.h"
+#include "peripheral/twi.h"
+#include "peripheral/spi.h"
+
+#include "driver/mcp3202.h"
+#include "driver/mcp4725.h"
+#include "driver/f1956.h"
+#include "driver/si4468.h"
+
+#include "module/gate.h"
+#include "module/drain.h"
+
+#include "cmd.h"
+#include "bsp.h"
+#include "status.h"
+#include "util.h"
+
+#include <avr/io.h>
+#include <util/delay.h>
+#include <avr/interrupt.h>
+#include <avr/pgmspace.h>
+#include <avr/wdt.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+extern t_gate_status gate_status;
+extern t_drain_status drain_status;
+extern t_system_status system_status;
+
+void system_info(void)
+{
+ CMD_PRINT("SYSTEM BOOT v" SYS_VER_STR);
+ CMD_PRINT("SYSTEM INIT");
+ printf("%d BYTES AVAILABLE\n", util_get_free_mem());
+}
+
+void setup(void)
+{
+ cli();
+
+ gate_status.status = GATE_STATUS_INACTIVE;
+ drain_status.status = DRAIN_STATUS_INACTIVE;
+ system_status.status = SYSTEM_STATUS_IDLE;
+
+ wdt_reset();
+ wdt_disable();
+
+ timer_init();
+
+ uart_init();
+ twi_init();
+ spi_init();
+
+ sei();
+
+ _delay_ms(INIT_DELAY_MS);
+
+ si4468_init();
+
+ adc_init();
+ att_init(0);
+ dac_init(0);
+
+ _delay_ms(INIT_DELAY_MS);
+
+ si4468_apply_startup_config();
+
+ _delay_ms(INIT_DELAY_MS);
+
+ si4468_rx_mode();
+}
+
+void report_system_status()
+{
+ uint16_t gate_v_set = 0x0;
+ uint16_t gate_v_count = 0x0;
+
+ uint16_t drain_v_set = 0x0;
+ uint16_t drain_v_adc = 0x0;
+ uint16_t drain_amp_count = 0x0;
+ uint16_t drain_amp = 0x0;
+ uint16_t drain_v_count = 0x0;
+
+ uint16_t fw_pwr_adc = 0x0;
+ uint16_t fw_pwr_volt = 0x0;
+ int16_t fw_pwr = 0x0;
+
+ // gate status
+
+ gate_status.q_drain = GATE_PROBE_DRAIN;
+ gate_v_count = dac_read(MCP4725_ID_GATE_VOLT, 0);
+ util_count_to_volt(gate_v_count, &gate_v_set);
+ gate_status.v_set = gate_v_set;
+
+
+ printf_P(PSTR("- GATE -\n"));
+ printf_P(PSTR("STATUS : %s\n"),
+ pgm_read_word(&gate_lut[gate_status.status]));
+ printf_P(PSTR("Q-VOLT (mV) : %d\n"), gate_status.q_volt);
+ printf_P(PSTR("Q-AMP (mA) : %d\n"), gate_status.q_amp);
+ printf_P(PSTR("Q-TIME (ms) : %d\n"), gate_status.q_time);
+ printf_P(PSTR("V-SET (mV) : %d\n"), gate_status.v_set);
+ printf_P(PSTR("Q-DRAIN (mV) : %d\n"), gate_status.q_drain);
+
+ // drain status
+ drain_v_count = dac_read(MCP4725_ID_DRAIN_VOLT, 0);
+ util_count_to_volt(drain_v_count, &drain_v_set);
+ drain_status.v_set = drain_v_set;
+
+ drain_v_count = adc_read(MCP3202_ID_DRAIN_VOLT, 0);
+ util_count_to_volt(drain_v_count, &drain_v_adc);
+ drain_status.v_adc = drain_v_adc;
+
+ drain_amp_count = adc_read(MCP3202_ID_DRAIN_AMP, 0);
+ util_count_to_volt(drain_amp_count, &drain_amp);
+ drain_status.amp = drain_amp;
+
+ printf_P(PSTR("- DRAIN -\n"));
+ printf_P(PSTR("STATUS : %s\n"),
+ pgm_read_word(&drain_lut[drain_status.status]));
+ printf_P(PSTR("V-ADC (mV) : %d\n"), drain_status.v_adc);
+ printf_P(PSTR("V-SET (mV) : %d\n"), drain_status.v_set);
+ printf_P(PSTR("AMP (mA) : %d\n"), drain_status.amp);
+
+ // system status
+ fw_pwr_adc = adc_read_n(MCP3202_ID_FW_POWER, DRAIN_POWER_SAMPLES);
+ util_count_to_volt(fw_pwr_adc, &fw_pwr_volt);
+ util_count_to_pwr(fw_pwr_adc, &fw_pwr);
+ system_status.v_fw = fw_pwr_volt;
+ system_status.p_fw = fw_pwr;
+
+ printf_P(PSTR("- SYSTEM -\n"));
+ printf_P(PSTR("STATUS : %s\n"),
+ pgm_read_word(&system_lut[system_status.status]));
+ printf_P(PSTR("V-FWD (mV) : %d\n"), system_status.v_fw);
+ printf_P(PSTR("P-FWD (cdBm) : %d\n"), system_status.p_fw);
+ printf_P(PSTR("P-SET (cdBm) : %d\n"), system_status.p_set);
+ printf_P(PSTR("T-DELTA (ms) : %d\n"), system_status.d_time);
+}
+
+void loop()
+{
+ char uart_buffer[UART_BUF_SIZE];
+ char cmd_buffer[UART_BUF_SIZE];
+ char* cmd_tok = 0x0;
+ int16_t argv[CMD_MAX_ARGS];
+ uint16_t argc = 0x0;
+ bool gate_probed = false;
+
+ while (1)
+ {
+ cmd_tok = 0x0;
+ argc = 0x0;
+
+ if(uart_ngets(uart_buffer, UART_BUF_SIZE))
+ {
+ cmd_tok = strtok(uart_buffer, " ");
+ strcpy(cmd_buffer, cmd_tok);
+
+ while ((cmd_tok = strtok(NULL, " ")) != NULL) {
+ argv[argc++] = atoi(cmd_tok);
+ }
+
+ if (CMD_SWITCH(CMD_REPORT, uart_buffer))
+ {
+ CMD_PRINT("DUMP SYSTEM REPORT");
+ report_system_status();
+ }
+
+ else if (CMD_SWITCH(CMD_POWER_GET, uart_buffer))
+ {
+ CMD_PRINT("DUMP POWER ADC");
+ uint16_t fw_pwr_adc = adc_read_n(MCP3202_ID_FW_POWER,10);
+ CMD_PRINT_FMT("%d", fw_pwr_adc);
+ }
+
+ else if (CMD_SWITCH(CMD_PROBE, uart_buffer))
+ {
+ uint16_t gate_probe_count = 0x0;
+ uint16_t drain_current = 0x0;
+ uint32_t time_start = 0x0;
+ uint16_t time_delta = 0x0;
+
+ if (!gate_probed)
+ {
+ CMD_PRINT("STARTING Q-PROBE");
+ CMD_PRINT_FMT("DRAIN_Q_AMP = %d mA", DRAIN_Q_AMP);
+
+ time_start = timer_millis();
+
+ si4468_rx_mode();
+ drain_current = gate_probe(DRAIN_Q_AMP, &gate_probe_count);
+
+ time_delta = (uint16_t)(timer_millis() - time_start);
+ gate_status.q_time = time_delta;
+
+ CMD_PRINT_FMT("PROBE COMPLETED IN %d ms", time_delta);
+
+ if (drain_current >= DRAIN_Q_AMP)
+ {
+ gate_status.status = GATE_STATUS_PROBE_SUCCESS;
+ CMD_PRINT_FMT("Q-PROBE SUCCEEDED, IQ = %d mA", drain_current);
+ gate_probed = true;
+ }
+ else
+ {
+ gate_status.status = GATE_STATUS_PROBE_ERROR;
+ CMD_PRINT_FMT("Q-PROBE FAILED, IQ = %d mA", drain_current);
+ continue;
+ }
+ }
+ else
+ {
+ CMD_ERROR("ACTIVE GATE PROBE");
+ }
+ }
+
+ else if (CMD_SWITCH(CMD_POWER_SET, cmd_buffer))
+ {
+ if (argc != 1)
+ {
+ CMD_ERROR("USAGE: POWER <cdBm>");
+ continue;
+ }
+
+ int16_t rf_power = argv[0];
+
+ if (rf_power < DRAIN_POWER_LOW_LIM ||
+ rf_power > DRAIN_POWER_HIGH_LIM ||
+ (rf_power % DRAIN_POWER_MODULUS) != 0)
+ {
+ CMD_ERROR("INVALID INPUT POWER");
+ continue;
+ }
+
+ if (!gate_probed)
+ {
+ CMD_PRINT("NO GATE PROBE");
+ continue;
+
+ }
+
+ system_status.p_set = rf_power;
+
+ CMD_PRINT_FMT("POWER SETPOINT = %d cdBm", rf_power);
+ CMD_PRINT("STARTING LINEAR SWEEP");
+ drain_sweep_pwr(rf_power);
+ CMD_PRINT_FMT("SWEEP COMPLETED IN %d ms", system_status.d_time);
+ }
+
+ else if (CMD_SWITCH(CMD_DRAIN_SET, cmd_buffer))
+ {
+ if (argc != 1)
+ {
+ CMD_ERROR("USAGE: DRAIN <count>");
+ continue;
+ }
+
+ uint16_t drain_count = argv[0];
+ if (drain_count > 4096)
+ {
+ CMD_ERROR("RANGE ERROR");
+ continue;
+ }
+
+ dac_write(MCP4725_ID_DRAIN_VOLT, drain_count, 1);
+ }
+
+ else if (CMD_SWITCH(CMD_RX, uart_buffer))
+ {
+ CMD_PRINT("RADIO RX MODE");
+ si4468_rx_mode();
+ }
+
+ else if (CMD_SWITCH(CMD_TX, uart_buffer))
+ {
+ CMD_PRINT("RADIO TX MODE");
+ si4468_tx_mode();
+ }
+
+ else if (CMD_SWITCH(CMD_HELP, uart_buffer))
+ {
+ uint16_t i = 0x0;
+
+ CMD_PRINT("DUMP COMMANDS");
+ printf("- COMMANDS -\n");
+ for(i = 0; i < CMD_SIZE; i++)
+ {
+ printf_P(PSTR("%-10s - %-10s\n"),
+ pgm_read_word(&cmd_lut[i][0]),
+ pgm_read_word(&cmd_lut[i][1]));
+ }
+ }
+
+ else if (CMD_SWITCH(CMD_LINK_REPLY, uart_buffer))
+ {
+ CMD_PRINT("HELLO");
+ }
+
+ else if (CMD_SWITCH(CMD_RESET, uart_buffer))
+ {
+ CMD_PRINT("SOFT RESET");
+ _delay_ms(10);
+
+ bsp_soft_reset();
+ }
+
+ else
+ {
+ CMD_PRINT("UNKNOWN COMMAND");
+ }
+ }
+ }
+}
+
+int main()
+{
+ setup();
+ system_info();
+
+ loop();
+
+ return 0;
+}
+
+
diff --git a/firmware/src/module/drain.c b/firmware/src/module/drain.c
new file mode 100644
index 0000000..1cb6110
--- /dev/null
+++ b/firmware/src/module/drain.c
@@ -0,0 +1,123 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/timer.h"
+
+#include "driver/mcp3202.h"
+#include "driver/mcp4725.h"
+#include "driver/si4468.h"
+
+#include "module/gate.h"
+#include "module/drain.h"
+
+#include "util.h"
+#include "status.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <avr/interrupt.h>
+#include <util/delay.h>
+
+extern t_drain_status drain_status;
+extern t_system_status system_status;
+
+uint16_t drain_sweep_pwr(
+ int16_t target_fw_pwr
+)
+{
+ uint16_t drain_count = 0x0;
+ uint16_t drain_inc = 0x0;
+ uint16_t fw_pwr_adc = 0x0;
+ int16_t fw_pwr = 0x0;
+ uint16_t test_count = 0x0;
+ int16_t test_boundary = DRAIN_TEST_BOUNDARY;
+
+ system_status.status = SYSTEM_STATUS_BUSY;
+ drain_status.status = DRAIN_STATUS_POWER_CONTROL;
+
+ drain_count = dac_read(MCP4725_ID_DRAIN_VOLT, 1);
+ _delay_ms(TRANSACT_DELAY_MS);
+
+ si4468_tx_mode();
+ _delay_ms(TRANSACT_DELAY_MS);
+
+ dac_write(MCP4725_ID_DRAIN_VOLT, drain_count, 1);
+ _delay_ms(TRANSACT_DELAY_MS);
+
+ fw_pwr_adc = adc_read_n(MCP3202_ID_FW_POWER, 10);
+ util_count_to_pwr(fw_pwr_adc, &fw_pwr);
+
+ _delay_ms(TRANSACT_DELAY_MS);
+
+ timer_reset();
+ uint32_t time_start = timer_millis();
+ bool timeout = false;
+
+ while ((test_count < DRAIN_TEST_COUNT) &&
+ (drain_count < DRAIN_PROBE_LIMIT))
+ {
+ if ((timer_millis() - time_start) > DRAIN_SETPOINT_TIMEOUT_MS)
+ {
+ timeout = true;
+ break;
+ }
+
+ if (abs(fw_pwr - target_fw_pwr) > DRAIN_PROBE_INC_THRESHOLD)
+ {
+ drain_inc = DRAIN_PROBE_INC_COARSE;
+ }
+ else
+ {
+ drain_inc = DRAIN_PROBE_INC_FINE;
+ }
+
+ if (fw_pwr < (target_fw_pwr))
+ {
+ drain_count += drain_inc;
+ }
+ else if (fw_pwr > (target_fw_pwr))
+ {
+ drain_count -= drain_inc;
+ }
+
+ dac_write(MCP4725_ID_DRAIN_VOLT, drain_count, 1);
+ _delay_us(DRAIN_LOOP_DELAY_US);
+
+ fw_pwr_adc = adc_read_n(MCP3202_ID_FW_POWER, DRAIN_POWER_SAMPLES);
+ util_count_to_pwr(fw_pwr_adc, &fw_pwr);
+
+ if (fw_pwr <= (target_fw_pwr + test_boundary) &&
+ fw_pwr >= (target_fw_pwr - test_boundary))
+ {
+ test_count += 1;
+ }
+ }
+
+ system_status.d_time = (uint16_t)(timer_millis() - time_start);
+
+ if (timeout)
+ {
+ system_status.status = SYSTEM_STATUS_SETPOINT_TIMEOUT;
+ }
+ else if(test_count >= DRAIN_TEST_COUNT)
+ {
+ system_status.status = SYSTEM_STATUS_SETPOINT_REACHED;
+ }
+ else
+ {
+ system_status.status = SYSTEM_STATUS_SETPOINT_FAIL;
+ }
+
+ return test_count;
+}
diff --git a/firmware/src/module/gate.c b/firmware/src/module/gate.c
new file mode 100644
index 0000000..b143eab
--- /dev/null
+++ b/firmware/src/module/gate.c
@@ -0,0 +1,78 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "driver/mcp3202.h"
+#include "driver/mcp4725.h"
+
+#include "module/gate.h"
+
+#include "util.h"
+#include "status.h"
+
+#include <avr/interrupt.h>
+#include <util/delay.h>
+
+extern t_gate_status gate_status;
+extern t_system_status system_status;
+
+uint16_t gate_probe(
+ uint16_t target_amp,
+ uint16_t* probe_result
+)
+{
+ uint16_t probe_volt = 0x0;
+ uint16_t drain_amp = 0x0;
+ uint16_t gate_count = 0x0;
+ uint16_t drain_count = 0x0;
+
+ system_status.status = SYSTEM_STATUS_BUSY;
+
+ util_volt_to_count(GATE_PROBE_DRAIN, &drain_count);
+ util_volt_to_count(GATE_PROBE_START, &gate_count );
+
+ dac_write(MCP4725_ID_GATE_VOLT, gate_count, 0);
+ _delay_ms(TRANSACT_DELAY_MS);
+ dac_write(MCP4725_ID_DRAIN_VOLT, drain_count, 0);
+ _delay_ms(TRANSACT_DELAY_MS);
+
+ while((drain_amp < target_amp) && probe_volt < GATE_PROBE_LIMIT)
+ {
+ gate_count += GATE_PROBE_INC;
+ dac_write(MCP4725_ID_GATE_VOLT, gate_count, 0);
+ _delay_us(GATE_LOOP_DELAY_US);
+
+ drain_count = adc_read(MCP3202_ID_DRAIN_AMP, 0);
+ util_count_to_volt(drain_count, &drain_amp);
+ util_count_to_volt(gate_count, &probe_volt);
+ }
+
+ _delay_ms(TRANSACT_DELAY_MS);
+ drain_count = adc_read(MCP3202_ID_DRAIN_AMP, 0);
+ util_count_to_volt(drain_count, &drain_amp);
+
+ if(GATE_PA_POWER_DOWN)
+ {
+ _delay_ms(TRANSACT_DELAY_MS);
+ dac_write(MCP4725_ID_DRAIN_VOLT, 0, 0);
+ }
+
+ gate_status.q_amp = drain_amp;
+ gate_status.q_volt = probe_volt;
+
+ *probe_result = probe_volt;
+
+ system_status.status = SYSTEM_STATUS_IDLE;
+
+ return drain_amp;
+}
diff --git a/firmware/src/peripheral/ring.c b/firmware/src/peripheral/ring.c
new file mode 100644
index 0000000..eacb654
--- /dev/null
+++ b/firmware/src/peripheral/ring.c
@@ -0,0 +1,115 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "peripheral/ring.h"
+
+#define RING_INC_ROLL_OVER(n, s, e) (((n)+1>=(e)) ? (s) : (n)+1)
+
+ring_t ring_init(
+ uint8_t *buf,
+ size_t len
+)
+{
+ return RING_INIT(buf, len);
+}
+
+bool ring_is_empty(
+ ring_t ring
+)
+{
+ return ring.read == ring.write;
+}
+
+bool ring_is_full(
+ ring_t ring
+)
+{
+ return RING_INC_ROLL_OVER(ring.write, ring.buf, ring.end) == ring.read;
+}
+
+size_t ring_push_available(
+ ring_t ring
+)
+{
+ if(ring.write < ring.read)
+ return ring.read - ring.write - 1;
+ else
+ return (ring.end - ring.buf) - (ring.write - ring.read) - 1;
+}
+
+size_t ring_pop_available(
+ ring_t ring
+)
+{
+ if(ring.read <= ring.write)
+ return ring.write - ring.read;
+ else
+ return (ring.end - ring.buf) - (ring.read - ring.write);
+}
+
+bool ring_push(
+ ring_t *ring,
+ uint8_t data
+)
+{
+ if(ring_is_full(*ring))
+ return 1;
+
+ *ring->write = data;
+ ring->write = RING_INC_ROLL_OVER(ring->write, ring->buf, ring->end);
+
+ return 0;
+}
+
+bool ring_push_over(
+ ring_t *ring,
+ uint8_t data
+)
+{
+ *ring->write = data;
+ ring->write = RING_INC_ROLL_OVER(ring->write, ring->buf, ring->end);
+
+ if(ring->read == ring->write)
+ {
+ ring->read = RING_INC_ROLL_OVER(ring->read, ring->buf, ring->end);
+ return 1;
+ }
+
+ return 0;
+}
+
+bool ring_pop(
+ ring_t *ring,
+ uint8_t *data
+)
+{
+ if(ring_is_empty(*ring))
+ return 1;
+
+ *data = *ring->read;
+ ring->read = RING_INC_ROLL_OVER(ring->read, ring->buf, ring->end);
+
+ return 0;
+}
+
+bool ring_peek(
+ ring_t *ring,
+ uint8_t *data
+)
+{
+ if(ring_is_empty(*ring))
+ return 1;
+
+ *data = *ring->read;
+
+ return 0;
+}
diff --git a/firmware/src/peripheral/spi.c b/firmware/src/peripheral/spi.c
new file mode 100644
index 0000000..884985d
--- /dev/null
+++ b/firmware/src/peripheral/spi.c
@@ -0,0 +1,106 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/spi.h"
+
+#include <stdio.h>
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <util/delay.h>
+
+#if SPI_MODE == 0
+ #define CPOL_VALUE 0
+ #define CPHA_VALUE 0
+#elif SPI_MODE == 1
+ #define CPOL_VALUE 0
+ #define CPHA_VALUE 1
+#elif SPI_MODE == 2
+ #define CPOL_VALUE 1
+ #define CPHA_VALUE 0
+#elif SPI_MODE == 3
+ #define CPOL_VALUE 1
+ #define CPHA_VALUE 1
+#else
+ #error "No valid SPI_MODE defined!"
+#endif
+
+#if SPI_DORD == 0
+ #define DORD_VALUE 0
+#elif SPI_DORD == 1
+ #define DORD_VALUE 1
+#else
+ #error "No valid SPI_DORD defined!"
+#endif
+
+static volatile uint8_t *spi_out;
+static volatile uint8_t *spi_in;
+static volatile size_t spi_len;
+static volatile uint8_t *spi_port;
+static volatile uint8_t spi_pin;
+
+void spi_init(void)
+{
+ DDRB |= (1 << DDB2) | (1 << DDB3) | (1 << DDB5);
+
+ SPSR |= (SPI2X_VALUE << SPI2X);
+ SPCR |= (1 << SPIE) | (1 << SPE) | (DORD_VALUE << DORD) | (1 << MSTR)
+ | (CPOL_VALUE << CPOL) | (CPHA_VALUE << CPHA)
+ | (SPR1_VALUE << SPR1) | (SPR0_VALUE << SPR0);
+}
+
+bool spi_busy(void)
+{
+ return spi_len;
+}
+
+void spi_flush(void)
+{
+ while(spi_len);
+}
+
+void spi_start(
+ uint8_t *out,
+ uint8_t *in,
+ size_t len,
+ uint8_t *port,
+ uint8_t pin
+)
+{
+ spi_flush();
+
+ spi_out = out;
+ spi_in = in;
+ spi_len = len;
+ spi_port = port;
+ spi_pin = pin;
+
+ if(spi_port)
+ *spi_port &= ~(1 << spi_pin);
+
+ SPDR = *spi_out++;
+}
+
+ISR(SPI_STC_vect)
+{
+ if(spi_in)
+ {
+ *spi_in++ = SPDR;
+ }
+
+ if(--spi_len)
+ SPDR = *spi_out++;
+ else
+ if(spi_port)
+ *spi_port |= (1 << spi_pin);
+}
diff --git a/firmware/src/peripheral/timer.c b/firmware/src/peripheral/timer.c
new file mode 100644
index 0000000..1caff1c
--- /dev/null
+++ b/firmware/src/peripheral/timer.c
@@ -0,0 +1,57 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/timer.h"
+
+#include <stdint.h>
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <util/atomic.h>
+
+uint32_t millis_count = 0;
+
+void timer_init(void)
+{
+ uint32_t ctc_overflow;
+
+ ctc_overflow = ((F_CPU / 1000) / 8);
+ TCCR1B |= (1 << WGM12) | (1 << CS11);
+
+ OCR1AH = (ctc_overflow >> 8);
+ OCR1AL = ctc_overflow;
+
+ TIMSK1 |= (1 << OCIE1A);
+}
+
+void timer_reset(void)
+{
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ millis_count = 0;
+ }
+}
+
+uint32_t timer_millis(void)
+{
+ uint32_t millis;
+
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ millis = millis_count;
+ }
+ return millis;
+}
+
+ISR(TIMER1_COMPA_vect)
+{
+ millis_count++;
+}
diff --git a/firmware/src/peripheral/twi.c b/firmware/src/peripheral/twi.c
new file mode 100644
index 0000000..671490b
--- /dev/null
+++ b/firmware/src/peripheral/twi.c
@@ -0,0 +1,114 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/twi.h"
+
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <util/delay.h>
+#include <util/twi.h>
+
+#define TWBR_VALUE ((F_CPU/TWI_FREQUENCY - 16) / (2 * TWI_PRESCALER))
+
+static uint8_t twi_address;
+static uint8_t* twi_data;
+static size_t twi_index;
+static size_t twi_len;
+
+void twi_init(void)
+{
+ TWBR = TWBR_VALUE;
+ TWSR = (TWPS1_VALUE << TWPS1) | (TWPS0_VALUE << TWPS0);
+
+ TWCR = (1 << TWINT) | (1 << TWEN);
+}
+
+bool twi_busy(void)
+{
+ return TWCR & (1<<TWIE);
+}
+
+void twi_flush(void)
+{
+ while(TWCR & (1<<TWIE));
+}
+
+void twi_start(
+ uint8_t address,
+ uint8_t *data,
+ size_t len
+)
+{
+ twi_flush();
+
+ twi_address = address;
+ twi_data = data;
+ twi_len = len;
+
+ TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN) | (1 << TWIE);
+}
+
+ISR(TWI_vect)
+{
+ switch(TW_STATUS)
+ {
+ case TW_START:
+ case TW_REP_START:
+ twi_index = 0;
+ TWDR = twi_address;
+ TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWIE);
+ break;
+
+ case TW_MT_SLA_ACK:
+ case TW_MT_DATA_ACK:
+ if(twi_index < twi_len)
+ {
+ TWDR = twi_data[twi_index++];
+ TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWIE);
+ }
+ else
+ {
+ TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
+ }
+ break;
+
+ case TW_MR_DATA_ACK:
+ twi_data[twi_index++] = TWDR;
+ case TW_MR_SLA_ACK:
+ if(twi_index < twi_len-1)
+ {
+ TWCR = (1 << TWINT) | (1 << TWEA) | (1 << TWEN) | (1 << TWIE);
+ }
+ else
+ {
+ TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWIE);
+ }
+ break;
+
+ case TW_MR_DATA_NACK:
+ twi_data[twi_index++] = TWDR;
+ TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
+ break;
+
+ case TW_MT_ARB_LOST:
+ TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN) | (1 << TWIE);
+ break;
+
+ case TW_MT_SLA_NACK:
+ case TW_MT_DATA_NACK:
+ case TW_MR_SLA_NACK:
+ default:
+ TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO);
+ }
+}
diff --git a/firmware/src/peripheral/uart.c b/firmware/src/peripheral/uart.c
new file mode 100644
index 0000000..defcf54
--- /dev/null
+++ b/firmware/src/peripheral/uart.c
@@ -0,0 +1,239 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "setup.h"
+
+#include "peripheral/ring.h"
+#include "peripheral/uart.h"
+
+#include "status.h"
+#include "cmd.h"
+
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <util/atomic.h>
+#include <util/setbaud.h>
+
+extern t_system_status system_status;
+
+static int uart_putc(
+ char c,
+ FILE *stream
+)
+{
+ (void)stream;
+
+ if(uart_tx(c))
+ return _FDEV_EOF;
+
+ return c;
+}
+
+static int uart_getc(
+ FILE *stream
+)
+{
+ uint8_t c;
+ (void)stream;
+
+ if(uart_rx(&c))
+ return _FDEV_EOF;
+
+ return c;
+}
+
+FILE uart_out = FDEV_SETUP_STREAM(uart_putc, NULL, _FDEV_SETUP_WRITE);
+FILE uart_in = FDEV_SETUP_STREAM(NULL, uart_getc, _FDEV_SETUP_READ);
+
+static volatile ring_t uart_tx_ring;
+static volatile ring_t uart_rx_ring;
+
+static volatile uint8_t uart_tx_buf[UART_BUF_SIZE];
+static volatile uint8_t uart_rx_buf[UART_BUF_SIZE];
+
+void uart_init(void)
+{
+ uart_tx_ring = ring_init((uint8_t*)uart_tx_buf, UART_BUF_SIZE - 1);
+ uart_rx_ring = ring_init((uint8_t*)uart_rx_buf, UART_BUF_SIZE - 1);
+
+ UBRR0H = UBRRH_VALUE;
+ UBRR0L = UBRRL_VALUE;
+ UCSR0B |= (1 << RXCIE0) | (1 << RXEN0) | (1 << TXEN0);
+ UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
+
+ stdout = &uart_out;
+ stdin = &uart_in;
+}
+
+bool uart_tx(
+ uint8_t data
+)
+{
+ bool ret;
+
+ while(ring_is_full(uart_tx_ring));
+
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
+ {
+ ret = ring_push((ring_t*)&uart_tx_ring, data);
+ }
+
+ if(!ret)
+ UCSR0B |= (1 << UDRIE0);
+
+ return ret;
+}
+
+bool uart_rx(
+ uint8_t* data
+)
+{
+ bool ret;
+
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
+ {
+ ret = ring_pop((ring_t*)&uart_rx_ring, data);
+ }
+
+ return ret;
+}
+
+size_t uart_tx_burst(
+ uint8_t *data,
+ size_t size
+)
+{
+ size_t i = 0;
+
+ while(i<size && !uart_tx(*data++))
+ i++;
+
+ return i;
+}
+
+size_t uart_rx_burst(
+ uint8_t* data,
+ size_t len
+)
+{
+ size_t i = 0;
+
+ while(i<len && !uart_rx(data++))
+ i++;
+
+ return i;
+}
+
+size_t uart_tx_available(void)
+{
+ size_t ret;
+
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
+ {
+ ret = ring_push_available(uart_tx_ring);
+ }
+
+ return ret;
+}
+
+size_t uart_rx_available(void)
+{
+ size_t ret;
+
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
+ {
+ ret = ring_pop_available(uart_rx_ring);
+ }
+
+ return ret;
+}
+
+bool uart_rx_peek(
+ uint8_t *data
+)
+{
+ bool ret;
+
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
+ {
+ ret = ring_peek((ring_t*)&uart_rx_ring, data);
+ }
+
+ return ret;
+}
+
+void uart_tx_flush(void)
+{
+ bool empty;
+ do
+ {
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
+ {
+ empty = ring_is_empty(uart_tx_ring);
+ }
+ } while(!empty);
+}
+
+char *uart_ngets(char *s, size_t n)
+{
+ uint8_t c;
+ static size_t i = 0;
+
+
+ while(!uart_rx(&c))
+ {
+ s[i++] = c;
+
+ if(c == '\n' || i >= n-1)
+ {
+ s[i] = '\0';
+ i = 0;
+
+ return s;
+ }
+ }
+
+ return NULL;
+}
+
+ISR(USART_UDRE_vect)
+{
+ uint8_t c;
+
+ if(!ring_pop((ring_t*)&uart_tx_ring, &c))
+ UDR0 = c;
+
+ else
+ UCSR0B &= ~(1 << UDRIE0);
+}
+
+ISR(USART_RX_vect)
+{
+ if (system_status.status == SYSTEM_STATUS_BUSY)
+ {
+ // system busy.. lock now!
+ CMD_ERROR("SYSTEM BUSY");
+ system_status.status = SYSTEM_STATUS_LOCKED;
+ }
+
+ if (system_status.status != SYSTEM_STATUS_LOCKED)
+ {
+ #ifdef UART_OVERWRITE
+ ring_push_over((ring_t*)&uart_rx_ring, UDR0);
+ #else
+ ring_push((ring_t*)&uart_rx_ring, UDR0);
+ #endif
+ }
+ else{
+ (uint8_t)UDR0;
+ }
+}
diff --git a/firmware/src/status.c b/firmware/src/status.c
new file mode 100644
index 0000000..d3bb9eb
--- /dev/null
+++ b/firmware/src/status.c
@@ -0,0 +1,46 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "status.h"
+#include <avr/pgmspace.h>
+
+t_gate_status gate_status;
+t_drain_status drain_status;
+t_system_status system_status;
+
+const char* const gate_lut[] PROGMEM =
+{
+ "INACTIVE",
+ "PROBE_ERR",
+ "PROBE_OK",
+ "PROBE_TIME"
+};
+
+const char* const drain_lut[] PROGMEM =
+{
+ "INACTIVE",
+ "POWER_SET",
+ "OVERRIDE",
+ "ADC_ERROR",
+ "AMP_ALARM"
+};
+
+const char* const system_lut[] PROGMEM =
+{
+ "IDLE",
+ "BUSY",
+ "LOCKED",
+ "SET_FAIL",
+ "SET_REACHED",
+ "SET_TIME",
+ "VSWR_ALARM"
+};
diff --git a/firmware/src/util.c b/firmware/src/util.c
new file mode 100644
index 0000000..af6ffed
--- /dev/null
+++ b/firmware/src/util.c
@@ -0,0 +1,152 @@
+/**
+ *
+ * Author: Dylan Muller
+ * Copyright (c) 2025
+ * All rights reserved.
+ *
+ * - Commercial/IP use prohibited.
+ * - Attribution required.
+ * See License.txt
+ *
+ */
+
+#include "util.h"
+
+#include <stdio.h>
+#include <avr/pgmspace.h>
+
+extern int __heap_start, *__brkval;
+
+const uint16_t fw_pwr_gradient[] = { 249, 356, 374, 388, 400, 402, 435, 460, 500, 553 };
+const int16_t fw_pwr_intercept[] = { -5418, -3810, -3602, -3431, -3289, -3261, -2877, -2601, -2189, -1725 };
+const uint16_t fw_pwr_threshold[] = { 1288, 1548, 1704, 1812, 1897, 1967, 2164, 2301, 2411, 2505 };
+
+uint8_t util_volt_to_count(
+ uint16_t volt,
+ uint16_t* count
+)
+{
+ uint8_t bits = 0x0;
+ uint32_t x = 0x0;
+ uint32_t y = 0x0;
+ uint16_t i = 0x0;
+
+ bits = UTIL_CONV_BITS;
+
+ if (volt > UTIL_SYSTEM_VOLT)
+ {
+ return 1;
+ }
+
+ for (i = 0; i < bits; i++)
+ {
+ x |= UTIL_CONV_BIT(i);
+ }
+
+ y = ((uint32_t)volt) * x;
+ y /= UTIL_SYSTEM_VOLT;
+
+ *count = (uint16_t)y;
+ return 0;
+}
+
+uint8_t util_count_to_volt(
+ uint16_t count,
+ uint16_t* volt
+)
+{
+ uint8_t bits = 0x0;
+ uint32_t x = 0x0;
+ uint32_t y = 0x0;
+ uint16_t i = 0x0;
+
+ bits = UTIL_CONV_BITS;
+
+ for (i = 0; i < bits; i++)
+ {
+ x |= UTIL_CONV_BIT(i);
+ }
+
+ if (count > x)
+ {
+ return 1;
+ }
+
+ y = ((uint32_t)count) * UTIL_SYSTEM_VOLT;
+ y /= x;
+
+ *volt = (uint16_t)y;
+ return 0;
+}
+
+int8_t util_count_to_pwr(
+ uint16_t count,
+ int16_t* cpwr
+)
+{
+ uint32_t x = 0x0;
+ int16_t y = 0x0;
+ uint16_t slope = 0x0;
+ int16_t intercept = 0x0;
+ uint16_t i = 0;
+
+ uint8_t pwr_cal_size = sizeof(fw_pwr_gradient) / sizeof(uint16_t);
+ uint8_t pwr_last_element = pwr_cal_size - 1;
+
+ if(count >= fw_pwr_threshold[pwr_last_element])
+ {
+ slope = fw_pwr_gradient[pwr_last_element];
+ intercept = fw_pwr_intercept[pwr_last_element];
+ }
+ else if (count <= fw_pwr_threshold[0])
+ {
+ slope = fw_pwr_gradient[0];
+ intercept = fw_pwr_intercept[0];
+ }
+ else
+ {
+ for(i = 0; i < pwr_cal_size; i++)
+ {
+ if(count < fw_pwr_threshold[i])
+ {
+ slope = fw_pwr_gradient[i];
+ intercept = fw_pwr_intercept[i];
+ break;
+ }
+ }
+ }
+
+ x = (((uint32_t)count * 1000) / slope);
+ y = (int16_t)x;
+
+ y = y + intercept;
+ *cpwr = y;
+
+ return 0;
+}
+
+void util_print_pwr(
+ int16_t cpwr
+)
+{
+ int16_t integer_part = cpwr / 100;
+
+ int16_t fractional_part = cpwr % 100;
+
+ if (fractional_part < 0) {
+ fractional_part = -fractional_part;
+ }
+
+ printf("%d.%02d dBm\n", integer_part, fractional_part);
+}
+
+uint16_t util_get_free_mem(void)
+{
+ uint16_t free_mem;
+ if ((int)__brkval == 0) {
+ free_mem = (int)&free_mem - (int)&__heap_start;
+ } else {
+ free_mem = (int)&free_mem - (int)__brkval;
+ }
+ return free_mem;
+}