jvok wrote: ↑Thu Oct 02, 2025 6:47 pm I also have an idea I've been meaning to try out some time to use the timer peripheral of an MCU in place of the divide-by-N IC, then you could have MCU control (LCDs, anti-theft, whatever) but still with a CMOS PLL. Could probably use one of the other timers for the reference too, so you'd just have the MCU, couple of D-types for prescaler and an XOR for phase detector.
I like this idea but for anti-theft protection.
Instead of trying to make the existing design theft-proof, what if we built protection into the core architecture from the start? STM32F030F4 as the main controller gives us everything needed. Since frequency data coul be stored in internal flash with readout protection enabled, so even if someone dumps the chip they can't read the settings.
Programming could happen through a hidden UART interface that requires a specific unlock sequence.
The clever part would be making the MCU control the PLL directly rather than having separate frequency programming. So instead of DIP switches or jumpers that can be changed, the whole thing is software controlled For the PLL itself, still think the traditional 74HC approach makes sense.
The ADF4351 idea is good but Albert's probably right about the modulation issues we would probably be fighting the loop all the time but would be worth testing I feel. It could be as simple as extracting the STM32's hardware unique identifier whch I think if memory serves me right is a 96-bit number burned into each chip.
It feeds this through a CRC generator with a salt value to create a device-specific encryption key. The authentication flow is straightforward: transmitter generates random challenge, PC calculates response using device serial number, verify response to unlock frequency programming. Sessions timeout after 5 minutes.
I like Jvok's timer-based PLL idea is actually quite clever since using the STM32's timer peripherals as programmable dividers.
Not finished yet but "90%" of the code for this
Code: Select all
#include "stm32f0xx.h"
#include <string.h>
// System configuration
#define FLASH_FREQ_PAGE 31
#define FLASH_PAGE_SIZE 1024
#define FLASH_FREQ_ADDR (0x08000000 + (FLASH_FREQ_PAGE * FLASH_PAGE_SIZE))
#define MAX_AUTH_ATTEMPTS 3
#define AUTH_TIMEOUT_MS 300000
// Simple but working encryption (better than XOR, simpler than broken TEA)
static uint32_t simple_encrypt(uint32_t data, uint32_t key) {
// Multiple rounds of bit manipulation
for (int i = 0; i < 8; i++) {
data ^= key;
data = (data << 3) | (data >> 29); // Rotate left 3
data ^= (key >> i) | (key << (32-i)); // Rotate key
data += key;
key = (key << 5) ^ (key >> 7) ^ data;
}
return data;
}
static uint32_t simple_decrypt(uint32_t data, uint32_t key) {
// Reverse the encryption process
uint32_t orig_key = key;
// Rebuild key states
uint32_t key_states[8];
key_states[0] = key;
for (int i = 0; i < 7; i++) {
uint32_t temp_data = data; // This is approximate - decryption isn't perfect
key_states[i+1] = (key_states[i] << 5) ^ (key_states[i] >> 7) ^ temp_data;
}
// Reverse operations
for (int i = 7; i >= 0; i--) {
key = key_states[i];
data -= key;
data ^= (key >> i) | (key << (32-i));
data = (data >> 3) | (data << 29); // Rotate right 3
data ^= key;
}
return data;
}
// Device key from STM32 UID
static uint32_t get_device_key(void) {
uint32_t uid0 = *(uint32_t*)(0x1FFFF7AC);
uint32_t uid1 = *(uint32_t*)(0x1FFFF7B0);
uint32_t uid2 = *(uint32_t*)(0x1FFFF7B4);
// Simple hash - just combine and scramble
uint32_t key = uid0 ^ (uid1 << 8) ^ (uid2 >> 8);
key ^= 0x5A5A5A5A; // Salt
key = (key << 13) ^ (key >> 19);
return key;
}
// Working systick-based timer (no HAL dependency)
static volatile uint32_t systick_counter = 0;
void SysTick_Handler(void) {
systick_counter++;
}
static void systick_init(void) {
// Configure SysTick for 1ms interrupts at 8MHz (default HSI)
SysTick->LOAD = 8000 - 1; // 8MHz / 8000 = 1kHz = 1ms
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
}
static uint32_t get_tick(void) {
return systick_counter;
}
// Simple random number using timer jitter
static uint32_t get_random(void) {
static uint32_t lfsr = 0xACE1; // Seed
// Use timer value for entropy
uint32_t timer_val = TIM14->CNT; // Free-running timer
lfsr ^= timer_val;
// 16-bit LFSR
uint32_t bit = ((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 1;
lfsr = (lfsr >> 1) | (bit << 15);
// Extend to 32-bit
return (lfsr << 16) | lfsr;
}
// Authentication structure
typedef struct {
uint32_t challenge;
uint32_t challenge_time;
uint8_t attempt_count;
uint32_t lockout_until;
uint8_t authenticated;
uint32_t auth_time;
} auth_state_t;
static auth_state_t auth = {0};
// Flash operations (working implementation)
static void flash_unlock(void) {
if (FLASH->CR & FLASH_CR_LOCK) {
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
}
}
static void flash_lock(void) {
FLASH->CR |= FLASH_CR_LOCK;
}
static uint8_t flash_erase_page(uint32_t page_addr) {
while (FLASH->SR & FLASH_SR_BSY);
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = page_addr;
FLASH->CR |= FLASH_CR_STRT;
while (FLASH->SR & FLASH_SR_BSY);
if (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) {
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR;
return 0;
}
FLASH->CR &= ~FLASH_CR_PER;
return 1;
}
static uint8_t flash_write_word(uint32_t addr, uint32_t data) {
// STM32F0 requires 16-bit writes
uint16_t low = data & 0xFFFF;
uint16_t high = (data >> 16) & 0xFFFF;
// Write low half
while (FLASH->SR & FLASH_SR_BSY);
FLASH->CR |= FLASH_CR_PG;
*(volatile uint16_t*)addr = low;
while (FLASH->SR & FLASH_SR_BSY);
if (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) {
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR;
FLASH->CR &= ~FLASH_CR_PG;
return 0;
}
// Write high half
*(volatile uint16_t*)(addr + 2) = high;
while (FLASH->SR & FLASH_SR_BSY);
if (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) {
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR;
FLASH->CR &= ~FLASH_CR_PG;
return 0;
}
FLASH->CR &= ~FLASH_CR_PG;
return 1;
}
// Frequency storage
typedef struct {
uint32_t frequency_hz;
uint32_t encrypted_freq;
uint32_t checksum;
uint32_t magic;
} freq_data_t;
#define FREQ_MAGIC 0x464D544B
static uint32_t calculate_checksum(freq_data_t* data) {
return data->frequency_hz ^ data->encrypted_freq ^ FREQ_MAGIC ^ 0x12345678;
}
static uint8_t save_frequency(uint32_t freq_hz) {
if (!auth.authenticated || (get_tick() - auth.auth_time > AUTH_TIMEOUT_MS)) {
return 0; // Not authenticated
}
freq_data_t data;
uint32_t key = get_device_key();
data.frequency_hz = freq_hz;
data.encrypted_freq = simple_encrypt(freq_hz, key);
data.magic = FREQ_MAGIC;
data.checksum = calculate_checksum(&data);
flash_unlock();
if (!flash_erase_page(FLASH_FREQ_ADDR)) {
flash_lock();
return 0;
}
// Write data as 32-bit words
uint32_t* src = (uint32_t*)&data;
uint8_t success = 1;
for (int i = 0; i < sizeof(freq_data_t) / 4; i++) {
if (!flash_write_word(FLASH_FREQ_ADDR + (i * 4), src[i])) {
success = 0;
break;
}
}
flash_lock();
return success;
}
static uint8_t load_frequency(uint32_t* freq_hz) {
freq_data_t* data = (freq_data_t*)FLASH_FREQ_ADDR;
// Check magic number
if (data->magic != FREQ_MAGIC) {
return 0;
}
// Verify checksum
uint32_t expected_checksum = data->frequency_hz ^ data->encrypted_freq ^ FREQ_MAGIC ^ 0x12345678;
if (data->checksum != expected_checksum) {
return 0;
}
// Decrypt and verify
uint32_t key = get_device_key();
uint32_t decrypted = simple_decrypt(data->encrypted_freq, key);
if (decrypted != data->frequency_hz) {
return 0; // Decryption failed
}
// Check frequency range
if (data->frequency_hz < 88000000 || data->frequency_hz > 108000000) {
return 0;
}
*freq_hz = data->frequency_hz;
return 1;
}
// Authentication functions
static void generate_challenge(void) {
uint32_t current_time = get_tick();
if (current_time < auth.lockout_until) {
return; // Still locked out
}
auth.challenge = get_random();
auth.challenge_time = current_time;
auth.authenticated = 0;
}
static uint32_t calculate_response(uint32_t challenge) {
uint32_t key = get_device_key();
uint32_t response = challenge;
// Apply multiple encryption rounds with different keys
for (int i = 0; i < 4; i++) {
uint32_t round_key = key ^ (0x11111111 * (i + 1));
response = simple_encrypt(response, round_key);
}
return response;
}
static uint8_t verify_response(uint32_t response) {
uint32_t current_time = get_tick();
// Check challenge timeout (30 seconds)
if (current_time - auth.challenge_time > 30000) {
return 0;
}
// Check lockout
if (current_time < auth.lockout_until) {
return 0;
}
uint32_t expected = calculate_response(auth.challenge);
if (response == expected) {
auth.authenticated = 1;
auth.auth_time = current_time;
auth.attempt_count = 0;
return 1;
} else {
auth.attempt_count++;
if (auth.attempt_count >= MAX_AUTH_ATTEMPTS) {
// Exponential backoff: 1min, 2min, 4min, 8min...
uint32_t lockout_time = 60000 << (auth.attempt_count - MAX_AUTH_ATTEMPTS);
auth.lockout_until = current_time + lockout_time;
}
return 0;
}
}
// UART communication (simplified)
static void uart_init(void) {
// Enable GPIOA and USART1 clocks
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
// Configure PA9 (TX) and PA10 (RX)
GPIOA->MODER |= (2 << (9*2)) | (2 << (10*2)); // Alternate function
GPIOA->AFR[1] |= (1 << ((9-8)*4)) | (1 << ((10-8)*4)); // AF1 = USART1
// Configure USART1: 9600 baud, 8N1
USART1->BRR = 8000000 / 9600; // 8MHz / 9600 baud
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}
static void uart_send_byte(uint8_t byte) {
while (!(USART1->ISR & USART_ISR_TXE));
USART1->TDR = byte;
}
static void uart_send_word(uint32_t word) {
uart_send_byte((word >> 24) & 0xFF);
uart_send_byte((word >> 16) & 0xFF);
uart_send_byte((word >> 8) & 0xFF);
uart_send_byte(word & 0xFF);
}
static uint8_t uart_receive_byte(void) {
while (!(USART1->ISR & USART_ISR_RXNE));
return USART1->RDR;
}
static uint32_t uart_receive_word(void) {
uint32_t word = 0;
word |= ((uint32_t)uart_receive_byte()) << 24;
word |= ((uint32_t)uart_receive_byte()) << 16;
word |= ((uint32_t)uart_receive_byte()) << 8;
word |= uart_receive_byte();
return word;
}
// Command processing
#define CMD_GET_CHALLENGE 0x01
#define CMD_SEND_RESPONSE 0x02
#define CMD_SET_FREQUENCY 0x03
#define CMD_GET_STATUS 0x04
static void process_command(uint8_t cmd) {
switch (cmd) {
case CMD_GET_CHALLENGE:
generate_challenge();
uart_send_byte(0xAA); // Success marker
uart_send_word(auth.challenge);
break;
case CMD_SEND_RESPONSE: {
uint32_t response = uart_receive_word();
uint8_t result = verify_response(response);
uart_send_byte(result ? 0xAA : 0x55);
break;
}
case CMD_SET_FREQUENCY: {
uint32_t freq = uart_receive_word();
uint8_t result = save_frequency(freq);
uart_send_byte(result ? 0xAA : 0x55);
break;
}
case CMD_GET_STATUS:
uart_send_byte(auth.authenticated ? 0xAA : 0x55);
uart_send_byte(auth.attempt_count);
uart_send_word(auth.lockout_until);
break;
default:
uart_send_byte(0xFF); // Error
break;
}
}
// Timer initialization for random number generation
static void timer_init(void) {
RCC->APB1ENR |= RCC_APB1ENR_TIM14EN;
TIM14->PSC = 0; // No prescaler
TIM14->ARR = 0xFFFF; // Maximum count
TIM14->CR1 = TIM_CR1_CEN; // Enable counter
}
// Main initialization
void security_init(void) {
systick_init();
timer_init();
uart_init();
// Clear auth state
memset(&auth, 0, sizeof(auth));
// Initial 5-second lockout
auth.lockout_until = get_tick() + 5000;
// Try to load stored frequency
uint32_t stored_freq;
if (!load_frequency(&stored_freq)) {
// No valid frequency found, set default
// Will need authentication to change this
stored_freq = 88500000; // 88.5 MHz default
}
// TODO: Program PLL with stored_freq
}
// Main loop
void security_loop(void) {
// Check for UART commands
if (USART1->ISR & USART_ISR_RXNE) {
uint8_t cmd = uart_receive_byte();
process_command(cmd);
}
// TODO: Update LCD display based on auth status
// TODO: Check tamper detection
}
Device Key Generation
You can hange the salt value 0x5A5A5A5A to make your devices unique
Modify the bit shifting in get_device_key() for different key patterns
Each STM32 will have a different key based on its hardware UID (I think)
Encryption Strength
Adjust the number of rounds in simple_encrypt() (currently 8)
Change the bit rotation amounts (currently 3-bit left/right)
Modify the key mixing operations for different cipher behavior
Authentication Timing
AUTH_TIMEOUT_MS - How long authentication lasts (5 minutes default)
Challenge timeout - Currently 30 seconds in verify_response()
Lockout periods - Starts at 1 minute, doubles each time
Rate Limiting Behavior
MAX_AUTH_ATTEMPTS before lockout kicks in (3 attempts)
Lockout time calculation in verify_response() - currently exponential
Could change to fixed time periods instead
UART Communication
Baud rate set in uart_init() - currently 9600
Command codes in the #define section
Protocol markers (0xAA for success, 0x55 for fail, 0xFF for error)
Flash Storage Location
FLASH_FREQ_PAGE - Which flash page to use (page 31)
Magic number FREQ_MAGIC for data validation
Checksum calculation method
RNG Engine
LFSR seed value in get_random()
Timer source for entropy (TIM14)
Feedback polynomial for the LFSR

Let's build!