333 lines
6.6 KiB
C
333 lines
6.6 KiB
C
// intellectual property is bullshit bgdc
|
|
|
|
#include "timdac_ll.h"
|
|
#include "timdac_config.h"
|
|
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
#include <inttypes.h>
|
|
#include <stdatomic.h>
|
|
|
|
// 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
|
|
|
|
static uint32_t _tune;
|
|
static uint16_t _tune_sar_top = UINT16_MAX;
|
|
static uint16_t _tune_sar_bot;
|
|
static uint8_t _chan;
|
|
static uint8_t _state;
|
|
static bool _slow;
|
|
|
|
#if TIMDAC_DIAGNOSTIC
|
|
static atomic_uint _diag_count;
|
|
static timdac_lldiag_t _diag;
|
|
#endif
|
|
|
|
// Compute the tuned compare value for a given DAC code
|
|
static uint16_t _compare_for_value(uint16_t value);
|
|
|
|
// Start the external hardware cycle for the current state. Note the timer
|
|
// behavior: in STATE_IDLE, _select() will never start the timer; in other
|
|
// states, it will always start the timer.
|
|
static void _select(void);
|
|
|
|
// Perform one step of the tuning SAR binary search by moving _tune_sar_top
|
|
// and _tune_sar_bot as appropriate. This function does not perform the
|
|
// computation to adjust _tune afterward; that is handled by timdac_ll_tune()
|
|
// to keep the more long-winded arithmetic out of ISR context.
|
|
static void _sar_search(void);
|
|
|
|
void timdac_ll_init(void)
|
|
{
|
|
_chan = 0;
|
|
_state = STATE_IDLE;
|
|
_tune = 0;
|
|
_slow = false;
|
|
_tune_sar_top = UINT16_MAX;
|
|
_tune_sar_bot = 0;
|
|
|
|
#if TIMDAC_DIAGNOSTIC
|
|
_diag.cycles = 0;
|
|
_diag.clobbers = 0;
|
|
_diag.noise = 0;
|
|
_diag.tune = 0;
|
|
#endif
|
|
|
|
timdac_hw_init();
|
|
}
|
|
|
|
bool timdac_ll_tune(void)
|
|
{
|
|
if (_tune_sar_top < _tune_sar_bot)
|
|
{
|
|
// Start fresh.
|
|
_tune_sar_top = UINT16_MAX;
|
|
_tune_sar_bot = 0;
|
|
return false;
|
|
}
|
|
else if (_tune_sar_top != _tune_sar_bot)
|
|
{
|
|
// A SAR tuning is starting or in progress.
|
|
|
|
uint16_t guess =
|
|
((uint32_t) _tune_sar_top
|
|
+ (uint32_t) _tune_sar_bot) / 2;
|
|
|
|
timdac_hw_timer_set_compare(guess);
|
|
_chan = CHAN_TUNE;
|
|
_state = STATE_DISCHARGE;
|
|
_select();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Done tuning, enter the new value
|
|
uint16_t tunevalue = _tune_sar_top;
|
|
|
|
int32_t err = tunevalue * 65536L - _tune;
|
|
|
|
bool clobber =
|
|
err > (TIMDAC_TUNE_NTHRESH * 65536L)
|
|
|| err < (TIMDAC_TUNE_NTHRESH * -65536L);
|
|
|
|
if (clobber)
|
|
{
|
|
_tune = tunevalue * 65536UL;
|
|
}
|
|
else
|
|
{
|
|
err *= TIMDAC_TUNE_WEIGHT;
|
|
err /= UINT16_MAX;
|
|
|
|
_tune += err;
|
|
}
|
|
|
|
#if TIMDAC_DIAGNOSTIC
|
|
_diag.tune = _tune >> 16;
|
|
_diag.cycles++;
|
|
if (_diag.cycles == 0)
|
|
_diag.noise = 0;
|
|
|
|
uint64_t noise = _diag.noise + err * err;
|
|
if (noise < _diag.noise && !clobber)
|
|
{
|
|
// Overflow
|
|
_diag.cycles = 1;
|
|
_diag.noise = err * err;
|
|
}
|
|
else if (!clobber)
|
|
_diag.noise = noise;
|
|
else
|
|
_diag.clobbers++;
|
|
|
|
atomic_fetch_add(&_diag_count, 1);
|
|
#endif
|
|
|
|
_tune_sar_top = UINT16_MAX;
|
|
_tune_sar_bot = 0;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void timdac_ll_emit(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(value_pos);
|
|
|
|
timdac_hw_gpio_polarity(value >= 0);
|
|
timdac_hw_timer_set_compare(comp);
|
|
|
|
_chan = chan;
|
|
_state = STATE_DISCHARGE;
|
|
_slow = slow;
|
|
|
|
_select();
|
|
}
|
|
|
|
bool timdac_ll_poll(void)
|
|
{
|
|
if (timdac_hw_timer_running())
|
|
return true;
|
|
|
|
switch (_state)
|
|
{
|
|
default:
|
|
// fall through
|
|
case STATE_IDLE:
|
|
_state = STATE_IDLE;
|
|
break;
|
|
|
|
case STATE_DISCHARGE:
|
|
// We just finished discharging the cap. Now, output the pulse.
|
|
_state = STATE_PULSE;
|
|
break;
|
|
|
|
case STATE_PULSE:
|
|
// The ramp is complete; give the integrator some time to settle
|
|
_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 (_chan == CHAN_TUNE)
|
|
{
|
|
_state = STATE_IDLE;
|
|
_sar_search();
|
|
}
|
|
else
|
|
{
|
|
_state = STATE_TRANSFER;
|
|
}
|
|
break;
|
|
|
|
case STATE_TRANSFER:
|
|
// Done!
|
|
_state = STATE_IDLE;
|
|
break;
|
|
}
|
|
|
|
_select();
|
|
return _state != STATE_IDLE;
|
|
}
|
|
|
|
void timdac_ll_idle(void)
|
|
{
|
|
if (_state != STATE_IDLE)
|
|
return;
|
|
|
|
_select();
|
|
timdac_hw_timer_set_period(100);
|
|
timdac_hw_timer_start();
|
|
}
|
|
|
|
bool timdac_ll_tuned(void)
|
|
{
|
|
return _tune != 0;
|
|
}
|
|
|
|
void timdac_ll_diagnostic(timdac_lldiag_t * diag)
|
|
{
|
|
#if TIMDAC_DIAGNOSTIC
|
|
// The diagnostic struct is only updated at about 760 Hz, so most of
|
|
// the time we'll get it just fine. Don't bother locking, just check a
|
|
// counter that increments on each update to ensure it was not changed.
|
|
unsigned count;
|
|
|
|
do {
|
|
count = atomic_load(&_diag_count);
|
|
memcpy(diag, &_diag, sizeof(timdac_lldiag_t));
|
|
} while (count != atomic_load(&_diag_count));
|
|
#else
|
|
memset(diag, 0, sizeof(timdac_lldiag_t));
|
|
#endif
|
|
}
|
|
|
|
static uint16_t _compare_for_value(uint16_t value)
|
|
{
|
|
// Scale value according to tuning, with rounding.
|
|
//
|
|
// WARNING: This can affect DNL! The rounding method used can shift
|
|
// the DNL plot up or down slightly around zero, which changes the
|
|
// maximum value. This method centers it the best for lowest DNL. Do
|
|
// not change without testing!
|
|
return (value * (_tune / UINT16_MAX) + 16384) / 32768;
|
|
}
|
|
|
|
static void _select(void)
|
|
{
|
|
bool inhibit = true;
|
|
bool discharge = false;
|
|
uint16_t period = UINT16_MAX;
|
|
|
|
uint8_t state = _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 = _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(_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(void)
|
|
{
|
|
bool tune_high = timdac_hw_gpio_tune_is_high();
|
|
|
|
uint16_t guess =
|
|
((uint32_t) _tune_sar_top
|
|
+ (uint32_t) _tune_sar_bot) / 2;
|
|
|
|
if (tune_high)
|
|
{
|
|
if (_tune_sar_top == guess)
|
|
_tune_sar_bot = guess;
|
|
else
|
|
_tune_sar_top = guess;
|
|
}
|
|
else
|
|
{
|
|
if (_tune_sar_bot == guess)
|
|
_tune_sar_top = guess;
|
|
else
|
|
_tune_sar_bot = guess;
|
|
}
|
|
|
|
if (_tune_sar_bot > _tune_sar_top)
|
|
{
|
|
uint16_t temp = _tune_sar_bot;
|
|
_tune_sar_bot = _tune_sar_top;
|
|
_tune_sar_top = temp;
|
|
}
|
|
}
|