Introduce AutoBaud mode to detect and set the UART baud rate from incoming data.
diff --git a/src/autobaud.c b/src/autobaud.c
new file mode 100644
index 0000000..e5d61c1
--- /dev/null
+++ b/src/autobaud.c
@@ -0,0 +1,387 @@
+#include <pico/stdlib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+
+#include <hardware/pio.h>
+#include <hardware/dma.h>
+#include <hardware/irq.h>
+#include <hardware/clocks.h>
+
+#include "autobaud.h"
+#include "probe_config.h"
+#include "autobaud.pio.h"
+
+// DMA buffer size
+#define BUF_SIZE 1024
+
+// Size of hash table for sample occurrence counts
+#define HASH_TBL_SIZE 500
+
+// Minimum sample occurrence ratio to consider a baud rate value valid
+#define MIN_FREQUENCY 0.05f
+
+// PIO clock frequency in Hz
+#define PIO_CLOCK_FREQUENCY 125000000
+
+// DMA IRQ for autobaud
+#define DMA_AUTOBAUD_IRQ 0
+
+// Priority for DMA IRQ handler
+#define DMA_AUTOBAUD_IRQ_PRIORITY PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY
+
+typedef struct {
+    int key;
+    int count;
+} Entry;
+
+typedef struct {
+    Entry *entries;
+    size_t size;
+} HashTable;
+
+// PIO instance
+static PIO pio;
+// PIO state machine
+static int sm = -1;
+// PIO program offset
+static int offset = -1;
+// UART RX GPIO 1 pin
+static const uint rx_pin = PROBE_UART_RX;
+
+// Frequency hash table to store samples occurance
+static HashTable *freq_table;
+
+// Estimated baud rate and validity
+static float baud;
+static float validity;
+
+// shortest bit duration in PIO cycles
+static uint32_t min_cycles_count = UINT32_MAX;
+// longest bit duration in PIO cycles
+static uint32_t max_cycles_count = 0;
+
+static uint32_t total_samples;  // total samples seen
+static uint32_t bit_time_sum;   // sum of 1-bit times
+static uint32_t bit_time_count; // total 1-bit times
+static uint32_t outlier_count;  // total of 1-bit times outliers
+
+// DMA channels to read RX PIO line
+static int ctrl_chan = -1;
+static int data_chan = -1;
+
+// DMA ring buffer storing PIO RX FIFO data
+static uint32_t rx_buffer[BUF_SIZE] __attribute__((aligned(4096)));
+
+// DMA control channel reads this value to reload transfer count
+static const uint32_t dma_reload_count = BUF_SIZE;
+
+static uintptr_t last_write_addr = (uintptr_t)rx_buffer;
+static uintptr_t curr_write_addr = (uintptr_t)rx_buffer;
+
+volatile bool autobaud_running = false;
+volatile bool autobaud_stopped = true;
+
+// Queue to hold new baud rate estimates
+QueueHandle_t baudQueue;
+
+TaskHandle_t autobaud_taskhandle;
+
+
+uint32_t hash(uint32_t x, size_t size) {
+    x = ((x >> 16) ^ x) * 0x45d9f3bu;
+    x = ((x >> 16) ^ x) * 0x45d9f3bu;
+    x = (x >> 16) ^ x;
+    return x % size;
+}
+
+HashTable *create_table(size_t size) {
+    HashTable *table = malloc(sizeof(HashTable));
+    if (!table) return NULL;
+    table->size = size;
+    table->entries = calloc(size, sizeof(Entry));
+    if (!table->entries) {
+        free(table);
+        return NULL;
+    }
+    return table;
+}
+
+void insert(HashTable *table, int key) {
+    uint32_t idx = hash(key, table->size);
+    while (table->entries[idx].key != 0) {
+        if (table->entries[idx].key == key) {
+            table->entries[idx].count++;
+            return;
+        }
+        idx = (idx + 1) % table->size;
+    }
+    table->entries[idx].key = key;
+    table->entries[idx].count = 1;
+}
+
+int get_count(HashTable *table, int key) {
+    uint32_t idx = hash(key, table->size);
+    while (table->entries[idx].key != 0) {
+        if (table->entries[idx].key == key) {
+            return table->entries[idx].count;
+        }
+        idx = (idx + 1) % table->size;
+    }
+    return 0;
+}
+
+void free_table(HashTable *table) {
+    free(table->entries);
+    free(table);
+}
+
+void __isr dma_handler() {
+    // Clear DMA interrupt
+    if ((data_chan >= 0) && dma_irqn_get_channel_status(DMA_AUTOBAUD_IRQ, data_chan)) {
+        dma_irqn_acknowledge_channel(DMA_AUTOBAUD_IRQ, data_chan);
+    }
+}
+
+bool dma_configure(PIO pio, uint sm) {
+    // Configure two DMA channels for continuous RX FIFO monitoring:
+    // - data_chan: Continuously reads from PIO RX FIFO into circular buffer
+    // - ctrl_chan: Triggers data_chan to restart when it completes a transfer
+    ctrl_chan = dma_claim_unused_channel(true);
+    if (ctrl_chan < 0) return false;
+    data_chan = dma_claim_unused_channel(true);
+    if (data_chan < 0) return false;
+
+    dma_channel_config data_cfg = dma_channel_get_default_config(data_chan);
+    channel_config_set_transfer_data_size(&data_cfg, DMA_SIZE_32);
+    channel_config_set_read_increment(&data_cfg, false);
+    channel_config_set_write_increment(&data_cfg, true);
+    channel_config_set_dreq(&data_cfg, pio_get_dreq(pio, sm, false)); // Trigger when PIO RX FIFO has data to read
+    channel_config_set_chain_to(&data_cfg, ctrl_chan); // Chain to control channel when transfer completes
+    channel_config_set_ring(&data_cfg, true, 12); // Ring buffer size 2^12 = 4096 bytes (1024 uint32_t)
+
+    dma_channel_configure(
+        data_chan,
+        &data_cfg,
+        rx_buffer,
+        &pio->rxf[sm],
+        BUF_SIZE,
+        false
+    );
+
+    dma_channel_config ctrl_cfg = dma_channel_get_default_config(ctrl_chan);
+    channel_config_set_transfer_data_size(&ctrl_cfg, DMA_SIZE_32);
+    channel_config_set_read_increment(&ctrl_cfg, false);
+    channel_config_set_write_increment(&ctrl_cfg, false);
+
+    dma_channel_configure(
+        ctrl_chan,
+        &ctrl_cfg,
+        &(dma_hw->ch[data_chan].al1_transfer_count_trig), // Destination: data channel's transfer count register
+        &dma_reload_count,                               // Source: reload transfer count value (BUF_SIZE)
+        1,
+        false
+    );
+
+    // Enable DMA interrupt on data channel completion
+    irq_add_shared_handler(dma_get_irq_num(DMA_AUTOBAUD_IRQ), dma_handler, DMA_AUTOBAUD_IRQ_PRIORITY);
+    irq_set_enabled(dma_get_irq_num(DMA_AUTOBAUD_IRQ), true);
+    dma_irqn_set_channel_enabled(DMA_AUTOBAUD_IRQ, data_chan, true);
+
+    dma_channel_start(data_chan);
+    return true;
+}
+
+// Compare rounded integer parts of baud rates
+// Tolerance of 0.5% in detected versus set
+inline bool baud_changed(float new_baud, float baud) {
+    uint32_t hi = (uint32_t)(baud * 1.005f);
+    uint32_t lo = (uint32_t)(baud * 0.995f);
+    uint32_t new = (uint32_t)new_baud;
+
+    return (new > hi || new < lo);
+}
+
+// Processes new DMA samples read from the PIO RX FIFO. Each DMA sample
+// represents a timestamp (or cycle count) between edges on the input signal.
+// Accumulates these durations, filters noise, and estimates baud rate.
+uint estimate_baud_rate() {
+    uint old_progress = total_samples;
+    // Get current DMA write address
+    curr_write_addr = dma_hw->ch[data_chan].write_addr;
+    // Convert absolute addresses to buffer indices
+    size_t curr_index = ((curr_write_addr) - (uintptr_t)rx_buffer) / sizeof(rx_buffer[0]);
+    size_t last_index = (last_write_addr - (uintptr_t)rx_buffer) / sizeof(rx_buffer[0]);
+
+    for (size_t i = last_index; i != curr_index; i = (i + 1) % BUF_SIZE) {
+        uint32_t raw = rx_buffer[i];
+        uint32_t curr_cycles_count = (UINT32_MAX - raw) * 2;
+        insert(freq_table, curr_cycles_count);
+
+        total_samples++;
+        if (curr_cycles_count > max_cycles_count)
+            max_cycles_count = curr_cycles_count;
+        float freq = (float) get_count(freq_table, curr_cycles_count) / (float) total_samples;
+        // if sample is seen at least 5% of all samples,
+        // it is assumed it's not a noisy value
+        if (freq < MIN_FREQUENCY) continue;
+        if (curr_cycles_count < min_cycles_count) {
+            min_cycles_count = curr_cycles_count;
+            bit_time_sum = 0;
+            bit_time_count = 0;
+            outlier_count = 0;
+            continue;
+        }
+        // If current duration is within +10% of min_cycles, treat it as a "1-bit period"
+        if ((curr_cycles_count - min_cycles_count) < ((float)min_cycles_count * 0.1f)) {
+            bit_time_sum += curr_cycles_count;
+            bit_time_count++;
+            // 1-bit period should not be less than 1/9th of the longest period
+            if (curr_cycles_count < (max_cycles_count / 9))
+                outlier_count++;
+            // Calculate baud from average of 1-bit times
+            float avg_bit_time = (float) bit_time_sum / (float) bit_time_count;
+            float new_baud = PIO_CLOCK_FREQUENCY / avg_bit_time;
+            // If baud has changed, send updated baud information to cdc_thread
+            if (baud_changed(new_baud, baud)) {
+                float completeness = 1.0f - expf(-(float) total_samples / 40.0f);
+                float noise_ratio = (float) outlier_count / (float) bit_time_count;
+                float consistency = 1.0f - fminf(noise_ratio * 2.0f, 1.0f);
+                float validity = completeness * consistency;
+                if (validity > 0.6f) {
+                    baud = new_baud;
+                    BaudInfo_t new_baud_info;
+                    new_baud_info.baud = (uint32_t)roundf(baud);
+                    new_baud_info.validity = validity;
+                    xQueueOverwrite(baudQueue, &new_baud_info);
+                }
+            }
+        }
+    }
+    last_write_addr = curr_write_addr;
+    return total_samples - old_progress;
+}
+
+void autobaud_deinit() {
+    // Disable DMA IRQ and channels
+    if (data_chan >= 0) {
+        dma_irqn_set_channel_enabled(DMA_AUTOBAUD_IRQ, data_chan, false);
+        dma_irqn_acknowledge_channel(DMA_AUTOBAUD_IRQ, data_chan);
+        dma_channel_unclaim(data_chan);
+        data_chan = -1;
+    }
+    if (ctrl_chan >= 0) {
+        dma_channel_unclaim(ctrl_chan);
+        ctrl_chan = -1;
+    }
+    irq_remove_handler(dma_get_irq_num(DMA_AUTOBAUD_IRQ), dma_handler);
+    if (!irq_has_shared_handler(dma_get_irq_num(DMA_AUTOBAUD_IRQ))) {
+        irq_set_enabled(dma_get_irq_num(DMA_AUTOBAUD_IRQ), false);
+    }
+
+    // Remove PIO program
+    if (pio && (sm >= 0)) {
+        pio_sm_set_enabled(pio, sm, false);
+        pio_sm_unclaim(pio, sm);
+    }
+    if (pio && (offset >= 0)) {
+        pio_remove_program(pio, &autobaud_program, offset);
+    }
+
+    if (freq_table) {
+        free_table(freq_table);
+        freq_table = NULL;
+    }
+    if (baudQueue) {
+        vQueueDelete(baudQueue);
+        baudQueue = NULL;
+    }
+
+    // Reset state
+    pio = NULL;
+    sm = offset = -1;
+    baud = validity = 0.0f;
+    min_cycles_count = UINT32_MAX;
+    max_cycles_count = 0;
+    total_samples = bit_time_sum = bit_time_count = outlier_count = 0;
+    last_write_addr = (uintptr_t)rx_buffer;
+    curr_write_addr = (uintptr_t)rx_buffer;
+}
+
+bool autobaud_init() {
+    pio = pio0;
+    // Claim a free PIO state machine
+    sm = pio_claim_unused_sm(pio, true);
+    if (sm < 0)
+        return false;
+    // Add PIO program
+    offset = pio_add_program(pio, &autobaud_program);
+    if (offset < 0) {
+        autobaud_deinit();
+        return false;
+    }
+    float div = (float)clock_get_hz(clk_sys) / PIO_CLOCK_FREQUENCY;
+    autobaud_program_init(pio, sm, offset, rx_pin, div);
+    pio_sm_set_enabled(pio, sm, true);
+
+    // Create hash table to keep count of sample occurance
+    freq_table = create_table(HASH_TBL_SIZE);
+    if (!freq_table) {
+        autobaud_deinit();
+        return false;
+    }
+    // Create queue to send baud information to cdc_thread
+    baudQueue = xQueueCreate(1, sizeof(BaudInfo_t));
+    if (!baudQueue) {
+        autobaud_deinit();
+        return false;
+    }
+    // Set up DMA to continuously write PIO RX data into RAM
+    if (!dma_configure(pio, sm)) {
+        autobaud_deinit();
+        return false;
+    }
+    return true;
+}
+
+void autobaud_start() {
+    xTaskNotify(autobaud_taskhandle, AUTOBAUD_CMD_START, eSetValueWithOverwrite);
+}
+
+void autobaud_wait_stop() {
+    while (!autobaud_stopped)
+        xTaskNotify(autobaud_taskhandle, AUTOBAUD_CMD_STOP, eSetValueWithOverwrite);
+}
+
+// FreeRTOS thread running if MAGIC_BAUD was set by host
+void autobaud_thread(void * param) {
+    TickType_t wake = xTaskGetTickCount();
+    uint32_t cmd = AUTOBAUD_CMD_NONE;
+    uint processed;
+
+    while (true) {
+        if (!autobaud_running) {
+            // Idle state: thread is blocked here until start command is received
+            xTaskNotifyWait(0, 0xFFFFFFFFu, &cmd, portMAX_DELAY);
+            if (cmd == AUTOBAUD_CMD_START) {
+                if (autobaud_init()) {
+                    autobaud_running = true;
+                    autobaud_stopped = false;
+                }
+            }
+        } else {
+            // Check if host requested autobaud termination
+            if (xTaskNotifyWait(0, 0xFFFFFFFFu, &cmd, 0) == pdTRUE) {
+                if (cmd == AUTOBAUD_CMD_STOP) {
+                    autobaud_running = false;
+                    autobaud_deinit();
+                    autobaud_stopped = true;
+                    continue;
+                }
+            }
+            processed = estimate_baud_rate();
+            if (!processed)
+                xTaskDelayUntil(&wake, 1);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/autobaud.h b/src/autobaud.h
new file mode 100644
index 0000000..c4b49aa
--- /dev/null
+++ b/src/autobaud.h
@@ -0,0 +1,30 @@
+#ifndef AUTOBAUD_H
+#define AUTOBAUD_H
+
+#include "FreeRTOS.h"
+#include "queue.h"
+#include "semphr.h"
+
+#define MAGIC_BAUD 9728 // 0x2600
+
+typedef enum {
+    AUTOBAUD_CMD_NONE = 0,
+    AUTOBAUD_CMD_START = 1,
+    AUTOBAUD_CMD_STOP = 2,
+} autobaud_cmd_t;
+
+typedef struct {
+    uint32_t baud;  // Estimated baud rate
+    float validity; // Validity of the estimated baud rate
+} BaudInfo_t;
+
+extern volatile bool autobaud_running;
+extern volatile bool autobaud_stopped;
+extern QueueHandle_t baudQueue;
+extern TaskHandle_t autobaud_taskhandle;
+
+void autobaud_start(void);
+void autobaud_wait_stop(void);
+void autobaud_thread(void * param);
+
+#endif
\ No newline at end of file
diff --git a/src/autobaud.pio b/src/autobaud.pio
new file mode 100644
index 0000000..e8fe1e8
--- /dev/null
+++ b/src/autobaud.pio
@@ -0,0 +1,42 @@
+.program autobaud
+
+.wrap_target
+
+falling_edge:
+    wait 0 pin 0                    ; falling edge detected
+    set x, 0                        ; set cycle counter to 0
+    mov x, ~x                       ; x = 0xFFFFFFFF, to decrement x
+
+count_cycles:
+    jmp pin rising_edge             ; if line went high, save current cycle count
+    jmp x-- count_cycles            ; otherwise, decrement and enter loop again
+
+rising_edge:
+    mov isr, x                      ; save elapsed cycle count in ISR
+    push noblock                    ; nonblocking push of the ISR content
+    jmp falling_edge                ; repeat sampling process
+
+.wrap
+
+% c-sdk {
+
+// Helper function (for use in C program) to initialize this PIO program
+void autobaud_program_init(PIO pio, uint sm, uint offset, uint rx_pin, float div) {
+
+    // Sets up state machine and wrap target. This function is automatically
+    // generated in autobaud.pio.h.
+    pio_sm_config c = autobaud_program_get_default_config(offset);
+
+    // No need to set PIO funcsel, as this program is receive-only
+    sm_config_set_in_pins(&c, rx_pin);
+    sm_config_set_jmp_pin(&c, rx_pin);
+    pio_sm_set_consecutive_pindirs(pio, sm, rx_pin, 1, false);
+
+    // Set the clock divider for the state machine
+    sm_config_set_clkdiv(&c, div);
+
+    // Load configuration and jump to start of the program
+    pio_sm_init(pio, sm, offset, &c);
+}
+
+%}
diff --git a/src/cdc_uart.c b/src/cdc_uart.c
index 53d07cb..d60e7b4 100644
--- a/src/cdc_uart.c
+++ b/src/cdc_uart.c
@@ -186,8 +186,16 @@
     keep_alive = cdc_task();
     if (!keep_alive) {
       delayed = xTaskDelayUntil(&last_wake, interval);
-        if (delayed == pdFALSE)
-          last_wake = xTaskGetTickCount();
+      if (delayed == pdFALSE)
+        last_wake = xTaskGetTickCount();
+      if (autobaud_running) {
+        // Receive baud information from autobaud thread
+        if (xQueueReceive(baudQueue, &baud_info, 0) == pdTRUE) {
+          cdc_uart_set_baudrate(baud_info.baud);
+          // Assume 8N1
+          uart_set_format(PROBE_UART_INTERFACE, 8, 1, UART_PARITY_NONE);
+        }
+      }
     }
   }
 }