Files
nixie-driver/firmware/nixitest1/nixitest1.c
2023-04-25 05:54:16 -04:00

604 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// cnixi - ch32v003 base driver for IN-15 nixie tubes.
//
// Fully integrated PI flyback PSU controller + Nixie Tube output
// control with up to 2 channel simultaneous dimming.
//
// Copyright 2023 <>< Charles Lohr, under the MIT-X11 license or NewBSD
// license, you choose.
//
// Note: Before running this, you should run the optionbytes folder script
// in order configure RESET as a GPIO so you can use the AUX input as well
// as forcing the watchdog on by default.
//
#define SYSTEM_CORE_CLOCK 48000000
#include "ch32v003fun.h"
#include <stdio.h>
static uint16_t GenOnMask( int segmenton );
static void ApplyOnMask( uint16_t onmask );
static inline void WatchdogPet();
// Limits the "ADC Set Value" in volts.
// This prevents us from exceeding 190 volts target.
#define ABSOLUTE_MAX_ADC_SET 190
// Do not mess with PWM_ values unless you know what you are willing to go down
// a very deep rabbit hole. I experimentally determined 140 for this particular
// system was on the more efficient side of things and gave good dynamic range.
// 140 means it's 48MHz / 140 = 342 kHz for the main flyback frequency.
//
// You can explore values by doing ENABLE_TUNING in here and in testnix.c
// #define ENABLE_TUNING
#ifndef ENABLE_TUNING
#define PWM_PERIOD 140
#else
int PWM_PERIOD = 140;
#endif
int pwm_max_duty = 48; //This is changed based on vdd.
// Flyback PID loop Tuning Parameters
//
// This is a PID loop (you should read about this separately). But these terms
// are actually on the order of 2^term. So we are only able to get the tuning
// to a rough ballpark. Thankfully, PID loops are forgiving.
//
// Honestly, this is MUCH more sophisticated than it needs to be! I was using
// a P-only loop for quite some time without any issues.
#define ERROR_P_TERM 2 // Actually 2^2
#define ERROR_D_TERM -1 // Actually 2^0
#define I_SAT_MAX (4096+2048) // SAT * 2^(-ADC_IIR+ERROR_I_TERM) = Max impact to PWM
#define I_SAT_MIN (-4096)
#define ERROR_I_TERM -5 // Actually 2^-6
// We filter our ADC inputs because they are kind of noisy.
//
// We can use Binary-shift IIR filters to filter the incoming ADC signals.
// See later in the code, but, it maps to only about 4 assembly instructions!
// (plus a read-back of the previous value we will be mixing).
#define ADC_IIR 2
#define VDD_IIR 2
// Target feedback, set by the user.
int target_feedback = 0;
// Feedback based on what the user set and the part's VDD.
int feedback_vdd = 0;
// Filtered ADC and VDD values.
int lastadc = 0;
int lastrefvdd = 0;
// Code for handling numeric fading, between 2 numbers or alone.
uint16_t fade_time0, fade_time1;
uint16_t fade_disp0, fade_disp1;
static uint32_t HandleFade( uint8_t fadepos ) __attribute__((section(".data")));
static uint32_t HandleFade( uint8_t fadepos )
{
// Digit fade. Use fade_timeX and fade_dispX to handle fade logic.
if( fadepos < fade_time0 )
return fade_disp0;
else if( fadepos < fade_time1 )
return fade_disp1;
else
return 0;
}
static inline uint32_t FastMultiply( uint32_t big_num, uint32_t small_num );
static inline uint32_t FastMultiply( uint32_t big_num, uint32_t small_num )
{
// The CH32V003 is an EC core, so no hardware multiply. GCC's way multiply
// is slow, so I wrote this.
//
// This basically does this:
// return small_num * big_num;
//
// Note: This does NOT check for zero to begin with, though this still
// produces the correct results, it is a little weird that even if
// small_num is zero it executes once.
//
// Additionally note, instead of the if( m&1 ) you can do the following:
// ret += multiplciant & neg(multiplicand & 1).
//
// BUT! Shockingly! That is slower than an extra branch! The CH32V003
// can branch unbelievably fast.
//
// This is functionally equivelent and much faster.
//
// Perf numbers, with small_num set to 180V.
// No multiply: 21.3% CPU Usage
// Assembly below: 42.4% CPU Usage (1608 bytes for whole program)
// C version: 41.4% CPU Usage (1600 bytes for whole program)
// Using GCC (__mulsi3) 65.4% CPU Usage (1652 bytes for whole program)
//
// The multiply can be done manually:
uint32_t ret = 0;
uint32_t multiplicand = small_num;
uint32_t mutliplicant = big_num;
do
{
if( multiplicand & 1 )
ret += mutliplicant;
mutliplicant<<=1;
multiplicand>>=1;
} while( multiplicand );
return ret;
}
// FYI You can use functions in ram to make them work faster. The .data
// attribute. This means this function gets placed into RAM. Normally this
// function takes approximately 2.5-3us to execute from flash, but only 2-2.5us
// to execute from RAM.
// This is an interrupt called by an ADC conversion.
void ADC1_IRQHandler(void)
__attribute__((interrupt))
__attribute__((section(".data")));
void ADC1_IRQHandler(void)
{
// If you want to see how long this functon takes to run, you can use a
// scope and then monitor pin D6 if you uncomment this and the bottom copy.
GPIOD->BSHR = 1<<6;
// Acknowledge pending interrupts.
// This will always be ADC_JEOC, so we don't need to check.
ADC1->STATR = 0;
// Acknowledge the interrupt.
// It is crucial that our ADC sample is ALWAYS ALIGNED to the PWM, that way
// any ripple and craziness that happens from the chopping of the flyback
// is completely filtered out because of where we are sampling.
// This performs a low-pass filter on our data, ADC1->RDATAR the sample
// rate, always. As a side note, the value of the IIR (but now it's
// 2^VDD_IIR bigger)
int adcraw = ADC1->RDATAR;
int newadc = adcraw + (lastadc - (lastadc>>ADC_IIR));
lastadc = newadc;
int err = feedback_vdd - lastadc;
static int integral;
static int lasterr;
int derivative = (err - lasterr);
lasterr = err;
integral += err;
// We asymmetrically allow the integral to saturate, to help prevent long-
// term oscillations.
integral = ( integral > ((I_SAT_MAX)<<ADC_IIR) ) ? ((I_SAT_MAX)<<ADC_IIR) : integral;
integral = ( integral < ((I_SAT_MIN)<<ADC_IIR) ) ? ((I_SAT_MIN)<<ADC_IIR) : integral;
// This is the heart of the PID loop.
// General note about shifting: Be sure to combine your shifts.
// If you shift right, then left, you will lose bits of precision.
// These shifts turn into single left and right immediate shifts.
int plant =
(err << ( (-ADC_IIR) + (ERROR_P_TERM) )) +
(integral >> ( ADC_IIR - (ERROR_I_TERM) )) +
(derivative >> ( (ADC_IIR) - (ERROR_D_TERM) ) );
plant = ( plant > pwm_max_duty ) ? pwm_max_duty : plant;
plant = ( plant < 0 ) ? 0 : plant;
TIM1->CH2CVR = plant;
// Use injection channel data to read vref. This is needed because we
// measure all values WRT to VDD and GND. So we need to measure the vref
// a lot to make sure we know what value we are are targeting Ballparks
// (for unfiltered numbers) (Just as a note)
// 0xF0 / 240 for 5V input << lastrefvdd
// 0x175 / 373 for 3.3v input << lastrefvdd
// Do an IIR low-pass filter on VDD. See IIR discussion above.
uint32_t vdd = lastrefvdd = ADC1->IDATAR1 + (lastrefvdd - (lastrefvdd>>VDD_IIR));
#ifndef ENABLE_TUNING
// If we aren't enabling tuning, we can update max on time here. We want to
// limit on-time based on DC voltage on the flyback so that we can get
// close to (But not get into) saturation of the flyback transformer's core
//
// We can compute expected values, but experimenting is better.
// Transformer inductance is ~6uH.
// Our peak current is ~500mA
// The average voltage is ~4V
//
// 4V / .000006H = 0.5A / 666666A/s = 750nS but turns out this was
// pessemistic.
//
// Experimentation showed that the core of the transformer saturates in
// about 1uS at 5V and 1.4uS at 3.3v. More specifically the relationhip
// between our maximum on-time and vref-measured-by-vdd works out to about:
//
// max_on_time_slices = lastrefvdd / 4.44.
//
// There's a neat trick where you can divide by weird decimal divisors by
// adding and subtracing terms. We perform this trick here and below
//
// 1÷(1÷41÷641÷1281÷1024) is about equal to dividing by 4.43290
// It can be simplified it for our purposes as: 1÷(1÷41÷641÷128)
//
// You can arbitrarily add and subtract terms to get as closed to your
// desired target value as possbile.
//
// When we divide a value by powers-of-two, it becomes a bit shift.
//
// The bit shift and IIR adjustments can be made so that the compiler can
// optimize out the addition there.
//
// The following code actually
pwm_max_duty =
(lastrefvdd>>(2+VDD_IIR))
- (lastrefvdd>>(6+VDD_IIR))
- (lastrefvdd>>(7+VDD_IIR));
#endif
// target_feedback is in volts. 0..200 maps to the device voltage.
// lastrefvdd = 0xF0 for 5V input.
// lastrefvdd = 0x175 for 3.3v input.
//
// feedback_vdd = 408 for ~192V @ 5
// feedback_vdd = 680 for ~192V @ 3.3
//
// 408 = 192 * 240 / x = (192*240)/408 = 112.941176471
// 680 = 192 * 373 / x = (373*680)/192 = 105.317647059
//
// More tests showed this value across units is around 120.
//
// X This becomes our denominator.
// feedback_vdd = (current vdd measurement * target voltage) / 120
//
// Further testing identified that the denominator is almost exactly 120.
// We can perform a divison by 120 very quickly by
//
// feedback = numerator/128 + numerator/2048
//
// See note above about the constant division trick.
//
// Side-note:
//
// This is unintuitively slow becuase is because it uses a multiply. The
// CH32V003 does not natively have a multiply instruction, If you use a *
// it calls out to __mulsi3 in libgcc.a.
// The following line of code is still *more* than fast enough but, to
// write it out manually, we can get even faster!
//
// uint32_t numerator = (vdd * target_feedback);
uint32_t numerator = FastMultiply( vdd, target_feedback );
feedback_vdd =
(numerator>>(7+VDD_IIR-ADC_IIR)) +
(numerator>>(11+VDD_IIR-ADC_IIR));
// Pet the watchdog. If we got here, things should be OK.
WatchdogPet();
GPIOD->BSHR = (1<<(16+6));
}
static void SetupTimer1()
{
// Enable Timer 1
RCC->APB2PRSTR |= RCC_APB2Periph_TIM1;
RCC->APB2PRSTR &= ~RCC_APB2Periph_TIM1;
TIM1->PSC = 0x0000; // Prescalar to 0x0000 (so, 48MHz base clock)
TIM1->ATRLR = PWM_PERIOD;
TIM1->CCER = TIM_CC2E | TIM_CC2NP; // CH2 is control for FET.
TIM1->CHCTLR1 = TIM_OC2M_2 | TIM_OC2M_1;
TIM1->CH2CVR = 0; // Actual duty cycle (Off to begin with)
// Setup TRGO for ADC. This makes is to the ADC will trigger on timer
// reset, so we trigger at the same position every time relative to the
// FET turning on.
TIM1->CTLR2 = TIM_MMS_1;
// Enable TIM1 outputs
TIM1->BDTR = TIM_MOE;
TIM1->CTLR1 = TIM_CEN;
}
static void SetupTimer2()
{
// Enable Timer 2
RCC->APB1PRSTR |= RCC_APB1Periph_TIM2;
RCC->APB1PRSTR &= ~RCC_APB1Periph_TIM2;
// PD7 will be used as Timer 2, Channel 4 Configure timer 2 to enable this.
//TIM2->SWEVGR = 0x0001; //TIM_PSCReloadMode_Immediate;
TIM2->PSC = 0x0020; // Prescalar to 0x0000 so, 48MHz base clock
TIM2->ATRLR = 255; // 0..255 (So we can be 100% on)
TIM2->CHCTLR2 = TIM_OC4M_2 | TIM_OC4M_1;
TIM2->CCER = TIM_CC4E;
TIM2->CH4CVR = 0; // Actual duty cycle (Off to begin with)
// Enable TIM1 outputs
TIM2->BDTR = TIM_MOE;
TIM2->CTLR1 = TIM_CEN;
}
static void SetupADC()
{
// Configure ADC.
// PD4 is analog input chl 7
GPIOD->CFGLR &= ~(0xf<<(4*4)); // CNF = 00: Analog, MODE = 00: Input
// Reset the ADC to init all regs
RCC->APB2PRSTR |= RCC_APB2Periph_ADC1;
RCC->APB2PRSTR &= ~RCC_APB2Periph_ADC1;
// ADCCLK = 12 MHz => RCC_ADCPRE divide by 4
RCC->CFGR0 &= ~RCC_ADCPRE; // Clear out the bis in case they were set
RCC->CFGR0 |= RCC_ADCPRE_DIV4; // set it to 010xx for /4.
// Set up single conversion on chl 7
ADC1->RSQR1 = 0;
ADC1->RSQR2 = 0;
ADC1->RSQR3 = 7; // 0-9 for 8 ext inputs and two internals
//Injection group is 8. NOTE: See note in 9.3.12 (ADC_ISQR) of TRM. The
// group numbers is actually 4-group numbers.
ADC1->ISQR = (8<<15) | (0<<20);
// Sampling time for channels. Careful: This has PID tuning implications.
// Note that with 3 and 3,the full loop (and injection) runs at 138kHz.
ADC1->SAMPTR2 = (3<<(3*7)) | (3<<(3*8));
// 0:7 => 3/9/15/30/43/57/73/241 cycles
// (4 == 43 cycles), (6 = 73 cycles) Note these are alrady /2, so
// setting this to 73 cycles actually makes it wait 256 total cycles
// @ 48MHz.
// Turn on ADC and set rule group to sw trig
// 0 = Use TRGO event for Timer 1 to fire ADC rule.
ADC1->CTLR2 = ADC_ADON | ADC_JEXTTRIG | ADC_JEXTSEL | ADC_EXTTRIG;
// Reset calibration
ADC1->CTLR2 |= ADC_RSTCAL;
while(ADC1->CTLR2 & ADC_RSTCAL);
// Calibrate ADC
ADC1->CTLR2 |= ADC_CAL;
while(ADC1->CTLR2 & ADC_CAL);
// enable the ADC Conversion Complete IRQ
NVIC_EnableIRQ( ADC_IRQn );
// ADC_JEOCIE: Enable the End-of-conversion interrupt.
// ADC_JDISCEN | ADC_JAUTO: Force injection after rule conversion.
// ADC_SCAN: Allow scanning.
ADC1->CTLR1 = ADC_JEOCIE | ADC_JDISCEN | ADC_SCAN | ADC_JAUTO;
}
// Apply a given output mask to the GPIO ports the nixie tubes are hooked into.
static void ApplyOnMask( uint16_t onmask )
{
GPIOD->OUTDR = (onmask >> 8) | 0x80;
GPIOC->OUTDR = onmask & 0xff;
}
uint16_t GenOnMask( int segmenton )
{
// Produce a bit mask with only one bit on. To indicate the IO to turn on
// to light up a given segment. If segmenton == 0, then all IO are off.
if( segmenton > 0 )
{
segmenton--;
if( segmenton < 8 )
return 1<<segmenton;
else if( segmenton == 8 )
return (1<<2)<<8;
else if( segmenton == 9 )
return (1<<3)<<8;
else if( segmenton == 10 )
return (1<<0)<<8;
else if( segmenton == 11 )
return (1<<7)<<8;
}
return 0;
}
static void HandleCommand( uint32_t dmdword )
{
// You can use minichlink to setup this:
// ./minichlink -s 0x04 0x00B40041 # Configure for 180V.
// ./minichlink -s 0x04 0x00030042 # Light digit "8"
// ./minichlink -s 0x04 0x60303243 # Dimly light 8 and 8.
// ./minichlink -g 0x04 # Get status.
// Note: To get here, DEBUG0's LSB must be 0x4x command is that 'x'
int command = dmdword & 0x0f;
switch( command )
{
case 1:
{
int feedback = dmdword >> 16;
if( feedback > ABSOLUTE_MAX_ADC_SET )
feedback = ABSOLUTE_MAX_ADC_SET;
target_feedback = feedback;
break;
}
case 2:
{
int segmenton = (dmdword>>16)&0x0f;
fade_time0 = -1;
fade_time1 = -1;
fade_disp0 = GenOnMask(segmenton);
fade_disp1 = 0;
break;
}
case 3:
{
// Configure a fade.
fade_disp0 = GenOnMask( ( dmdword >> 8 ) & 0xf );
fade_disp1 = GenOnMask( ( dmdword >> 12 ) & 0xf );
fade_time0 = ( dmdword >> 16 ) & 0xff;
fade_time1 = ( dmdword >> 24 ) & 0xff;
break;
}
case 4:
{
#ifdef ENABLE_TUNING
// this is only for tuning.
if( ( dmdword & 0xff00 ) == 0xaa00 )
{
int period = (dmdword>>16)&0xff;
if( period < 20 ) period = 20;
PWM_PERIOD = period;
TIM1->ATRLR = PWM_PERIOD;
int max_duty = (dmdword>>24)&0xff;
if( max_duty > period - 14 ) max_duty = period - 14;
pwm_max_duty = max_duty;
}
#endif
break;
}
case 5:
{
// Aux Neon Control
TIM2->CH4CVR = dmdword>>16;
}
}
// Write the status back to the host PC. Status is our VDD and our FB V
*DMDATA0 = ((lastadc>>ADC_IIR) << 12) | ((lastrefvdd>>VDD_IIR) << 22);
}
static inline void WatchdogPet()
{
// Writing 0xaaaa into the ctlr prevents the watchdog from killing us.
IWDG->CTLR = 0xAAAA;
}
static inline void WatchdogSetup()
{
// Setup watchdog.
IWDG->CTLR = 0x5555; // Go into watchdog setup mode.
while( IWDG->STATR & IWDG_PVU ); // Wait for PSCR to become recepitve.
IWDG->PSCR = 1; // div LSI by 8 (4 seems unreliable)
IWDG->RLDR = 0xFFF; // reload watchdog, not important don't need to check.
IWDG->CTLR = 0xCCCC; // commit registers.
WatchdogPet();
}
static inline void AdvanceFadePlace()
{
static uint32_t lastmask = 0;
// Causes us to cycle through all 256 sequence points every 1.5ms.
uint32_t fadepos = (SysTick->CNT >> 5) & 0xff;
// We want to glow the LEDs with a chopping period of less, so we
// "rotate" the bits. This has the effect of making the primary
// switching frequency for the tubes much higher, but, at the same time
// also jittering the edges in time so you can get a full 8-bit fade.
// You can rotate more or less to control the periodicity.
// With this scramble, the period scramble is about 93us.
fadepos = (fadepos << 4) | ( fadepos >> 4);
uint32_t mask = HandleFade( fadepos );
if( mask != lastmask )
{
if( lastmask )
{
// Make sure we have a short gap with nothing on.
ApplyOnMask( 0 );
Delay_Us( 3 );
}
ApplyOnMask( mask );
lastmask = mask;
}
}
int main()
{
// Configure a watchdog timer so if the chip goes crazy it will reset.
WatchdogSetup();
// Use internall RC oscillator + 2xPLL to generate 48 MHz system clock.
SystemInit48HSI();
// For the ability to printf() if we want.
SetupDebugPrintf();
// Pet watchdog for the rest of startup.
WatchdogPet();
// Enable Peripherals
RCC->APB2PCENR |= RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOC |
RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1 | RCC_APB2Periph_ADC1 |
RCC_APB2Periph_AFIO;
RCC->APB1PCENR = RCC_APB1Periph_TIM2;
// I'm paranoid - let's make sure all tube cathodes are high-Z.
ApplyOnMask( 0 );
GPIOD->CFGLR =
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*6) | // GPIO D6 Debug
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP_AF)<<(4*7) | // DIG_AUX (TIM2CH4)
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*3) | // DIG_9
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*2) | // DIG_8
(GPIO_Speed_10MHz | GPIO_CNF_IN_FLOATING)<<(4*1) | // PGM Floats.
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*0); // DIG_DOT
GPIOC->CFGLR =
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*0) | // DIG_0
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*1) | // DIG_1
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*2) | // DIG_2
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*3) | // DIG_3
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*4) | // DIG_4
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*5) | // DIG_5
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*6) | // DIG_6
(GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*7); // DIG_7
GPIOA->CFGLR =
(GPIO_Speed_50MHz | GPIO_CNF_OUT_PP_AF)<<(4*1); //FLYBACK (T1CH2)
SetupADC();
SetupTimer1();
SetupTimer2();
*DMDATA0 = 0;
target_feedback = 0;
// Cause system timer to run and reload when it hits CMP and HCLK/8.
// Also, don't stop at comparison value.
SysTick->CTLR = 1;
while(1)
{
uint32_t dmdword = *DMDATA0;
if( (dmdword & 0xf0) == 0x40 )
{
// I think there is a compiler bug here. For some reason if I put
// the code in this function right here, it doesn't work right.
// so I encapsulated the code in a function.
//
// This function handles commands we get over the programming
// interface. Like "set HV bus" or "set this digit on."
HandleCommand( dmdword );
}
AdvanceFadePlace();
}
}