commit d1e7ce0776d646ddc7017228cfea5127ebd7afef Author: Alexis Lockwood Date: Mon Jan 2 11:55:41 2023 -0700 Dump prerelease code, needs cleaned up diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6203d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Ignore list for: c +*.d +*.o +*.obj +*.elf +*.map +*.gch +*.pch +*.lib +*.a +*.so +*.out +*.su +*.bin +*.eep +*.hex +*.lss +*.dSYM + +# Ignore list for: kicad +*.bck +*.kicad_pcb-bak +*.sch-bak +*-cache +*-backups +*.kicad_prl diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..316c395 --- /dev/null +++ b/meson.build @@ -0,0 +1,29 @@ +project('timdac', 'c', default_options: ['default_library=static']) + +o_hw = get_option('timdac_hw') + +sources = ['src/timdac.c', 'src/timdac_ll.c'] +dependencies = [] +defs = [] + +if o_hw == 'ch32v103' + ch32v103_proj = subproject('ch32v103-meson') + ch32v103_dep = ch32v103_proj.get_variable('ch32v103_dep') + dependencies += ch32v103_dep + sources += ['src/timdac_hw_ch32v103.c'] +endif + +add_project_arguments(defs, language: ['c', 'cpp']) + +timdac_lib = library( + 'timdac', + sources, + dependencies: dependencies, + include_directories: ['src'], +) + +libtimdac_dep = declare_dependency( + compile_args: defs, + include_directories: include_directories('inc'), + link_with: timdac_lib, +) diff --git a/src/timdac.c b/src/timdac.c new file mode 100644 index 0000000..7077a6f --- /dev/null +++ b/src/timdac.c @@ -0,0 +1,132 @@ +// intellectual property is bullshit bgdc + +#include "timdac.h" +#include "timdac_config.h" + +#include +#include +#include +#include +#include + +// Reserved channel numbers to indicate a pending queue-jumper. First, a queue +// jumper is written into ->pend. The ISR swaps that with PEND_SLOT. PEND_SLOT +// is like NO_PEND, but timdac_set will not pend another channel while it's set +// - ensuring we do not flood the sequence with queue jumpers. Once PEND_SLOT +// comes back around to the ISR, it is replaced with NO_PEND. +#define NO_PEND UINT8_MAX +#define PEND_SLOT (NO_PEND - 1) + +// Channels are stored as 32-bit integers, packing a 16-bit signed int into the +// low half and flags into the high half. +#define CHAN_FLAG_MASK 0xFFFF0000u +#define CHAN_VALUE_MASK 0x0000FFFFu +#define ENCODE_CHAN(value, flags) \ + ((uint32_t)(uint16_t)(value) | ((flags) & CHAN_FLAG_MASK)) +#define CHAN_VALUE(chan) ((int16_t)(uint16_t)((chan) & CHAN_VALUE_MASK)) + +#define CHAN_FLAG_SLOW 0x00010000u + +void timdac_init(timdac_t * tds) +{ + tds->chan = 0; + tds->pend = NO_PEND; + tds->count = 0; + + for (size_t i = 0; i < tds->n_channels; i++) + tds->channels[i] = CHAN_FLAG_SLOW; + + timdac_ll_init(tds->td); +} + +void timdac_start(timdac_t * tds) +{ + timdac_ll_idle(tds->td); +} + +bool timdac_set(timdac_t * tds, uint8_t chan, int32_t value) +{ + if (chan >= tds->n_channels) + return false; + + int32_t prev = CHAN_VALUE(atomic_load(&tds->channels[chan])); + bool slow = prev & CHAN_FLAG_SLOW; + + if ((value < 0 && prev > 0) || (value > 0 && prev < 0)) + slow = true; + else + slow |= abs(value - prev) >= TIMDAC_SLOW_DELTA; + + bool stable = atomic_compare_exchange_strong( + &tds->channels[chan], + &prev, + ENCODE_CHAN(value, slow ? CHAN_FLAG_SLOW : 0) + ); + + if (!stable) + atomic_store(&tds->channels[chan], + ENCODE_CHAN(value, CHAN_FLAG_SLOW)); + + unsigned no_pend = NO_PEND; + atomic_compare_exchange_strong( + &tds->pend, + &no_pend, + chan + ); + + return true; +} + +bool timdac_pending(timdac_t * tds) +{ + return atomic_load(&tds->pend) < PEND_SLOT; +} + +void timdac_poll(timdac_t * tds) +{ + if (timdac_ll_poll(tds->td)) + return; + + _Static_assert( + TIMDAC_TUNE_INTERVAL <= UINT8_MAX, + "Calibration interval too high!"); + + if (tds->count >= TIMDAC_TUNE_INTERVAL || !tds->td->tune) + { + // Reached the end, time to calibrate + tds->count = 0; + if (timdac_ll_tune(tds->td)) + return; + } + + uint8_t chan; + unsigned pend = atomic_exchange(&tds->pend, PEND_SLOT); + + if (pend < PEND_SLOT) + { + chan = tds->pend; + } + else + { + chan = tds->chan; + uint8_t next_chan = chan + 1; + if (next_chan >= tds->n_channels) + next_chan = 0; + tds->chan = next_chan; + + atomic_compare_exchange_strong(&tds->pend, &pend, NO_PEND); + } + + uint32_t chancode = atomic_fetch_and( + &tds->channels[chan], + ~CHAN_FLAG_SLOW + ); + + tds->count++; + timdac_ll_emit( + tds->td, + chan, + CHAN_VALUE(chancode), + chancode & CHAN_FLAG_SLOW + ); +} diff --git a/src/timdac.h b/src/timdac.h new file mode 100644 index 0000000..d082688 --- /dev/null +++ b/src/timdac.h @@ -0,0 +1,74 @@ +// intellectual property is bullshit bgdc + +#ifndef TIMDAC_H +#define TIMDAC_H + +// ============================================================================ +// TIMDAC +// ---------------------------------------------------------------------------- +// +// NOTE - estimated CPU utilization - 0.9% at 72 MHz, 12.25us total +// (5.48us discharge, 2.29us pulse, 2.14us stabilize, 2.29us transfer) across a +// 1.29ms cycle +// ============================================================================ + +#include +#include +#include +#include +#include +#include "timdac_ll.h" + +#define TIMDAC_SCANNER_MAX_CHANNELS 32 + +// timdac_scanner provides a higher level interface to timdac, allowing the +// user to simply write output values as they need changing. When in use, it +// takes full ownership of the timdac; you should never call any timdac_ +// functions including timdac_init(). + +typedef struct timdac_s { + // #### BEGIN - USER MUST INITIALIZE #### + + // Pointer to the timdac low-level instance in use + timdac_ll_t * td; + + // Array of output channel values + atomic_uint * channels; + + // Number of channels, must be no more than TIMDAC_SCANNER_MAX_CHANNELS + uint8_t n_channels; + + // #### END - USER MUST INITIALIZE #### + + sig_atomic_t volatile chan; // Current channel number + sig_atomic_t volatile count; // How many channels have been emitted since tuning + atomic_uint pend; // A pending immediate-update channel +} timdac_t; + +// Initialize the DAC and DAC scanner. You must have filled in the "USER MUST +// INITIALIZE" fields in the struct. +void timdac_init(timdac_t * tds); + +// Start the scanner, if using interrupts. If you are not using interrupts, +// simply poll in a loop when you want to run - do not call this. +void timdac_start(timdac_t * tds); + +// Set a new value for an output channel. If no other channel has a pending +// immediate update, this channel will also be added to the pend slot so that +// it can be updated as soon as the DAC is ready without waiting for a full +// cycle. +// +// Returns true on success. Only fails on an invalid channel number. +bool timdac_set(timdac_t * tds, uint8_t chan, int32_t value); + +// Return whether an update is currently pending. Only one channel can hold the +// pending slot at a time, so if you post an update while another channel is +// pending, the newer update will not be written to the output until it is +// next hit in the normal scanning cycle. To ensure the fastest update, you +// can poll for this flag to clear before calling timdac_set(). +bool timdac_pending(timdac_t * tds); + +// Poll the scanner. Should be called periodically. +void timdac_poll(timdac_t * tds); + +#endif // !defined(TIMDAC_H) diff --git a/src/timdac_config.h b/src/timdac_config.h new file mode 100644 index 0000000..a426b80 --- /dev/null +++ b/src/timdac_config.h @@ -0,0 +1,66 @@ +// intellectual property is bullshit bgdc + +#ifndef TIMDAC_CONFIG_H +#define TIMDAC_CONFIG_H + +#include +#include +#include + +#define TIMDAC_N_CHANNELS 4 + +// ---------------------------------------------------------------------------- +// HARDWARE DRIVER CONFIGURATION - These may differ between hardware drivers, +// check the documentation in yours. + +// CH32V103 timer, must be a general-purpose timer +#define TIMDAC_HW_TIMER TIM3 + +// CH32V103 output compare channel +#define TIMDAC_HW_TIMER_CHANNEL 1 + +// CH32V103 timer interrupt. Optional, if you don't define this you must poll. +#define TIMDAC_HW_TIMER_IRQn TIM3_IRQn +// CH32V103 timer interrupt preemption priority +#define TIMDAC_HW_TIMER_IRQPRIO 3 +// CH32V103 timer interrupt subpriority +#define TIMDAC_HW_TIMER_IRQSUBPRIO 3 + +// CH32V103 GPIO and pin: timer output compare +// You are responsible for making sure this and TIMDAC_HW_TIMER_CHANNEL match +// up, and also for applying any GPIO_PinRemapConfig() and clock startup as +// necessary prior to initializing timdac. +#define TIMDAC_HW_GPIO_TIMER GPIOC, GPIO_Pin_6 + +// CH32V103 GPIO and pin: mux inhibit +#define TIMDAC_HW_GPIO_MUXINHIBIT GPIOC, GPIO_Pin_3 + +// CH32V103 GPIO and pin: integrator discharge +#define TIMDAC_HW_GPIO_DISCHARGE GPIOC, GPIO_Pin_5 + +// CH32V103 GPIO and pin: polarity +// OPTIONAL, do not define if polarity switch is not implemented +#define TIMDAC_HW_GPIO_POLARITY GPIOC, GPIO_Pin_4 + +// CH32V103 GPIO and pin: tuning comparator +#define TIMDAC_HW_GPIO_TUNE GPIOC, GPIO_Pin_7 + +// CH32V103 tuning comparator polarity, true = noninverting +#define TIMDAC_HW_GPIO_TUNE_POL false + +// CH32V103 GPIO: channel select. All channel select pins must be on the same +// port +#define TIMDAC_HW_GPIO_CHAN_PORT GPIOC + +// CH32V103 pins: channel select. Start from the LSB, omit or zero unused +// bits. Up to 8 bits. +#define TIMDAC_HW_GPIO_CHAN_PINS GPIO_Pin_0, GPIO_Pin_1, GPIO_Pin_2 + +#define TIMDAC_TIMERFREQ_HZ 72000000 + +// ---------------------------------------------------------------------------- +// TIMDAC CONFIGURATION + +#include "timdac_config_ref1p0.h" + +#endif // !defined(TIMDAC_CONFIG_H) diff --git a/src/timdac_config_ref1p0.h b/src/timdac_config_ref1p0.h new file mode 100644 index 0000000..5155356 --- /dev/null +++ b/src/timdac_config_ref1p0.h @@ -0,0 +1,106 @@ +// intellectual property is bullshit bgdc + +// ============================================================================ +// TIMDAC REFERENCE DESIGN CONFIGURATION - REFERENCE DESIGN 1.0 +// +// This file is designed to be #included from your main timdac_config.h, and +// provides all the configuration specified for the timdac hardware reference +// design. These parameters should not be changed without good reason, as they +// can affect the DAC performance in surprising ways. If you do need to change +// them, you should #undef and re-#define them after including this file. +// +// The macros in this file depend on a definition in timdac_config.h: +// +// #define TIMDAC_TIMERFREQ_HZ 72000000 +// This definition should be set to the base operating frequency of your +// system's timer in Hz. It is strongly recommended not to deviate far +// from the design frequency of 72 MHz. +// +// For the curious, there is a good amount of documentation in this file +// explaining how the parameters affect the output, but for a basic +// implementation you are not required to understand this. +// ============================================================================ + +#ifndef TIMDAC_CONFIG_REF1P0_H +#define TIMDAC_CONFIG_REF1P0_H + +// ---------------------------------------------------------------------------- +// Helper macros + +// Convert n*R*C into a number of timer counts +#ifdef __GNUC__ +#define TIMDAC_NRC(n, r, c) ({ \ + _Static_assert((n) * (r) * (c) * TIMDAC_TIMERFREQ_HZ <= 65535, \ + "Time constant too high for 16-bit timer"); \ + (unsigned)(((n) * (r) * (c) * TIMDAC_TIMERFREQ_HZ)) }) +#else +#define TIMDAC_NRC(n, r, c) ((unsigned)((n) * (r) * (c) * TIMDAC_TIMERFREQ_HZ)) +#endif + +// ---------------------------------------------------------------------------- +// Circuit properties + +#define TIMDAC_INT_CAP 10e-9 +#define TIMDAC_OUT_CAP 100e-9 +#define TIMDAC_DISCH_RES 1e+3 +#define TIMDAC_OUT_RES 1e+3 + +// ---------------------------------------------------------------------------- +// TIMDAC control parameters + +// Numerator of the tuning weight. This weight determines how quickly the DAC +// tuning is pulled towards the result of each tuning operation; adjustments +// are made by a factor of (TIMDAC_TUNE_WEIGHT / 65536). This parameter will +// determine how much tuning noise bleeds through into the output; the RMS +// noise can be measured using the TIMDAC_DIAGNOSTIC option, but should also +// be validated using a precise multimeter. +#define TIMDAC_TUNE_WEIGHT 64 + +// Tuning noise threshold. Above this amount of error in timer counts, the +// tuning is assumed to have drifted suddenly, and is immediately clobbered. +// Because the tuning algorithm can be noisy, this must be high enough to avoid +// spurious clobbers which can result in sudden shifts in output value. The +// clobber count can be measured using the TIMDAC_DIAGNOSTIC option. +#define TIMDAC_TUNE_NTHRESH 2048 + +// How many channels may be scanned before tuning. Beware that changing this +// will have ramifications for TIMDAC_TUNE_WEIGHT and TIMDAC_TUNE_NTHRESH. +#define TIMDAC_TUNE_INTERVAL 64 + +// Integrator discharge time. It is essential that all charge on the integrator +// be fully discharged before the next channel is emitted. This parameter can +// be validated by setting all channels to zero and measuring the output, then +// setting all channels except one to full-scale and ensuring that the channel +// that remains set to zero has not shifted. +// +// REV 1.1 TODO: Rev 1.1 is changing the recommended integrating cap type; +// recheck this afterward. It can probably go down. +#define TIMDAC_DISCHARGE_TIME TIMDAC_NRC(12, TIMDAC_DISCH_RES, TIMDAC_INT_CAP) + +// Stabilization time. This is the time after the pulse finishes before we +// transfer the final ramp value to the holding capacitor. This time allows the +// integrator to settle. If too short, transient edge behavior could create +// offsets or ripple in the output. If too long, the integrator will integrate +// leakage current and create an offset error. +#define TIMDAC_STABILIZATION_TIME TIMDAC_NRC(50e-6, 1, 1) + +// Fast transfer time in timer counts. This is the time for which the +// integrated ramp is sent to the output when doing fast scanning (no output +// has been changed significantly). It only needs to be fast enough to "top up" +// the output capacitor to account for any leakage that may have occurred +// during a cycle. +#define TIMDAC_FAST_TRANSFER_TIME TIMDAC_NRC(2, TIMDAC_OUT_RES, TIMDAC_OUT_CAP) + +// Slow transfer time in timer counts. This is the time for which the +// integrated ramp is sent to the output when performing an output value +// change. +// +// REV 1.1 TODO: This is currently set to the maximum because dielectric +// absorption in the rev 1.0 design is extending the time necessary to set the +// output voltage. After changing to lower absorption capacitors, recheck. +#define TIMDAC_SLOW_TRANSFER_TIME 65535 + +// How much can an output change without triggering a slow transfer cycle. +#define TIMDAC_SLOW_DELTA 128 + +#endif // !defined(TIMDAC_CONFIG_REF1P0_H) diff --git a/src/timdac_hw_ch32v103.c b/src/timdac_hw_ch32v103.c new file mode 100644 index 0000000..f818bd9 --- /dev/null +++ b/src/timdac_hw_ch32v103.c @@ -0,0 +1,282 @@ +// intellectual property is bullshit bgdc + +// ============================================================================ +// TIMDAC HARDWARE DRIVER -- WCH CH32V103 +// ---------------------------------------------------------------------------- +// This driver implements TIMDAC for the WCH CH32V103 microcontroller. It +// requires the following: +// +// - Core clock = 72 MHz +// - One general-purpose timer module +// - The standard TIMDAC GPIOs: +// - 1 output compare pin +// - 1 mux inhibit pin +// - 1 discharge pin +// - 0 or 1 polarity pins +// - From 0 to 8 channel select pins +// +// To use it, include this source file in your build, and define the following +// in timdac_config.h: +// +// #define TIMDAC_HW_TIMER +// Name of the timer module (e.g. TIM3) +// +// #define TIMDAC_HW_TIMER_CHANNEL +// Number of the output compare channel, from 1 to 4 +// +// #define TIMDAC_HW_GPIO_TIMER +// GPIO and pin corresponding to the output compare channel +// (e.g. GPIOC, GPIO_Pin_6) +// +// #define TIMDAC_HW_GPIO_MUXINHIBIT +// GPIO and pin corresponding to the mux inhibit signal +// +// #define TIMDAC_HW_GPIO_DISCHARGE +// GPIO and pin corresponding to the discharge signal +// +// #define TIMDAC_HW_GPIO_POLARITY +// OPTIONAL, GPIO and pin corresponding to the polarity signal, where High +// means Negative. If not using polarity, do not define this. +// +// #define TIMDAC_HW_GPIO_TUNE +// GPIO and pin corresponding to the tuning comparator output signal. +// +// #define TIMDAC_HW_GPIO_TUNE_POL +// Polarity of the tuning comparator; false = inverting with respect to the +// positive-going ramp. Note that the reference design uses inverting +// polarity, so this should generally be false. +// +// #define TIMDAC_HW_GPIO_CHAN_PORT +// GPIO port where the channel select pins live. If there is only one +// channel, this may be left undefined. If this is undefined, +// TIMDAC_HW_GPIO_CHAN_PINS must also be undefined. +// (e.g. GPIOC) +// +// #define TIMDAC_HW_GPIO_CHAN_PINS +// List of channel select pins, least significant bit first, up to eight. +// If there is only one channel, this may be left undefined. If this is +// undefined, TIMDAC_HW_GPIO_CHAN must also be undefined. Note that if +// a channel number is requested that exceeds pow2(CHAN_PINS)-1, the +// channel number will be truncated. +// (e.g. GPIO_Pin_0, GPIO_Pin_1, GPIO_Pin_2) +// ============================================================================ + + +#include "timdac_ll.h" +#include "timdac_config.h" + +#include +#include +#include + +#include +#include + +#if defined(TIMDAC_HW_GPIO_CHAN_PORT) && !defined(TIMDAC_HW_GPIO_CHAN_PINS) +# error "timdac_hw: channel select port defined, but no pins" +#elif defined(TIMDAC_HW_GPIO_CHAN_PINS) && !defined(TIMDAC_HW_GPIO_CHAN_PORT) +# error "timdac_hw: channel select pins defined, but no port" +#elif defined(TIMDAC_HW_GPIO_CHAN_PORT) +static const uint16_t _chansel[8] = {TIMDAC_HW_GPIO_CHAN_PINS}; +#define N_CHANSEL (sizeof(_chansel)/sizeof(_chansel[0])) +#endif + +static uint16_t _ntim_to_channel(uint16_t ntim); +static void _init_gpio( + GPIO_TypeDef * gpio, + uint16_t pin, + GPIOMode_TypeDef mode, + bool fast, + bool value +); + +void timdac_hw_init(void) +{ + _init_gpio(TIMDAC_HW_GPIO_MUXINHIBIT, GPIO_Mode_Out_PP, false, true); + _init_gpio(TIMDAC_HW_GPIO_DISCHARGE, GPIO_Mode_Out_PP, false, true); +#ifdef TIMDAC_HW_GPIO_POLARITY + _init_gpio(TIMDAC_HW_GPIO_POLARITY, GPIO_Mode_Out_PP, false, false); +#endif + +#ifdef TIMDAC_HW_GPIO_CHAN_PORT + for (size_t i = 0; i < N_CHANSEL; i++) + { + _init_gpio( + TIMDAC_HW_GPIO_CHAN_PORT, + _chansel[i], + GPIO_Mode_Out_PP, + false, + false + ); + } +#endif + + _init_gpio(TIMDAC_HW_GPIO_TUNE, GPIO_Mode_IPU, false, false); + _init_gpio(TIMDAC_HW_GPIO_TIMER, GPIO_Mode_AF_PP, true, false); + + TIM_TimeBaseInitTypeDef tbi = { + .TIM_Prescaler = 0, + .TIM_CounterMode = TIM_CounterMode_Up, + .TIM_Period = TIMDAC_DISCHARGE_TIME, + .TIM_ClockDivision = TIM_CKD_DIV1, + .TIM_RepetitionCounter = 0x0000, + }; + + TIM_TimeBaseInit(TIMDAC_HW_TIMER, &tbi); + + TIM_OCInitTypeDef oci = { + .TIM_OCMode = TIM_OCMode_PWM2, + .TIM_OutputState = TIM_OutputState_Enable, + .TIM_OutputNState = TIM_OutputNState_Disable, + .TIM_Pulse = 0, + .TIM_OCPolarity = TIM_OCPolarity_High, + .TIM_OCNPolarity = TIM_OCPolarity_High, + .TIM_OCIdleState = TIM_OCIdleState_Reset, // TIM1/8 only + .TIM_OCNIdleState = TIM_OCIdleState_Reset, // TIM1/8 only + }; + + switch (TIMDAC_HW_TIMER_CHANNEL) { + case 1: + TIM_OC1Init(TIMDAC_HW_TIMER, &oci); + TIM_OC1PreloadConfig(TIMDAC_HW_TIMER, TIM_OCPreload_Disable); + break; + case 2: + TIM_OC2Init(TIMDAC_HW_TIMER, &oci); + TIM_OC2PreloadConfig(TIMDAC_HW_TIMER, TIM_OCPreload_Disable); + break; + case 3: + TIM_OC3Init(TIMDAC_HW_TIMER, &oci); + TIM_OC3PreloadConfig(TIMDAC_HW_TIMER, TIM_OCPreload_Disable); + break; + case 4: + TIM_OC4Init(TIMDAC_HW_TIMER, &oci); + TIM_OC4PreloadConfig(TIMDAC_HW_TIMER, TIM_OCPreload_Disable); + break; + } + + TIM_SelectOnePulseMode(TIMDAC_HW_TIMER, TIM_OPMode_Single); + TIM_CtrlPWMOutputs(TIMDAC_HW_TIMER, ENABLE); + +#ifdef TIMDAC_HW_TIMER_IRQn + NVIC_InitTypeDef tim_it_init = { + .NVIC_IRQChannel = TIMDAC_HW_TIMER_IRQn, + .NVIC_IRQChannelPreemptionPriority = TIMDAC_HW_TIMER_IRQPRIO, + .NVIC_IRQChannelSubPriority = TIMDAC_HW_TIMER_IRQSUBPRIO, + .NVIC_IRQChannelCmd = ENABLE, + }; + NVIC_Init(&tim_it_init); + TIM_ITConfig(TIMDAC_HW_TIMER, TIM_IT_Update, ENABLE); +#endif +} + +void timdac_hw_timer_set_period(uint16_t counts) +{ + TIM_SetAutoreload(TIMDAC_HW_TIMER, counts); +} + +void timdac_hw_timer_set_compare(uint16_t counts) +{ + // Our timer mode results in a pulse that is LOW for `counts`, so + // invert the duration: + uint16_t value = UINT16_MAX - counts; + + switch (TIMDAC_HW_TIMER_CHANNEL) { + case 1: + TIM_SetCompare1(TIMDAC_HW_TIMER, value); + break; + case 2: + TIM_SetCompare2(TIMDAC_HW_TIMER, value); + break; + case 3: + TIM_SetCompare3(TIMDAC_HW_TIMER, value); + break; + case 4: + TIM_SetCompare4(TIMDAC_HW_TIMER, value); + break; + } +} + +void timdac_hw_timer_set_oc_enabled(bool en) +{ + TIM_CCxCmd(TIMDAC_HW_TIMER, _ntim_to_channel(TIMDAC_HW_TIMER_CHANNEL), + en ? TIM_CCx_Enable : TIM_CCx_Disable); +} + +void timdac_hw_timer_start(void) +{ + TIM_Cmd(TIMDAC_HW_TIMER, ENABLE); +} + +bool timdac_hw_timer_running(void) +{ + return TIMDAC_HW_TIMER->CTLR1 & TIM_CEN; +} + +void timdac_hw_gpio_muxinhibit(bool en) +{ + GPIO_WriteBit(TIMDAC_HW_GPIO_MUXINHIBIT, en ? Bit_SET : Bit_RESET); +} + +void timdac_hw_gpio_discharge(bool en) +{ + GPIO_WriteBit(TIMDAC_HW_GPIO_DISCHARGE, en ? Bit_SET : Bit_RESET); +} + +void timdac_hw_gpio_polarity(bool pos) +{ +#ifdef TIMDAC_HW_GPIO_POLARITY + GPIO_WriteBit(TIMDAC_HW_GPIO_POLARITY, !pos ? Bit_SET : Bit_RESET); +#endif +} + +void timdac_hw_gpio_select_channel(uint8_t chan) +{ +#ifdef TIMDAC_HW_GPIO_CHAN_PORT + uint16_t chan_mask = 0, chan_bits = 0; + + for (uint8_t i = 0; i < N_CHANSEL; i++, chan >>= 1) + { + if (!_chansel[i]) break; + uint16_t bit = _chansel[i]; + chan_mask |= bit; + if (chan & 1) chan_bits |= bit; + } + + GPIO_ResetBits(TIMDAC_HW_GPIO_CHAN_PORT, chan_mask); + GPIO_SetBits(TIMDAC_HW_GPIO_CHAN_PORT, chan_bits); +#else + (void) chan; +#endif +} + +bool timdac_hw_gpio_tune_is_high(void) +{ + return GPIO_ReadInputDataBit(TIMDAC_HW_GPIO_TUNE) + ^ !TIMDAC_HW_GPIO_TUNE_POL; +} + +static uint16_t _ntim_to_channel(uint16_t ntim) +{ + return (ntim - 1) << 2; +} + +static void _init_gpio( + GPIO_TypeDef * gpio, + uint16_t pin, + GPIOMode_TypeDef mode, + bool fast, + bool value +) +{ + if (mode == GPIO_Mode_Out_OD || mode == GPIO_Mode_Out_PP) + GPIO_WriteBit(gpio, pin, value); + + GPIO_InitTypeDef init = { + .GPIO_Pin = pin, + .GPIO_Speed = fast ? GPIO_Speed_50MHz : GPIO_Speed_2MHz, + .GPIO_Mode = mode, + }; + + GPIO_Init(gpio, &init); +} + diff --git a/src/timdac_ll.c b/src/timdac_ll.c new file mode 100644 index 0000000..a113170 --- /dev/null +++ b/src/timdac_ll.c @@ -0,0 +1,290 @@ +// intellectual property is bullshit bgdc + +#include "timdac_ll.h" +#include "timdac_config.h" + +#include "light_output.h" + +#include +#include +#include + +// State definitions +enum state { + STATE_IDLE, + STATE_DISCHARGE, + STATE_PULSE, + STATE_STABILIZE, + STATE_TRANSFER, +}; + +#define CHAN_TUNE UINT8_MAX + +#define VALUE_FLAG_NEG 0x10000000u +#define VALUE_MASK 0x0000FFFFu + +// Compute the tuned compare value for a given DAC code +static uint16_t _compare_for_value(timdac_ll_t * td, uint16_t value); + +// Start the external hardware cycle for the current state +static void _select(timdac_ll_t * td); + +// Perform one step of the tuning SAR binary search +static void _sar_search(timdac_ll_t * td); + +#define NTIM_VALID(ntim) ((ntim) >= 1 && (ntim) <= 4) + +void timdac_ll_init(timdac_ll_t * td) +{ + td->chan = 0; + td->state = STATE_IDLE; + td->tune = 0; + td->slow = false; + td->tune_sar_top = UINT16_MAX; + td->tune_sar_bot = 0; + + if (td->tunediag) + { + td->tunediag->cycles = 0; + td->tunediag->clobbers = 0; + td->tunediag->noise = 0; + td->tunediag->tune = 0; + } + + timdac_hw_init(); +} + +bool timdac_ll_tune(timdac_ll_t * td) +{ + if (td->tune_sar_top == 0 || td->tune_sar_bot == UINT16_MAX) + { + // Start fresh. + td->tune_sar_top = UINT16_MAX; + td->tune_sar_bot = 0; + return false; + } + else if (td->tune_sar_top != td->tune_sar_bot) + { + // A SAR tuning is starting or in progress. + + uint16_t guess = + ((uint32_t) td->tune_sar_top + + (uint32_t) td->tune_sar_bot) / 2; + + timdac_hw_timer_set_compare(guess); + td->chan = CHAN_TUNE; + td->state = STATE_DISCHARGE; + _select(td); + return true; + } + else + { + // Done tuning, enter the new value + uint16_t tunevalue = td->tune_sar_top; + + int32_t err = tunevalue * 65536L - td->tune; + + bool clobber = + err > (TIMDAC_TUNE_NTHRESH * 65536L) + || err < (TIMDAC_TUNE_NTHRESH * -65536L); + + if (clobber) + { + td->tune = tunevalue * 65536UL; + } + else + { + err *= TIMDAC_TUNE_WEIGHT; + err /= UINT16_MAX; + + td->tune += err; + } + + if (td->tunediag) + { + td->tunediag->tune = td->tune >> 16; + td->tunediag->cycles++; + if (td->tunediag->cycles == 0) + td->tunediag->noise = 0; + + uint64_t noise = td->tunediag->noise + err * err; + if (noise < td->tunediag->noise && !clobber) + { + // Overflow + td->tunediag->cycles = 1; + td->tunediag->noise = err * err; + } + else if (!clobber) + td->tunediag->noise = noise; + else + td->tunediag->clobbers++; + } + + td->tune_sar_top = UINT16_MAX; + td->tune_sar_bot = 0; + return false; + } +} + +void timdac_ll_emit(timdac_ll_t * td, uint8_t chan, int16_t value, bool slow) +{ + uint16_t const value_pos = + value >= 0 ? value : + value == INT16_MIN ? INT16_MAX : -value; + + uint16_t const comp = _compare_for_value(td, value_pos); + + timdac_hw_gpio_polarity(value >= 0); + timdac_hw_timer_set_compare(comp); + + td->chan = chan; + td->state = STATE_DISCHARGE; + td->slow = slow; + + _select(td); +} + +bool timdac_ll_poll(timdac_ll_t * td) +{ + if (timdac_hw_timer_running()) + return true; + + switch (td->state) + { + default: + // fall through + case STATE_IDLE: + td->state = STATE_IDLE; + break; + + case STATE_DISCHARGE: + // We just finished discharging the cap. Now, output the pulse. + td->state = STATE_PULSE; + break; + + case STATE_PULSE: + // The ramp is complete; give the integrator some time to settle + td->state = STATE_STABILIZE; + break; + + case STATE_STABILIZE: + // The ramp has now reached its final value. Transfer it to + // the holding capacitor or save a tuning. + if (td->chan == CHAN_TUNE) + { + td->state = STATE_IDLE; + _sar_search(td); + } + else + { + td->state = STATE_TRANSFER; + } + break; + + case STATE_TRANSFER: + // Done! + td->state = STATE_IDLE; + break; + } + + _select(td); + return td->state != STATE_IDLE; +} + +void timdac_ll_idle(timdac_ll_t * td) +{ + if (td->state != STATE_IDLE) + return; + + _select(td); + timdac_hw_timer_set_period(100); + timdac_hw_timer_start(); +} + +static uint16_t _compare_for_value(timdac_ll_t * td, uint16_t value) +{ + // Scale value according to tuning, with rounding. + return (value * (td->tune / UINT16_MAX) + 16384) / 32768; +} + +static void _select(timdac_ll_t * td) +{ + bool inhibit = false; + bool discharge = false; + uint16_t period = UINT16_MAX; + + uint8_t state = td->state; + + switch (state) + { + case STATE_DISCHARGE: + period = TIMDAC_DISCHARGE_TIME; + // fall through + case STATE_IDLE: + inhibit = true; + discharge = true; + break; + + case STATE_STABILIZE: + period = TIMDAC_STABILIZATION_TIME; + // fall through + case STATE_PULSE: + inhibit = true; + discharge = false; + break; + + case STATE_TRANSFER: + period = td->slow + ? TIMDAC_SLOW_TRANSFER_TIME + : TIMDAC_FAST_TRANSFER_TIME; + inhibit = false; + discharge = false; + break; + } + + // Perform channel switching + if (inhibit) timdac_hw_gpio_muxinhibit(true); + if (state == STATE_DISCHARGE) timdac_hw_gpio_select_channel(td->chan); + if (!inhibit) timdac_hw_gpio_muxinhibit(false); + timdac_hw_gpio_discharge(discharge); + + // Timer is not going to run now if we're in idle. + if (state == STATE_IDLE) + return; + + // Set up the timer and go! + timdac_hw_timer_set_oc_enabled(state == STATE_PULSE); + timdac_hw_timer_set_period(period); + timdac_hw_timer_start(); +} + +static void _sar_search(timdac_ll_t * td) +{ + bool tune_high = timdac_hw_gpio_tune_is_high(); + + uint16_t guess = + ((uint32_t) td->tune_sar_top + + (uint32_t) td->tune_sar_bot) / 2; + + if (tune_high) + { + if (td->tune_sar_top == guess) + td->tune_sar_bot = guess; + else + td->tune_sar_top = guess; + } + else + { + if (td->tune_sar_bot == guess) + td->tune_sar_top = guess; + else + td->tune_sar_bot = guess; + } + + if (td->tune_sar_bot > td->tune_sar_top) + { + uint16_t temp = td->tune_sar_bot; + td->tune_sar_bot = td->tune_sar_top; + td->tune_sar_top = temp; + } +} diff --git a/src/timdac_ll.h b/src/timdac_ll.h new file mode 100644 index 0000000..0fd1eef --- /dev/null +++ b/src/timdac_ll.h @@ -0,0 +1,131 @@ +// intellectual property is bullshit bgdc + +#ifndef TIMDAC_LL_H +#define TIMDAC_LL_H + +// ============================================================================ +// TIMDAC LOW LEVEL DRIVER +// ---------------------------------------------------------------------------- +// This module provides the low-level (but still hardware-abstracted) part of +// TIMDAC. It implements the state machine that provides each individual cycle +// of operation, but must be sequenced through cycles correctly. You are not +// generally meant to use it directly; see the high-level interface in timdac.h +// which handles this sequencing. +// ============================================================================ + +#include +#include +#include +#include +#include + +#define TIMDAC_MAX_SELCHAN 5 + +typedef struct timdac_tunediag_s { + // Total number of calibration cycles run (will overflow) + uint16_t cycles; + + // Tuning clobber counter. + uint16_t clobbers; + + // Current tune value. + uint16_t tune; + + // Tuning noise accumulator. This will hold the sum of the squares of + // the tuning errors, and will be reset to zero each time cycles wraps + // around. To get the RMS tuning noise, divide this by cycles, take + // the square root, then divide by 65536. + uint64_t noise; +} timdac_tunediag_t; + +typedef struct timdac_ll_s { + // #### BEGIN - USER MUST INITIALIZE #### + // ---- Diagnostics ---- + + // Optional calibration diagnostics struct + timdac_tunediag_t * tunediag; + + // #### END - USER MUST INITIALIZE #### + + // Tuning value. This is 65536 times the timer setting required to + // produce Vref. May be read but should not be written; if zero, the + // first tuning has not completed yet. + uint32_t tune; + + // Tuning successive approximation upper bound + uint16_t tune_sar_top; + + // Tuning successive approximation lower bound + uint16_t tune_sar_bot; + + // Current channel number + uint8_t chan; + + // Current state + uint8_t state; + + // Whether performing a slow update + bool slow; +} timdac_ll_t; + +// Initialize the DAC. The mux GPIOs will be initialized as well, but you are +// responsible for configuring the timer channel IOs as well as enabling the +// clock to the GPIO and timer. +// +// td: timdac instance +void timdac_ll_init(timdac_ll_t * td); + +// Start to tune the DAC. After calling, run timdac_poll() until it +// reports idle. +// +// Tuning requires external hardware support. A comparator must sense +// when the ramp crosses Vref and signal back to us on the input capture line. +// Note that the CH32V10x has no internal comparator; an external one will be +// needed. Do not use hysteresis as this will make the level less exact. +// +// Returns whether the timer is now busy. +bool timdac_ll_tune(timdac_ll_t * td); + +// Start to emit a voltage on a channel. The integrating ramp will be started. +// After calling, run timdac_poll() until it reports idle. +// +// If timdac_poll() was not already reporting idle, the previous cycle will be +// aborted. +// +// td: timdac instance +// chan: channel number, sent to the channel select GPIOs. Remember that 0 +// is the discharge channel - you should never use it directly. Behavior +// is undefined if you do. +// value: DAC code +// slow: if true, dwell longer during transfer for large updates +void timdac_ll_emit(timdac_ll_t * td, uint8_t chan, int16_t value, bool slow); + +// Poll the timer. This must be called repeatedly while a tuning or output +// cycle is in progress - it does not need to be called with perfect regularity, +// but waiting too long may result in some signal droop and loss of accuracy. +// +// td: timdac instance +// return: true if busy, false if idle +bool timdac_ll_poll(timdac_ll_t * td); + +// Run a single idle cycle. This can be used to kickstart an interrupt driven +// timdac. If the timdac is not already at the idle state, does nothing. +void timdac_ll_idle(timdac_ll_t * td); + +// ---------------------------------------------------------------------------- +// HARDWARE DRIVER INTERFACE +// These functions are implemented by each hardware driver. + +void timdac_hw_init(void); +void timdac_hw_timer_set_period(uint16_t counts); +void timdac_hw_timer_set_compare(uint16_t counts); +void timdac_hw_timer_set_oc_enabled(bool en); +void timdac_hw_timer_start(void); +bool timdac_hw_timer_running(void); +void timdac_hw_gpio_muxinhibit(bool en); +void timdac_hw_gpio_discharge(bool en); +void timdac_hw_gpio_polarity(bool pos); +void timdac_hw_gpio_select_channel(uint8_t chan); +bool timdac_hw_gpio_tune_is_high(void); + +#endif // !defined(TIMDAC_LL_H)