diff --git a/Core/Inc/drivers/config_ll.h b/Core/Inc/drivers/config_ll.h
new file mode 100644
index 0000000..5053e70
--- /dev/null
+++ b/Core/Inc/drivers/config_ll.h
@@ -0,0 +1,168 @@
+/*
+Copyright 2020-2025 Piotr Wilkon
+This file is part of VP-Digi.
+
+VP-Digi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or
+(at your option) any later version.
+
+VP-Digi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with VP-Digi. If not, see .
+*/
+
+#ifndef DRIVERS_CONFIG_LL_H_
+#define DRIVERS_CONFIG_LL_H_
+
+#include
+
+#if defined(STM32F103xB) || defined(STM32F103x8)
+
+#include "stm32f1xx.h"
+
+#define CONFIG_ADDRESS (uintptr_t)0x800F000 //64 KiB starting at 0x8000000 minus 4 kiB (two 1024-word pages)
+#define CONFIG_PAGE_COUNT 2 //two 1024-word pages
+#define CONFIG_PAGE_SIZE 1024 //1024 words per page (2048 bytes)
+
+/**
+ * @brief Write word to configuration part in flash
+ * @param[in] address Relative address
+ * @param[in] data Data to write
+ * @warning Flash must be unlocked first
+ */
+static void write(uint32_t address, uint16_t data)
+{
+ FLASH->CR |= FLASH_CR_PG; //programming mode
+
+ *((volatile uint16_t*)(address + CONFIG_ADDRESS)) = data; //store data
+
+ while (FLASH->SR & FLASH_SR_BSY);; //wait for completion
+ if(!(FLASH->SR & FLASH_SR_EOP)) //an error occurred
+ FLASH->CR &= ~FLASH_CR_PG;
+ else
+ FLASH->SR |= FLASH_SR_EOP;
+}
+
+/**
+ * @brief Read single word from configuration part in flash
+ * @param[in] address Relative address
+ * @return Data (word)
+ */
+static uint16_t read(uint32_t address)
+{
+ return *(volatile uint16_t*)((address + CONFIG_ADDRESS));
+}
+
+static void erase(void)
+{
+ while (FLASH->SR & FLASH_SR_BSY)
+ ;
+ FLASH->CR |= FLASH_CR_PER; //erase mode
+ for(uint8_t i = 0; i < CONFIG_PAGE_COUNT; i++)
+ {
+ FLASH->AR = CONFIG_ADDRESS + (CONFIG_PAGE_SIZE * i);
+ FLASH->CR |= FLASH_CR_STRT; //start erase
+ while (FLASH->SR & FLASH_SR_BSY)
+ ;
+ if(!(FLASH->SR & FLASH_SR_EOP))
+ {
+ FLASH->CR &= ~FLASH_CR_PER;
+ return;
+ }
+ else
+ FLASH->SR |= FLASH_SR_EOP;
+ }
+ FLASH->CR &= ~FLASH_CR_PER;
+}
+
+static void unlock(void)
+{
+ FLASH->KEYR = 0x45670123;
+ FLASH->KEYR = 0xCDEF89AB;
+}
+
+static void lock(void)
+{
+ FLASH->CR &= ~FLASH_CR_PG;
+ FLASH->CR |= FLASH_CR_LOCK;
+}
+
+#elif defined(STM32F302xC)
+
+#include "stm32f3xx.h"
+
+#define CONFIG_ADDRESS (uintptr_t)0x801F000 //128 KiB starting at 0x8000000 minus 4 kiB (two 1024-word pages)
+#define CONFIG_PAGE_COUNT 2 //two 1024-word pages
+#define CONFIG_PAGE_SIZE 1024 //1024 words per page (2048 bytes)
+
+/**
+ * @brief Write word to configuration part in flash
+ * @param[in] address Relative address
+ * @param[in] data Data to write
+ * @warning Flash must be unlocked first
+ */
+static void write(uint32_t address, uint16_t data)
+{
+ FLASH->CR |= FLASH_CR_PG; //programming mode
+
+ *((volatile uint16_t*)(address + CONFIG_ADDRESS)) = data; //store data
+
+ while (FLASH->SR & FLASH_SR_BSY);; //wait for completion
+ if(!(FLASH->SR & FLASH_SR_EOP)) //an error occurred
+ FLASH->CR &= ~FLASH_CR_PG;
+ else
+ FLASH->SR |= FLASH_SR_EOP;
+}
+
+/**
+ * @brief Read single word from configuration part in flash
+ * @param[in] address Relative address
+ * @return Data (word)
+ */
+static uint16_t read(uint32_t address)
+{
+ return *(volatile uint16_t*)((address + CONFIG_ADDRESS));
+}
+
+static void erase(void)
+{
+ while (FLASH->SR & FLASH_SR_BSY)
+ ;
+ FLASH->CR |= FLASH_CR_PER; //erase mode
+ for(uint8_t i = 0; i < CONFIG_PAGE_COUNT; i++)
+ {
+ FLASH->AR = CONFIG_ADDRESS + (CONFIG_PAGE_SIZE * i);
+ FLASH->CR |= FLASH_CR_STRT; //start erase
+ while (FLASH->SR & FLASH_SR_BSY)
+ ;
+ if(!(FLASH->SR & FLASH_SR_EOP))
+ {
+ FLASH->CR &= ~FLASH_CR_PER;
+ return;
+ }
+ else
+ FLASH->SR |= FLASH_SR_EOP;
+ }
+ FLASH->CR &= ~FLASH_CR_PER;
+}
+
+static void unlock(void)
+{
+ FLASH->KEYR = 0x45670123;
+ FLASH->KEYR = 0xCDEF89AB;
+}
+
+static void lock(void)
+{
+ FLASH->CR &= ~FLASH_CR_PG;
+ FLASH->CR |= FLASH_CR_LOCK;
+}
+
+#endif
+
+#endif
diff --git a/Core/Inc/drivers/modem_ll_aioc.h b/Core/Inc/drivers/modem_ll_aioc.h
new file mode 100644
index 0000000..41fdb37
--- /dev/null
+++ b/Core/Inc/drivers/modem_ll_aioc.h
@@ -0,0 +1,400 @@
+/*
+Copyright 2020-2025 Piotr Wilkon
+This file is part of VP-Digi.
+
+VP-Digi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or
+(at your option) any later version.
+
+VP-Digi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with VP-Digi. If not, see .
+*/
+
+#ifndef DRIVERS_MODEM_LL_STM32F302_H_
+#define DRIVERS_MODEM_LL_STM32F302_H_
+
+#if defined(STM32F302xC)
+
+#define USE_FPU 1 /**< Use FPU - F302 has one */
+#define MODEM_LL_DAC_MAX 4095 /**< Maximum value for DAC - 4095 for 12-bit DAC */
+
+#include
+#include "stm32f3xx.h"
+
+/**
+ * TIM1 is used for pushing samples to DAC (clocked at 18 MHz)
+ * TIM3 is the baudrate generator for TX (clocked at 18 MHz)
+ * TIM2 is the RX sampling timer with no software interrupt, but it directly calls DMA
+ */
+
+#define MODEM_LL_DMA_INTERRUPT_HANDLER DMA1_Channel2_IRQHandler
+#define MODEM_LL_DAC_INTERRUPT_HANDLER TIM1_UP_TIM16_IRQHandler
+#define MODEM_LL_BAUDRATE_TIMER_INTERRUPT_HANDLER TIM3_IRQHandler
+
+#define MODEM_LL_DMA_IRQ DMA1_Channel2_IRQn
+#define MODEM_LL_DAC_IRQ TIM1_UP_TIM16_IRQn
+#define MODEM_LL_BAUDRATE_TIMER_IRQ TIM3_IRQn
+
+#define MODEM_LL_DMA_TRANSFER_COMPLETE_FLAG (DMA1->ISR & DMA_ISR_TCIF2)
+#define MODEM_LL_DMA_CLEAR_TRANSFER_COMPLETE_FLAG() (DMA1->IFCR |= DMA_IFCR_CTCIF2)
+
+#define MODEM_LL_BAUDRATE_TIMER_CLEAR_INTERRUPT_FLAG() (TIM3->SR &= ~TIM_SR_UIF)
+#define MODEM_LL_BAUDRATE_TIMER_ENABLE() (TIM3->CR1 = TIM_CR1_CEN)
+#define MODEM_LL_BAUDRATE_TIMER_DISABLE() (TIM3->CR1 &= ~TIM_CR1_CEN)
+#define MODEM_LL_BAUDRATE_TIMER_SET_RELOAD_VALUE(val) (TIM3->ARR = (val))
+
+#define MODEM_LL_DAC_TIMER_CLEAR_INTERRUPT_FLAG (TIM1->SR &= ~TIM_SR_UIF)
+#define MODEM_LL_DAC_TIMER_SET_RELOAD_VALUE(val) (TIM1->ARR = (val))
+#define MODEM_LL_DAC_TIMER_SET_CURRENT_VALUE(val) (TIM1->CNT = (val))
+#define MODEM_LL_DAC_TIMER_ENABLE() (TIM1->CR1 |= TIM_CR1_CEN)
+#define MODEM_LL_DAC_TIMER_DISABLE() (TIM1->CR1 &= ~TIM_CR1_CEN)
+
+#define MODEM_LL_ADC_TIMER_ENABLE() (TIM2->CR1 |= TIM_CR1_CEN)
+#define MODEM_LL_ADC_TIMER_DISABLE() (TIM2->CR1 &= ~TIM_CR1_CEN)
+
+#define MODEM_LL_DAC_PUT_VALUE(value) (DAC1->DHR12R1 = (value))
+
+static inline void MODEM_LL_DCD_LED_ON(void)
+{
+ GPIOB->BSRR = GPIO_BSRR_BR_8;
+ GPIOB->BSRR = GPIO_BSRR_BS_9;
+}
+
+static inline void MODEM_LL_DCD_LED_OFF(void)
+{
+ GPIOB->BSRR = GPIO_BSRR_BR_8;
+ GPIOB->BSRR = GPIO_BSRR_BR_9;
+}
+
+#define MODEM_LL_SET_TX_ATTENUATOR(state) (GPIOA->BSRR = (state ? GPIO_BSRR_BR_3 : GPIO_BSRR_BS_3))
+
+/**
+ * @brief Enable PTT
+ * @param output Output number (AIOC only, meaningless on other platforms)
+ */
+static inline void MODEM_LL_PTT_ON(uint8_t output)
+{
+ GPIOB->BSRR = GPIO_BSRR_BR_9;
+ GPIOB->BSRR = GPIO_BSRR_BS_8;
+ switch (output)
+ {
+ case 0:
+ GPIOA->BSRR = GPIO_BSRR_BS_0;
+ break;
+ case 1:
+ GPIOA->BSRR = GPIO_BSRR_BS_1;
+ break;
+ }
+}
+
+/**
+ * @brief Enable PTT
+ * @param output Output number (AIOC only, meaningless on other platforms)
+ */
+static inline void MODEM_LL_PTT_OFF(uint8_t output)
+{
+ GPIOB->BSRR = GPIO_BSRR_BR_8;
+ GPIOB->BSRR = GPIO_BSRR_BR_9;
+ switch (output)
+ {
+ case 0:
+ GPIOA->BSRR = GPIO_BSRR_BR_0;
+ break;
+ case 1:
+ GPIOA->BSRR = GPIO_BSRR_BR_1;
+ break;
+ }
+}
+
+/**
+ * @brief Initialize clocks
+ */
+static void MODEM_LL_INITIALIZE_RCC(void)
+{
+ RCC->AHBENR |= RCC_AHBENR_GPIOBEN;
+ RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
+ RCC->AHBENR |= RCC_AHBENR_ADC12EN;
+ RCC->APB1ENR |= RCC_APB1ENR_DAC1EN;
+ RCC->AHBENR |= RCC_AHBENR_DMA1EN;
+ RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
+ RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
+ RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
+}
+
+/**
+ * @brief Initialize PTT outputs and LEDs
+ */
+static void MODEM_LL_INITIALIZE_OUTPUTS(void)
+{
+ /* DCD and PTT LEDs: between PB8 and PB9 */
+ GPIOB->MODER &= ~GPIO_MODER_MODER8;
+ GPIOB->MODER |= GPIO_MODER_MODER8_0;
+ GPIOB->OTYPER &= ~GPIO_OTYPER_OT_8;
+ GPIOB->BSRR = GPIO_BSRR_BR_8;
+ GPIOB->MODER &= ~GPIO_MODER_MODER9;
+ GPIOB->MODER |= GPIO_MODER_MODER9_0;
+ GPIOB->OTYPER &= ~GPIO_OTYPER_OT_9;
+ GPIOB->BSRR = GPIO_BSRR_BR_9;
+ /* PTT: PA0, PA1 */
+ GPIOA->MODER &= ~GPIO_MODER_MODER0;
+ GPIOA->MODER |= GPIO_MODER_MODER0_0;
+ GPIOA->OTYPER &= ~GPIO_OTYPER_OT_0;
+ GPIOA->BSRR = GPIO_BSRR_BR_0;
+ GPIOA->MODER &= ~GPIO_MODER_MODER1;
+ GPIOA->MODER |= GPIO_MODER_MODER1_0;
+ GPIOA->OTYPER &= ~GPIO_OTYPER_OT_1;
+ GPIOA->BSRR = GPIO_BSRR_BR_1;
+ /* PA3: open-drain TX attenuator */
+ GPIOA->MODER &= ~GPIO_MODER_MODER3;
+ GPIOA->MODER |= GPIO_MODER_MODER3_0;
+ GPIOA->OTYPER |= GPIO_OTYPER_OT_3;
+ GPIOA->BSRR = GPIO_BSRR_BS_3; //open drain = no attenuation by default
+}
+
+/**
+ * @brief Set RX gain
+ * @param gain New RX gain: 1, 2, 4, 8 or 16
+ * @note Invalid values have no effect
+ * @note Only used for AIOC rev. >= 1.2, has no effect on other platforms
+ */
+static void MODEM_LL_SET_GAIN(uint8_t gain)
+{
+ switch(gain)
+ {
+ case 1:
+ //adc gain=1, switch to follower mode later
+ OPAMP2->CSR &= ~OPAMP_CSR_PGGAIN; //bias gain=16
+ OPAMP2->CSR |= OPAMP_CSR_PGGAIN_0 | OPAMP_CSR_PGGAIN_1;
+ break;
+ case 2:
+ OPAMP2->CSR &= ~OPAMP_CSR_PGGAIN; //bias gain=8
+ OPAMP2->CSR |= OPAMP_CSR_PGGAIN_1;
+ OPAMP1->CSR &= ~OPAMP_CSR_PGGAIN; //adc gain=2
+ //3.3%*2*8=52.8%
+ break;
+ case 4:
+ OPAMP2->CSR &= ~OPAMP_CSR_PGGAIN; //bias gain=4
+ OPAMP2->CSR |= OPAMP_CSR_PGGAIN_0;
+ OPAMP1->CSR &= ~OPAMP_CSR_PGGAIN; //adc gain=4
+ OPAMP1->CSR |= OPAMP_CSR_PGGAIN_0;
+ //3.3%*4*4=52.8%
+ break;
+ case 8:
+ OPAMP2->CSR &= ~OPAMP_CSR_PGGAIN; //bias gain=2
+ OPAMP1->CSR &= ~OPAMP_CSR_PGGAIN; //adc gain=8
+ OPAMP1->CSR |= OPAMP_CSR_PGGAIN_1;
+ //3.3%*8*2=52.8%
+ break;
+ case 16:
+ //bias gain=1, switch to follower mode later
+ OPAMP1->CSR &= ~OPAMP_CSR_PGGAIN; //adc gain=16
+ OPAMP1->CSR |= OPAMP_CSR_PGGAIN_0 | OPAMP_CSR_PGGAIN_1;
+ //3.3%*16*1=52.8%
+ break;
+ default:
+ return;
+ }
+
+ if(1 == gain)
+ {
+ //adc gain at 1 (follower), bias gain at 16 (just enable PGA here)
+ OPAMP1->CSR |= OPAMP_CSR_VMSEL;
+ OPAMP2->CSR &= ~OPAMP_CSR_VMSEL;
+ OPAMP2->CSR |= OPAMP_CSR_VMSEL_1;
+ }
+ else if(16 == gain)
+ {
+ //adc gain at 16 (just enable PGA here), bias gain at 1 (follower)
+ OPAMP1->CSR &= ~OPAMP_CSR_VMSEL;
+ OPAMP1->CSR |= OPAMP_CSR_VMSEL_1;
+ OPAMP2->CSR |= OPAMP_CSR_VMSEL;
+ }
+ else
+ {
+ //in any other case, enable PGA on both op amps
+ OPAMP1->CSR &= ~OPAMP_CSR_VMSEL;
+ OPAMP1->CSR |= OPAMP_CSR_VMSEL_1;
+ OPAMP2->CSR &= ~OPAMP_CSR_VMSEL;
+ OPAMP2->CSR |= OPAMP_CSR_VMSEL_1;
+ }
+}
+
+/**
+ * @brief Initialize ADC
+ * @return Utilized ADC pointer
+ */
+static volatile ADC_TypeDef* MODEM_LL_INITIALIZE_ADC(int32_t *bias)
+{
+ //OPAMP2 as ADC bias generator
+ OPAMP2->CSR |= OPAMP_CSR_FORCEVP; //reference voltage as non-inverting input
+ OPAMP2->CSR &= ~OPAMP_CSR_VMSEL; //programmable gain amplifier mode
+ OPAMP2->CSR |= OPAMP_CSR_VMSEL_1;
+ OPAMP2->CSR &= ~OPAMP_CSR_CALSEL; //reference = 3.3% VDDA
+ OPAMP2->CSR &= ~OPAMP_CSR_PGGAIN; //x16 gain to get 52.8% VDDA bias
+ OPAMP2->CSR |= OPAMP_CSR_PGGAIN_0 | OPAMP_CSR_PGGAIN_1;
+ OPAMP2->CSR &= ~OPAMP_CSR_TSTREF; //make sure reference voltage is enabled
+ OPAMP2->CSR |= OPAMP_CSR_OPAMPxEN; //enable op amp
+
+ //OPAMP1 as ADC preamp
+ OPAMP1->CSR &= ~OPAMP_CSR_FORCEVP; //make sure reference voltage is not used
+ OPAMP1->CSR &= ~OPAMP_CSR_VPSEL_1; //PA5 (signal input) as non-inverting inptu
+ OPAMP1->CSR |= OPAMP_CSR_VPSEL_0;
+ OPAMP1->CSR |= OPAMP_CSR_VMSEL; //input follower mode for now
+ OPAMP1->CSR |= OPAMP_CSR_OPAMPxEN; //enable op amp
+
+ MODEM_LL_SET_GAIN(1);
+
+ /* ADC input: PB2 (ADC2 channel 12) or PA5 via OPAMP1 (ADC1 channel 3) */
+ GPIOB->MODER |= GPIO_MODER_MODER2;
+ GPIOA->MODER |= GPIO_MODER_MODER5;
+ /*/4 prescaler */
+ RCC->CFGR2 &= ~RCC_CFGR2_ADCPRE12;
+ RCC->CFGR2 |= RCC_CFGR2_ADCPRE12_DIV4;
+
+ //configure ADC1 channel 3 first and check if there is bias (AIOC rev. >= 1.2)
+ ADC1->CFGR |= ADC_CFGR_CONT;
+ ADC1->CFGR &= ~ADC_CFGR_EXTEN;
+ /* 61.5 cycle sampling = 292 kHz */
+ ADC1->SMPR1 &= ~ADC_SMPR1_SMP3;
+ ADC1->SMPR1 |= ADC_SMPR1_SMP3_2 | ADC_SMPR1_SMP3_0;
+ ADC1->SQR1 &= ~ADC_SQR1_SQ1;
+ ADC1->SQR1 |= (3 << ADC_SQR1_SQ1_Pos); //channel 3
+ ADC1->SQR1 &= ~ADC_SQR1_L; //single conversion
+ ADC1->DIFSEL &= ~ADC_DIFSEL_DIFSEL_3;
+ /* Enable voltage regulator and perform calibration */
+ ADC1->CR &= ~ADC_CR_ADVREGEN;
+ ADC1->CR |= ADC_CR_ADVREGEN_0;
+ HAL_Delay(20);
+ ADC1->CR &= ~ADC_CR_ADCALDIF;
+ ADC1->CR |= ADC_CR_ADCAL;
+ while(ADC1->CR & ADC_CR_ADCAL)
+ ;
+ HAL_Delay(20);
+ /* Enable ADC */
+ ADC1->CR |= ADC_CR_ADEN;
+ while(!(ADC1->ISR & ADC_ISR_ADRDY))
+ ;
+ ADC1->CR |= ADC_CR_ADSTART;
+
+ *bias = 4325; //ADC bias is around 4325, because the opamp generates the bias at 3.3%*16=52.8% of VDDA
+
+ //collect some samples and average them
+ ADC1->DR;
+ uint32_t sum = 0;
+ for(uint8_t i = 0; i < 16; i++)
+ {
+ while(!(ADC1->ISR & ADC_ISR_EOC))
+ ;
+ sum += ADC1->DR;
+ }
+ sum /= 16;
+
+ //check whether the average is between 40% and 60% of the ADC range, that is, whether there is a proper bias present
+ if((sum < (uint32_t)(0.4f * 4096.f)) || ((sum > (uint32_t)(0.6f * 4096.f))))
+ {
+ //if not, disable ADC1 and set up ADC2 (old AIOC revision)
+ ADC1->CR |= ADC_CR_ADSTP;
+ while(ADC1->CR & ADC_CR_ADSTP)
+ ;
+ ADC1->CR |= ADC_CR_ADDIS;
+
+ ADC2->CFGR |= ADC_CFGR_CONT;
+ ADC2->CFGR &= ~ADC_CFGR_EXTEN;
+ /* 61.5 cycle sampling = 292 kHz */
+ ADC2->SMPR2 &= ~ADC_SMPR2_SMP12;
+ ADC2->SMPR2 |= ADC_SMPR2_SMP12_2 | ADC_SMPR2_SMP12_0;
+ ADC2->SQR1 &= ~ADC_SQR1_SQ1;
+ ADC2->SQR1 |= (12 << ADC_SQR1_SQ1_Pos); //channel 12
+ ADC2->SQR1 &= ~ADC_SQR1_L; //single conversion
+ ADC2->DIFSEL &= ~ADC_DIFSEL_DIFSEL_12;
+ /* Enable voltage regulator and perform calibration */
+ ADC2->CR &= ~ADC_CR_ADVREGEN;
+ ADC2->CR |= ADC_CR_ADVREGEN_0;
+ HAL_Delay(20);
+ ADC2->CR &= ~ADC_CR_ADCALDIF;
+ ADC2->CR |= ADC_CR_ADCAL;
+ while(ADC2->CR & ADC_CR_ADCAL)
+ ;
+ /* Enable ADC */
+ ADC2->CR |= ADC_CR_ADEN;
+ while(!(ADC2->ISR & ADC_ISR_ADRDY))
+ ;
+ ADC2->CR |= ADC_CR_ADSTART;
+
+ *bias = 4095; //assume bias to be in the middle in old AIOC revision
+
+ return ADC2;
+ }
+ else
+ return ADC1;
+}
+
+/**
+ * @brief Initialize DMA
+ * @param *buffer Target memory buffer
+ * @param count Number of words to be copied
+ * @param *adc Source ADC
+ */
+static void MODEM_LL_INITIALIZE_DMA(volatile void *buffer, uint16_t count, volatile ADC_TypeDef *adc)
+{
+ /* 16 bit memory region */
+ DMA1_Channel2->CCR |= DMA_CCR_MSIZE_0;
+ DMA1_Channel2->CCR &= ~DMA_CCR_MSIZE_1;
+ DMA1_Channel2->CCR |= DMA_CCR_PSIZE_0;
+ DMA1_Channel2->CCR &= ~DMA_CCR_PSIZE_1;
+ /* Enable memory pointer increment, circular mode and interrupt generation */
+ DMA1_Channel2->CCR |= DMA_CCR_MINC | DMA_CCR_CIRC | DMA_CCR_TCIE;
+ DMA1_Channel2->CNDTR = count;
+ DMA1_Channel2->CPAR = (uintptr_t)&(adc->DR);
+ DMA1_Channel2->CMAR = (uintptr_t)(buffer);
+ DMA1_Channel2->CCR |= DMA_CCR_EN;
+}
+
+/**
+ * @brief Initialize primary DAC
+ */
+static void MODEM_LL_INITIALIZE_DAC(void)
+{
+ DAC1->CR &= DAC_CR_WAVE1;
+ DAC1->CR &= ~DAC_CR_TEN1;
+ DAC1->CR |= DAC_CR_BOFF1;
+ DAC1->CR |= DAC_CR_EN1;
+}
+
+static void MODEM_LL_ADC_TIMER_INITIALIZE(void)
+{
+ /* 72 / 9 = 8 MHz */
+ TIM2->PSC = 8;
+ /* enable DMA call instead of standard interrupt */
+ TIM2->DIER |= TIM_DIER_UDE;
+}
+
+static void MODEM_LL_DAC_TIMER_INITIALIZE(void)
+{
+ /* 72 / 4 = 18 MHz */
+ TIM1->PSC = 3;
+ TIM1->DIER |= TIM_DIER_UIE;
+}
+
+static void MODEM_LL_BAUDRATE_TIMER_INITIALIZE(void)
+{
+ /* 72 / 4 = 18 MHz */
+ TIM3->PSC = 3;
+ TIM3->DIER |= TIM_DIER_UIE;
+}
+
+#define MODEM_LL_ADC_SET_SAMPLE_RATE(rate) (TIM2->ARR = (8000000 / (rate)) - 1)
+
+#define MODEM_LL_DAC_TIMER_CALCULATE_STEP(frequency) ((18000000 / (frequency)) - 1)
+
+#define MODEM_LL_BAUDRATE_TIMER_CALCULATE_STEP(frequency) ((18000000 / (frequency)) - 1)
+
+#endif
+
+#endif /* DRIVERS_MODEM_LL_H_ */
diff --git a/Core/Inc/drivers/modem_ll_bluepill.h b/Core/Inc/drivers/modem_ll_bluepill.h
new file mode 100644
index 0000000..533a346
--- /dev/null
+++ b/Core/Inc/drivers/modem_ll_bluepill.h
@@ -0,0 +1,209 @@
+/*
+Copyright 2020-2025 Piotr Wilkon
+This file is part of VP-Digi.
+
+VP-Digi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or
+(at your option) any later version.
+
+VP-Digi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with VP-Digi. If not, see .
+*/
+
+#ifndef DRIVERS_MODEM_LL_STM32F103_H_
+#define DRIVERS_MODEM_LL_STM32F103_H_
+
+#if defined(STM32F103xB) || defined(STM32F103x8)
+
+#include
+#include "stm32f1xx.h"
+
+#define MODEM_LL_DAC_MAX 255 /**< PWM max value */
+
+/**
+ * TIM1 is used for pushing samples to DAC (R2R or PWM) (clocked at 18 MHz)
+ * TIM3 is the baudrate generator for TX (clocked at 18 MHz)
+ * TIM4 is the PWM generator with no software interrupt
+ * TIM2 is the RX sampling timer with no software interrupt, but it directly calls DMA
+ */
+
+#define MODEM_LL_DMA_INTERRUPT_HANDLER DMA1_Channel2_IRQHandler
+#define MODEM_LL_DAC_INTERRUPT_HANDLER TIM1_UP_IRQHandler
+#define MODEM_LL_BAUDRATE_TIMER_INTERRUPT_HANDLER TIM3_IRQHandler
+
+#define MODEM_LL_DMA_IRQ DMA1_Channel2_IRQn
+#define MODEM_LL_DAC_IRQ TIM1_UP_IRQn
+#define MODEM_LL_BAUDRATE_TIMER_IRQ TIM3_IRQn
+
+#define MODEM_LL_DMA_TRANSFER_COMPLETE_FLAG (DMA1->ISR & DMA_ISR_TCIF2)
+#define MODEM_LL_DMA_CLEAR_TRANSFER_COMPLETE_FLAG() (DMA1->IFCR |= DMA_IFCR_CTCIF2)
+
+#define MODEM_LL_BAUDRATE_TIMER_CLEAR_INTERRUPT_FLAG() (TIM3->SR &= ~TIM_SR_UIF)
+#define MODEM_LL_BAUDRATE_TIMER_ENABLE() (TIM3->CR1 = TIM_CR1_CEN)
+#define MODEM_LL_BAUDRATE_TIMER_DISABLE() (TIM3->CR1 &= ~TIM_CR1_CEN)
+#define MODEM_LL_BAUDRATE_TIMER_SET_RELOAD_VALUE(val) (TIM3->ARR = (val))
+
+#define MODEM_LL_DAC_TIMER_CLEAR_INTERRUPT_FLAG (TIM1->SR &= ~TIM_SR_UIF)
+#define MODEM_LL_DAC_TIMER_SET_RELOAD_VALUE(val) (TIM1->ARR = (val))
+#define MODEM_LL_DAC_TIMER_SET_CURRENT_VALUE(val) (TIM1->CNT = (val))
+#define MODEM_LL_DAC_TIMER_ENABLE() (TIM1->CR1 |= TIM_CR1_CEN)
+#define MODEM_LL_DAC_TIMER_DISABLE() (TIM1->CR1 &= ~TIM_CR1_CEN)
+
+#define MODEM_LL_ADC_TIMER_ENABLE() (TIM2->CR1 |= TIM_CR1_CEN)
+#define MODEM_LL_ADC_TIMER_DISABLE() (TIM2->CR1 &= ~TIM_CR1_CEN)
+
+#define MODEM_LL_DAC_PUT_VALUE(value) (TIM4->CCR1 = (value))
+
+static inline void MODEM_LL_DCD_LED_ON(void)
+{
+ GPIOC->BSRR = GPIO_BSRR_BR13;
+ GPIOB->BSRR = GPIO_BSRR_BS5;
+}
+
+static inline void MODEM_LL_DCD_LED_OFF(void)
+{
+ GPIOC->BSRR = GPIO_BSRR_BS13;
+ GPIOB->BSRR = GPIO_BSRR_BR5;
+}
+
+#define MODEM_LL_PTT_ON(output) (GPIOB->BSRR = GPIO_BSRR_BS7)
+#define MODEM_LL_PTT_OFF(output) (GPIOB->BSRR = GPIO_BSRR_BR7)
+
+static void MODEM_LL_INITIALIZE_RCC(void)
+{
+ RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
+ RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
+ RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
+ RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
+ RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
+ RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
+ RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
+ RCC->AHBENR |= RCC_AHBENR_DMA1EN;
+ RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
+}
+
+static void MODEM_LL_INITIALIZE_OUTPUTS(void)
+{
+ /* DCD LEDs: PC13 (cathode driven - built-in LED on Blue Pill) and PB5 (anode driven) */
+ GPIOC->CRH |= GPIO_CRH_MODE13_1;
+ GPIOC->CRH &= ~GPIO_CRH_MODE13_0;
+ GPIOC->CRH &= ~GPIO_CRH_CNF13;
+ GPIOB->CRL |= GPIO_CRL_MODE5_1;
+ GPIOB->CRL &= ~GPIO_CRL_MODE5_0;
+ GPIOB->CRL &= ~GPIO_CRL_CNF5;
+ /* PTT: PB7 */
+ GPIOB->CRL |= GPIO_CRL_MODE7_1;
+ GPIOB->CRL &= ~GPIO_CRL_MODE7_0;
+ GPIOB->CRL &= ~GPIO_CRL_CNF7;
+}
+
+/**
+ * @brief Initialize ADC
+ * @return Utilized ADC pointer
+ */
+static volatile ADC_TypeDef* MODEM_LL_INITIALIZE_ADC(int32_t *bias)
+{
+ /* ADC input: PA0 */
+ GPIOA->CRL &= ~GPIO_CRL_CNF0;
+ GPIOA->CRL &= ~GPIO_CRL_MODE0;
+ /*/6 prescaler */
+ RCC->CFGR |= RCC_CFGR_ADCPRE_1;
+ RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;
+ ADC1->CR2 |= ADC_CR2_CONT;
+ ADC1->CR2 |= ADC_CR2_EXTSEL;
+ ADC1->SQR1 &= ~ADC_SQR1_L;
+ /* 41.5 cycle sampling */
+ ADC1->SMPR2 |= ADC_SMPR2_SMP0_2;
+ ADC1->SQR3 &= ~ADC_SQR3_SQ1;
+ ADC1->CR2 |= ADC_CR2_ADON;
+ /* calibrate */
+ ADC1->CR2 |= ADC_CR2_RSTCAL;
+ while (ADC1->CR2 & ADC_CR2_RSTCAL)
+ ;
+ ADC1->CR2 |= ADC_CR2_CAL;
+ while (ADC1->CR2 & ADC_CR2_CAL)
+ ;
+ ADC1->CR2 |= ADC_CR2_EXTTRIG;
+ ADC1->CR2 |= ADC_CR2_SWSTART;
+
+ *bias = 4095;
+ return ADC1;
+}
+
+/**
+ * @brief Initialize DMA
+ * @param *buffer Target memory buffer
+ * @param count Number of words to be copied
+ * @param *adc Source ADC
+ */
+static void MODEM_LL_INITIALIZE_DMA(volatile void *buffer, uint16_t count, volatile ADC_TypeDef *adc)
+{
+ /* 16 bit memory region */
+ DMA1_Channel2->CCR |= DMA_CCR_MSIZE_0;
+ DMA1_Channel2->CCR &= ~DMA_CCR_MSIZE_1;
+ DMA1_Channel2->CCR |= DMA_CCR_PSIZE_0;
+ DMA1_Channel2->CCR &= ~DMA_CCR_PSIZE_1;
+ /* enable memory pointer increment, circular mode and interrupt generation */
+ DMA1_Channel2->CCR |= DMA_CCR_MINC | DMA_CCR_CIRC | DMA_CCR_TCIE;
+ DMA1_Channel2->CNDTR = count;
+ DMA1_Channel2->CPAR = (uintptr_t)&(adc->DR);
+ DMA1_Channel2->CMAR = (uintptr_t)buffer;
+ DMA1_Channel2->CCR |= DMA_CCR_EN;
+}
+
+/**
+ * @brief Initialize DAC
+ */
+static void MODEM_LL_INITIALIZE_DAC(void)
+{
+ /* PWM output: PB6 */
+ GPIOB->CRL |= GPIO_CRL_CNF6_1;
+ GPIOB->CRL |= GPIO_CRL_MODE6;
+ GPIOB->CRL &= ~GPIO_CRL_CNF6_0;
+
+ /* 72 / 3 = 24 MHz to provide 8 bit resolution at around 100 kHz */
+ TIM4->PSC = 2;
+ /* 24 MHz / 256 = 94 kHz */
+ TIM4->ARR = 255;
+ TIM4->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;
+ TIM4->CCER |= TIM_CCER_CC1E;
+ TIM4->CR1 |= TIM_CR1_CEN;
+}
+
+static void MODEM_LL_ADC_TIMER_INITIALIZE(void)
+{
+ /* 72 / 9 = 8 MHz */
+ TIM2->PSC = 8;
+ /* enable DMA call instead of standard interrupt */
+ TIM2->DIER |= TIM_DIER_UDE;
+}
+
+static void MODEM_LL_DAC_TIMER_INITIALIZE(void)
+{
+ /* 72 / 4 = 18 MHz */
+ TIM1->PSC = 3;
+ TIM1->DIER |= TIM_DIER_UIE;
+}
+
+static void MODEM_LL_BAUDRATE_TIMER_INITIALIZE(void)
+{
+ /* 72 / 4 = 18 MHz */
+ TIM3->PSC = 3;
+ TIM3->DIER |= TIM_DIER_UIE;
+}
+
+#define MODEM_LL_ADC_SET_SAMPLE_RATE(rate) (TIM2->ARR = (8000000 / (rate)) - 1)
+
+#define MODEM_LL_DAC_TIMER_CALCULATE_STEP(frequency) ((18000000 / (frequency)) - 1)
+
+#define MODEM_LL_BAUDRATE_TIMER_CALCULATE_STEP(frequency) ((18000000 / (frequency)) - 1)
+
+#endif
+
+#endif /* DRIVERS_MODEM_LL_H_ */