rushabhcodes/desk-fidget-counter
A desk knob assembly combining a circuit board with PETG-printed mechanical parts and vendor CAD-modeled hardware.
- Version
- 5.6.1
- License
- MIT
- Stars
- 0
firmware/main.c
// SPDX-License-Identifier: MIT
#include "counter.h"
#include "button.h"
#include "lcd.h"
#include "stm32f0xx.h"
#include "tusb.h"
#include <stdio.h>
#include <string.h>
uint32_t SystemCoreClock = 48000000;
static volatile uint32_t ticks;
static Counter counter;
static bool stored = false, encoder_ok = true, peripherals_on = false;
static Dashboard dashboard;
static uint32_t cleared_at;
static bool clear_notice;
static unsigned active_slot = 0;
static uint32_t sequence = 0;
void SysTick_Handler(void) { ticks++; }
uint32_t tusb_time_millis_api(void) { return ticks; }
void USB_IRQHandler(void) {
EXTI->PR = 1u << 18; // USB wake-up shares this IRQ on STM32F072 (RM0091).
tud_int_handler(0);
}
static void pinmode(GPIO_TypeDef *g, unsigned p, unsigned mode) {
g->MODER = (g->MODER & ~(3u << (2 * p))) | (mode << (2 * p));
}
static void out(unsigned p, bool on) {
GPIOA->BSRR = 1u << (p + (on ? 0 : 16));
}
static void delay_ms(uint32_t n) {
uint32_t t = ticks;
while ((uint32_t)(ticks - t) < n) {
}
}
static void clear_motion(void) {
counter.pending = 0;
counter.scroll_fraction = 0;
counter.baseline = false;
}
void tud_mount_cb(void) { clear_motion(); }
void tud_umount_cb(void) { clear_motion(); dashboard.received=false; }
void tud_suspend_cb(bool remote_wakeup_en) {
(void)remote_wakeup_en;
clear_motion(); dashboard.received=false;
}
void tud_resume_cb(void) { clear_motion(); }
static void clock_run(void) {
FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY;
RCC->CR2 |= RCC_CR2_HSI48ON;
while (!(RCC->CR2 & RCC_CR2_HSI48RDY)) {
}
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_HSI48;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI48) {
}
}
static void usb_sleep(void) {
// RM0091 Stop-mode / USB suspend sections: USB's asynchronous resume wakes EXTI18.
// Keep IRQs masked until 48MHz is restored; a newly pending IRQ wakes WFI.
__disable_irq();
if (!tud_suspended() || !(USB->CNTR & USB_CNTR_LPMODE) ||
NVIC_GetPendingIRQ(USB_IRQn)) { __enable_irq(); return; }
SysTick->CTRL = 0;
SCB->ICSR = SCB_ICSR_PENDSTCLR_Msk;
PWR->CR = (PWR->CR & ~PWR_CR_PDDS) | PWR_CR_LPDS;
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
__DSB(); __WFI(); __ISB();
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
clock_run(); // Stop exits on HSI8; restore USB/application HSI48 before IRQ.
SysTick_Config(48000);
__enable_irq();
}
static void init(void) {
clock_run();
RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOBEN | RCC_AHBENR_GPIOCEN | RCC_AHBENR_GPIOFEN;
GPIOC->MODER = 0xffffffffu;
GPIOF->MODER = 0xffffffffu;
GPIOB->MODER =
0xffffffffu; // Unused GPIOs analog, configured pins overridden below.
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN | RCC_APB1ENR_USBEN | RCC_APB1ENR_CRSEN |
RCC_APB1ENR_TIM3EN | RCC_APB1ENR_SPI2EN | RCC_APB1ENR_PWREN;
EXTI->RTSR |= 1u << 18;
EXTI->IMR |= 1u << 18;
RCC->CFGR3 &= ~RCC_CFGR3_USBSW; // HSI48 is the USB clock.
CRS->CFGR =
47999u | (34u << 16) | (2u << 28); // 1kHz USB SOF, default error limit.
CRS->CR = CRS_CR_AUTOTRIMEN | CRS_CR_CEN | (32u << 8);
SysTick_Config(48000);
// Single front-side momentary button, PA0 active low with internal pull-up.
pinmode(GPIOA, 0, 0);
GPIOA->PUPDR = (GPIOA->PUPDR & ~3u) | 1u;
for (unsigned p=1;p<8;p++) pinmode(GPIOA,p,3);
pinmode(GPIOA,8,1);out(8,false);
pinmode(GPIOA,9,3);pinmode(GPIOA,10,3);pinmode(GPIOA,15,3);
GPIOB->BSRR = 1u << (2+16); // peripherals remain off until configured
pinmode(GPIOB, 2, 1);
pinmode(GPIOB, 6, 2);
pinmode(GPIOB, 7, 2);
GPIOB->OTYPER |= (1u << 6) | (1u << 7);
GPIOB->AFR[0] =
(GPIOB->AFR[0] & ~(0xffu << 24)) | (0x11u << 24); // PB6/7 AF1 I2C1.
pinmode(GPIOA, 11, 2);
pinmode(GPIOA, 12, 2); // AF0 USB; PA13/14 remain SWD.
// Conservative ~300kHz bus: low 1.67us, high 1.33us plus
// synchronizer/filter/rise time.
I2C1->TIMINGR = 0x30b00f13; // PRESC3=83.3ns, SCLDEL11, SDADEL0, H15, L19.
I2C1->CR1 = I2C_CR1_PE;
// Dual Schmitt outputs -> PB4/PB5 AF1 TIM3_CH1/CH2. Hardware keeps
// counting edges while a display row or FRAM transfer is in progress.
pinmode(GPIOB, 4, 2);
pinmode(GPIOB, 5, 2);
GPIOB->AFR[0] = (GPIOB->AFR[0] & ~(0xffu << 16)) | (0x11u << 16);
TIM3->ARR = 65535;
TIM3->CCMR1 = 1u | (13u << 4) | (1u << 8) | (13u << 12); // ~3.3us filters
TIM3->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E;
TIM3->SMCR = 3; // encoder mode 3, count both edges of both channels
TIM3->EGR = TIM_EGR_UG;
TIM3->CNT = 0;
TIM3->CR1 = TIM_CR1_CEN;
}
static void i2c_abort(void) {
I2C1->CR2 |= I2C_CR2_STOP;
I2C1->CR1 = 0;
I2C1->ICR = 0x3f38;
I2C1->CR1 = I2C_CR1_PE;
}
static bool wait_flag(uint32_t flag) {
uint32_t start = ticks;
while (!(I2C1->ISR & flag)) {
if ((I2C1->ISR & (I2C_ISR_NACKF | I2C_ISR_BERR | I2C_ISR_ARLO)) ||
(uint32_t)(ticks - start) > 3) {
i2c_abort();
return false;
}
}
return true;
}
static bool transfer(uint8_t addr, const uint8_t *w, unsigned wn, uint8_t *r,
unsigned rn) {
if (I2C1->ISR & I2C_ISR_BUSY) {
i2c_abort();
return false;
}
I2C1->ICR = 0x3f38;
if (wn) {
I2C1->CR2 =
(addr << 1) | (wn << 16) | I2C_CR2_START | (rn ? 0 : I2C_CR2_AUTOEND);
for (unsigned i = 0; i < wn; i++) {
if (!wait_flag(I2C_ISR_TXIS))
return false;
I2C1->TXDR = w[i];
}
if (!wait_flag(rn ? I2C_ISR_TC : I2C_ISR_STOPF))
return false;
}
if (rn) {
I2C1->CR2 = (addr << 1) | (rn << 16) | I2C_CR2_RD_WRN | I2C_CR2_START |
I2C_CR2_AUTOEND;
for (unsigned i = 0; i < rn; i++) {
if (!wait_flag(I2C_ISR_RXNE))
return false;
r[i] = (uint8_t)I2C1->RXDR;
}
if (!wait_flag(I2C_ISR_STOPF))
return false;
}
I2C1->ICR = I2C_ICR_STOPCF;
return true;
}
static bool fram_read(unsigned a, uint8_t *p, unsigned n) {
uint8_t w[2] = {(uint8_t)(a >> 8), (uint8_t)a};
return transfer(0x50, w, 2, p, n);
}
static bool fram_write(unsigned a, const uint8_t *p, unsigned n) {
uint8_t w[34];
if (n > 32)
return false;
w[0] = (uint8_t)(a >> 8);
w[1] = (uint8_t)a;
memcpy(w + 2, p, n);
return transfer(0x50, w, n + 2, NULL, 0);
}
static bool save(uint64_t turns) {
unsigned slot = active_slot ^ 1u, addr = slot * 64;
uint8_t p[32], verify[32], zero[4] = {0};
uint32_t seq;
uint64_t count;
record_encode(p, sequence + 1, turns);
if (!fram_write(addr + 28, zero, 4) || !fram_write(addr, p, 28) ||
!fram_write(addr + 28, p + 28, 4) || !fram_read(addr, verify, 32) ||
!record_decode(verify, &seq, &count) || count != turns ||
seq != sequence + 1)
return false;
active_slot = slot;
sequence = seq;
stored = true;
return true;
}
static void restore(void) {
uint8_t a[32], b[32];
uint32_t sa = 0, sb = 0;
uint64_t ca = 0, cb = 0;
bool va = fram_read(0, a, 32) && record_decode(a, &sa, &ca),
vb = fram_read(64, b, 32) && record_decode(b, &sb, &cb);
if (va && (!vb || (int32_t)(sa - sb) > 0)) {
active_slot = 0;
sequence = sa;
counter_init(&counter, ca);
stored = true;
} else if (vb) {
active_slot = 1;
sequence = sb;
counter_init(&counter, cb);
stored = true;
} else
counter_init(&counter, 0); // Require deliberate RESET CONFIRM / 3s button
// to initialize unknown FRAM.
}
static void reply(const char *s) {
if (tud_cdc_connected()) {
tud_cdc_write_str(s);
tud_cdc_write_flush();
}
}
static void status(void) {
char b[320], turns[21];
counter_format_total(counter.turns, turns);
snprintf(b,sizeof b,
"{\"device\":\"desk-fidget-counter\",\"protocol\":1,\"firmware\":\"5.0.0\","
"\"turns\":%s,\"progress\":%ld,\"storage_ok\":%s,"
"\"encoder_timer_running\":%s,\"scroll_on\":%s,\"rejected\":%lu,"
"\"lcd_transport_ok\":%s,\"host_data_fresh\":%s}\r\n",
turns,(long)counter.progress,stored?"true":"false",
encoder_ok?"true":"false",counter.scrolling?"true":"false",
(unsigned long)counter.rejected,lcd_ok()?"true":"false",
dashboard_fresh(&dashboard,ticks)?"true":"false");
reply(b);
}
static void reset_count(void) {
if (save(0)) {
counter_init(&counter, 0);
cleared_at = ticks; clear_notice = true;
reply("OK RESET\r\n");
} else {
stored = false;
reply("ERROR STORAGE\r\n");
}
}
static void console(void) {
static char line[64];
static unsigned n = 0;
static bool overflow = false;
unsigned bytes=0;
while (tud_cdc_available() && bytes++<64) {
char c = (char)tud_cdc_read_char();
if (c == '\r')
continue;
if (c == '\n') {
line[n] = 0;
if (overflow)
reply("ERROR LINE TOO LONG\r\n");
else if (!strcmp(line, "COUNT") || !strcmp(line, "STATUS"))
status();
else if (!strcmp(line, "RESET CONFIRM"))
reset_count();
else if (!strcmp(line, "HELP"))
reply("COUNT | STATUS | RESET CONFIRM | VERSION | DASH <clock date cpu ram battery ac>\r\n");
else if (!strcmp(line, "VERSION"))
reply("Desk Knob Rev E / firmware 5.0.0 / TinyUSB 0.21.0\r\n");
else if (!strncmp(line,"DASH ",5)) {
if (!dashboard_receive(&dashboard,line,ticks)) reply("ERROR DASH\r\n");
} else
reply("ERROR COMMAND\r\n");
n = 0;
overflow = false;
} else if (n < sizeof(line) - 1)
line[n++] = c;
else
overflow = true;
}
}
int main(void) {
init();
delay_ms(50);
restore();
tusb_rhport_init_t cfg = {.role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_FULL};
tusb_init(0, &cfg);
uint32_t sample_at = ticks;
bool scroll_mode = false; // Safe power-on default: count without host scrolling.
ButtonState mode_button = {0};
while (1) {
tud_task();
uint32_t now = ticks;
bool awake = tud_mounted() && !tud_suspended();
if (awake != peripherals_on) {
clear_motion();
counter.progress=0; // Cannot infer turns made while the optical LED is off.
if (awake) {
GPIOB->BSRR=1u<<2;
delay_ms(5); // TPS22919 controlled ramp before SPI is driven
lcd_power(true,ticks);
} else {
lcd_power(false,now); // All LCD signal pins low before removing its rail
GPIOB->BSRR=1u<<(2+16);
}
peripherals_on=awake;
}
ButtonEvent event = button_update(&mode_button, !(GPIOA->IDR & 1u), awake, now);
if (event == BUTTON_SHORT) {
scroll_mode = !scroll_mode;
counter.pending = 0;
counter.scroll_fraction = 0; // Preserve partial-turn counting across mode changes.
} else if (event == BUTTON_LONG) {
reset_count();
}
if ((uint32_t)(now - sample_at) >= 2) {
sample_at = now;
encoder_ok = (TIM3->CR1 & TIM_CR1_CEN) != 0;
uint16_t raw = (uint16_t)TIM3->CNT;
bool changed = counter_sample(
&counter, raw, encoder_ok && stored && peripherals_on, now,
scroll_mode && awake);
if (changed && !save(counter.turns)) {
stored = false;
counter.baseline = false;
}
}
if (tud_hid_ready() && counter.pending) {
int8_t wheel = counter_report(&counter);
tud_hid_mouse_report(0, 0, 0, 0, wheel, 0);
}
console();
now=ticks; // FRAM/console work can advance time before the UI snapshot.
if (clear_notice && (uint32_t)(now-cleared_at)>=2000) clear_notice=false;
LcdView view = {.turns=counter.turns,.host=dashboard,.now=now,
.storage_ok=stored,.scroll_on=scroll_mode,.usb_ready=awake,
.encoder_ok=encoder_ok,.hold_ms=button_hold_ms(&mode_button,now),
.count_cleared=clear_notice && (uint32_t)(now-cleared_at)<2000};
if (peripherals_on) lcd_task(now,&view);
if (tud_suspended() && !peripherals_on) usb_sleep();
else __WFI();
}
}
