;=============================================================================== ; Title: Digital Power Supply. ; ; Author: Rob Jansen, Copyright (c) 2020 .. 2020, all rights reserved. ; ; Revisions ; --------- ; 2020-04-19 : Initial version using a digital potentiometer. ; 2020-05-03 : Revision 1. Including high voltage detection. ; 2020-05-21 : Revision 2. Version with DAC instead of digital potentiometer. ; ; Compiler: jalv25r3 ; ; Description: Control software for the digital power supply. ; Control is done by push buttons for up and down. A power on/off ; and three programmable preset buttons are also available. ; The program also checks the output voltage on a regular basis ; and switches the power off if becomes higher or lower than the ; voltage that was set. ; ; Sources: - ; ;=============================================================================== ; Some compiler pragmas for optimizations. Pragma warn all yes Pragma opt variable_reduce yes include 16f1765 ; This uses the 4 MHz internal oscillator, no high speed is required. pragma target clock 4_000_000 ; Oscillator frequency 4 MHz ; Configuration memory settings (fuses). pragma target OSC INTOSC_NOCLKOUT ; Internal Clock pragma target PLLEN DISABLED ; No PLL pragma target WDT DISABLED ; No Watchdog pragma target PWRTE ENABLED ; Power up timer enabled pragma target BROWNOUT DISABLED ; No brownout reset pragma target FCMEN DISABLED ; No clock monitoring pragma target IESO DISABLED ; int/ext osc. switch pragma target LVP ENABLED ; Low voltage programming pragma target MCLR INTERNAL ; Reset internal ; Set the internal clock frequency to 4 MHz. OSCCON_IRCF = 0b1101 ; Set 4 MHz OSCCON_SCS = 0b00 ; Clock determined by FOSC (32 MHz) ; Enable weak pull up for all ports except for port AN3 used for the ADC. WPUA = 0b0011_0111 WPUC = 0b0011_1111 OPTION_REG_WPUEN = FALSE ; Make all pins digital I/O instead of analog. enable_digital_io() ; ============================== Pin definitions ============================== ; This pin is externally pulled up as to shut down the power supply before it ; it is intialzed. After reset all pins are input. alias power_down_control is pin_A1 ; Pin 12 of DIP 14. alias power_pin_direction is pin_A1_direction ; All switches are active low. alias up_switch is pin_C4 ; Pin 6 of DIP 14. pin_C4_direction = input alias down_switch is pin_C5 ; Pin 5 of DIP 14. pin_C5_direction = input alias preset_1_switch is pin_C3 ; Pin 7 of DIP 14. pin_C3_direction = input alias preset_2_switch is pin_C2 ; Pin 8 of DIP 14. pin_C2_direction = input alias preset_3_switch is pin_C1 ; Pin 9 of DIP 14. pin_C1_direction = input alias power_switch is pin_C0 ; Pin 10 of DIP 14. pin_C0_direction = input ; ========================= Constants and Variables =========================== ; The minimal DAC value is not 0 because there is some initial voltage needed ; before the output of the LM723 will start to increase. const word MIN_DAC_VALUE = 125 const word MAX_DAC_VALUE = 1023 ; Digital to Analog Converter values. const word MIN_DAC_STEP = 1 const word MAX_DAC_STEP = 50 ; Hystersis for the high voltage protection const byte VOLTAGE_HYSTERESIS = 9 ; Equals about 0.2 Volt. ; Number of high voltage samples to be taken before deciding to power off. const byte VOLTAGE_SAMPLES = 10 ; High Endurance Flash (HEF) address declarations. It starts at the last FLASH ; address minus 128 bytes. The last FLASH address is 0x1FFF for this device. const HEF_NR_OF_BYTES = 128 const HEF_START_ADDRESS = 8192 - HEF_NR_OF_BYTES ; The DAC is 10 bits and the HEF uses word of 14 bits so we can store the DAC ; value immediately on these 14 bit words. const word HEF_CURRENT_ADDRESS = HEF_START_ADDRESS const word HEF_PRESET_1_ADDRESS = HEF_START_ADDRESS + 1 const word HEF_PRESET_2_ADDRESS = HEF_START_ADDRESS + 2 const word HEF_PRESET_3_ADDRESS = HEF_START_ADDRESS + 3 const word HEF_HIGH_VOLTAGE_ADDRESS = HEF_START_ADDRESS + 4 const word HEF_LOW_VOLTAGE_ADDRESS = HEF_START_ADDRESS + 5 ; Values for storing high and low voltage settings. Since the HEF uses 14 bits ; for storage we use a word for this. const word VOLTAGE_SETTING_ON = 0x0000 const word VOLTAGE_SETTING_OFF = 0x3FFF ; This is the default of an erased HEF. ; Key and device store times. const word KEY_STORE_TIME = 200 ; 200 * 10 ms = 2 seconds. const word DEVICE_STORE_TIME = 200 ; 200 * 10 ms = 2 seconds. const byte DEBOUNCE_COUNT = 10 ; 10 * 10 ms = 100 milseconds. ; _usec_delay times. const LOOP_TIME_10_MS = 10_000 const KEY_REPEAT_TIME = 250_000 ; 0.25 seconds. const KEY_DEBOUNCE_TIME = 50_000 ; Power supply control, active low. const bit POWER_CONTROL_ON = FALSE const bit POWER_CONTROL_OFF = TRUE ; Global variables. Only the relevant ones are initialized for power up. var word dac_current_value var word dac_new_value var word adc_capture_value var word adc_new_value var word adc_high_value var word adc_low_value var word key_store_counter var word device_store_counter var byte debounce_counter var word dac_step = MIN_DAC_STEP var byte high_voltage_sample_counter = 0 var byte low_voltage_sample_counter = 0 var bit adc_value_available var bit high_voltage_detection var bit low_voltage_detection var bit value_changed = FALSE var bit power_is_on ; ========================= Functions and Procedures ========================== -- Execute the High Endurance Flash unlock sequence for writing or erasing. procedure _hef_unlock() is PMCON2 = 0x55 PMCON2 = 0xAA PMCON1_WR = TRUE assembler nop nop end assembler end procedure -- Write a byte from the High Endurance Flash and return it. procedure _hef_erase(word in address) is var bit saved_gie saved_gie = INTCON_GIE INTCON_GIE = FALSE PMADR = address PMCON1_CFGS = FALSE PMCON1_FREE = TRUE PMCON1_WREN = TRUE _hef_unlock() -- This will erase one row. INTCON_GIE = saved_gie end procedure -- Read a byte from the High Endurance Flash and return it. Note that you -- can store (14-bit) words. function hef_read(word in address) return word is PMADR = address PMCON1_CFGS = FALSE PMCON1_RD = TRUE ; See datasheet why nops are needed. assembler nop nop end assembler return (PMDAT & 0x3FFF) end function -- Write a byte from the High Endurance Flash and return it. procedure _hef_write_flash(word in address, word in data, bit in latch) is var bit saved_gie saved_gie = INTCON_GIE INTCON_GIE = FALSE PMADR = address PMDAT = data PMCON1_LWLO = latch PMCON1_CFGS = FALSE PMCON1_FREE = FALSE PMCON1_WREN = TRUE _hef_unlock() -- This will store the data in the latch or flash. INTCON_GIE = saved_gie end procedure -- Write one word to the High Endurance Flash. Data is written to Flash in rows -- of (14-bit) words, called a latch. procedure hef_write(word in address, word in hef_data) is const HEF_ROWSIZE = 32 var word write_address var word row_address var word row_data[HEF_ROWSIZE] var byte counter write_address = address -- Determine the start address of the row for erase and write. The start -- address of the flash must always be the start address of a row. row_address = (address / HEF_ROWSIZE) * HEF_ROWSIZE address = row_address for HEF_ROWSIZE using counter loop row_data[counter] = hef_read(address) address = address + 1 end loop -- Map the index back to a location within one row. address = write_address % HEF_ROWSIZE row_data[byte(address)] = hef_data -- Now erase this flash row. _hef_erase(row_address) -- Fill the latches with the new data. address = row_address for HEF_ROWSIZE using counter loop _hef_write_flash(address, row_data[counter], TRUE) address = address + 1 end loop -- Store the contents of the latches into the Flash. Note that the datasheets -- describe that it is sufficient to write the whole latch but that does not -- work correctly. Instead we use the approach below as described in AN1673. address = row_address for HEF_ROWSIZE using counter loop _hef_write_flash(address, row_data[counter], FALSE) address = address + 1 end loop end procedure ; Read a setting from the given High Endurance Flash address and check if the ; HEF data is balid. If valid return the data read, otherwise return MIN_DAC_VALUE. function get_setting_from_hef(word in hef_address) return word is var word value value = hef_read(hef_address) ; Value must be in range. If not set it to MIN_DAC_VALUE. if (value > MAX_DAC_VALUE) then value = MIN_DAC_VALUE end if return value end function ; Initalize the power control. Power is off after initialization. procedure power_init() is power_down_control = TRUE ; Set high (=off) before making it output. power_pin_direction = output end procedure ; Switch the power supply on. procedure power_on() is power_is_on = TRUE power_down_control = POWER_CONTROL_ON end procedure ; Switch the power supply off. procedure power_off() is power_is_on = FALSE power_down_control = POWER_CONTROL_OFF end procedure ; Init the ADC and the GPIO pins (AN0). We follow the steps from the datasheet. ; Note that we also need to set the refence voltage so we need to start at least ; one ADC conversion. This procedure must be called after the power supply is ; powerd on and the supply voltage is set. procedure adc_init() Is ; We use Fosc/8 which is OK for a 4 MHz clock. VDD as reference voltage and ; AN3 as analog input. ANSELA = 0b0000_1000 ADCON0 = 0b0000_1101 ADCON1 = 0b1001_0000 adc_value_available = FALSE ; Nothing captured yet. ; Set and enable all interrupts PIR1_ADIF = FALSE PIE1_ADIE = TRUE INTCON_PEIE = TRUE INTCON_GIE = TRUE end procedure ; Handle the ADC Interrupt if set. procedure adc_Interrupt is pragma interrupt if PIR1_ADIF & PIE1_ADIE then ; New ADC value captured, copy data. adc_capture_value = (word(ADRESH) * 256) + word(ADRESL) adc_value_available = TRUE PIR1_ADIF = FALSE end if end procedure ; This function indicates when a new ADC value is available and copies the ; values to the global 'adc_wiper_new_value' variable. function new_adc_value return bit is var bit new_data ; Disable ADC interrupt as to prevent data change because of ADC interrupt. PIE1_ADIE = FALSE if adc_value_available then ; New value available, copy data. adc_new_value = adc_capture_value adc_value_available = FALSE new_data = TRUE else new_data = FALSE end if PIE1_ADIE = TRUE return new_data end function ; Start an Analog to Digital Conversion. procedure adc_start() is ADCON0_GO_NDONE = TRUE end procedure ; Set the voltage detection reference voltage if enabled and enable high and/or ; low voltage detection. The ADC must be initialized. procedure set_voltage_detection() is ; Get one ADC measurement. adc_start() _usec_delay(LOOP_TIME_10_MS) while !new_adc_value() loop ; Wait for measurement result. end loop ; Copy value and add some hystersis to it. This is about 0.15 Volt. adc_high_value = adc_new_value + VOLTAGE_HYSTERESIS adc_low_value = adc_new_value - VOLTAGE_HYSTERESIS ; Enable high and/or low voltage detection if needed. high_voltage_detection = (hef_read(HEF_HIGH_VOLTAGE_ADDRESS) == VOLTAGE_SETTING_ON) low_voltage_detection = (hef_read(HEF_LOW_VOLTAGE_ADDRESS) == VOLTAGE_SETTING_ON) end procedure procedure disable_voltage_detection() is high_voltage_detection = FALSE low_voltage_detection = FALSE end procedure ; Write the given value to the 10-bit DAC. procedure dac_set(word in value) is DAC1REF = value DACLD_DAC1LD = TRUE while DACLD_DAC1LD loop ; Wait for DAC to complete transfer. end loop end procedure ; Initialize the 10-bit Digital to Analog Converter (DAC) procedure dac_init() is ; Enable, right justified, output enable, use VDD as reference, VRef- to VSS. DAC1CON0 = 0b1010_0000 ; VDD reference. dac_set(MIN_DAC_VALUE) ; Output at minimum end procedure ; Increment the given value but let it never get higher than MAX_DAC_VALUE. function dac_increment(word in value) return word is var word new_value new_value = value + dac_step if (new_value > MAX_DAC_VALUE) then value = MAX_DAC_VALUE else value = new_value end if return value end function ; Decrement the given value but let it never get lower than MIN_DAC_VALUE. function dac_decrement(word in value) return word is if (dac_step > value) then ; Value cannot be negative. value = MIN_DAC_VALUE else value = value - dac_step end if return value end function ; ========================= Main program starts here ========================== power_init() adc_init() dac_init() ; Read the current settings from HEF(if any) and set the wiper accordingly. dac_current_value = get_setting_from_hef(HEF_CURRENT_ADDRESS) dac_set(dac_current_value) ; Now we can enable the output of the power supply. power_on() ; Let the power stabilize. _usec_delay(500_000) ; Before continuing, see if we need to check if we need to (re-) set the ; voltage detection. if !power_switch then _usec_delay(KEY_DEBOUNCE_TIME) if !power_switch then ; Power switch pressed at power on will reset all voltage detection. hef_write(HEF_HIGH_VOLTAGE_ADDRESS, VOLTAGE_SETTING_OFF) hef_write(HEF_LOW_VOLTAGE_ADDRESS, VOLTAGE_SETTING_OFF) end if else if !down_switch then _usec_delay(KEY_DEBOUNCE_TIME) if !down_switch then ; Power down pressed at power on will set low voltage detection. hef_write(HEF_LOW_VOLTAGE_ADDRESS, VOLTAGE_SETTING_ON) end if end if if !up_switch then _usec_delay(KEY_DEBOUNCE_TIME) if !up_switch then ; Power up pressed at power on will set high voltage detection. hef_write(HEF_HIGH_VOLTAGE_ADDRESS, VOLTAGE_SETTING_ON) end if end if end if ; Enable voltage detection (if set). set_voltage_detection() forever loop ; We use a 10 ms looptime for all our timing. _usec_delay(LOOP_TIME_10_MS) ; If the up switch and down switch are released, we reset the DAC step. if up_switch & down_switch then dac_step = MIN_DAC_STEP end if ; Check if we need to power off the power supply because of high of low ; voltage. if power_is_on then ; Read the adc value once. Value is stored in 'adc_new_value'. if new_adc_value() then ; First check for high voltage. if high_voltage_detection & (adc_new_value > adc_high_value) then ; New voltage is higher check if was not a spike. if high_voltage_sample_counter == VOLTAGE_SAMPLES then ; The voltage is too high for too long, power off the power supply. high_voltage_sample_counter = 0 power_off() else high_voltage_sample_counter = high_voltage_sample_counter + 1 end if else high_voltage_sample_counter = 0 end if ; Now check for low voltage. if low_voltage_detection & (adc_new_value < adc_low_value) then ; New voltage is lower check if was not a spike. if low_voltage_sample_counter == VOLTAGE_SAMPLES then ; The voltage is too low for too long, power off the power supply. low_voltage_sample_counter = 0 power_off() else low_voltage_sample_counter = low_voltage_sample_counter + 1 end if else low_voltage_sample_counter = 0 end if end if end if ; We use a more solid detection of a key press by checking if the switch is ; actually pressed for a certain time without any debouncing in betweeen. ; The debounce counter is used for that purpose and is reset before each ; measurement. ; The power switch will toggle the power supply on or off. debounce_counter = 0 while !power_switch & (debounce_counter < DEBOUNCE_COUNT) loop _usec_delay(LOOP_TIME_10_MS) debounce_counter = debounce_counter + 1 end loop if (debounce_counter == DEBOUNCE_COUNT) then ; Toggle the power mode. if power_is_on then power_off() else power_on() _usec_delay(100_000) set_voltage_detection() end if ; Wait for power switch release to prevent a toggle repeat. while !power_switch loop end loop end if ; The up switch has a repeat functionality when pressed longer. The debounce ; time has to be taken into account in the repeat frequency. debounce_counter = 0 while !up_switch & (debounce_counter < DEBOUNCE_COUNT) loop _usec_delay(LOOP_TIME_10_MS) debounce_counter = debounce_counter + 1 end loop if (debounce_counter == DEBOUNCE_COUNT) then while !up_switch & (dac_current_value < MAX_DAC_VALUE) loop ; Not at maximum yet. power_on() ; In case it was off. dac_current_value = dac_increment(dac_current_value) dac_set(dac_current_value) ; Storing in HEF is done later to minize storage writes. value_changed = TRUE device_store_counter = 0 ; Speed up the DAC increase step in voltage as long as key pressed. if(dac_step < MAX_DAC_STEP) then dac_step = dac_step + 1 end if _usec_delay(KEY_REPEAT_TIME) end loop end if ; The down switch has a repeat functionality when pressed longer. The debounce ; time has to be taken into account in the repeat frequency. debounce_counter = 0 while !down_switch & (debounce_counter < DEBOUNCE_COUNT) loop _usec_delay(LOOP_TIME_10_MS) debounce_counter = debounce_counter + 1 end loop if (debounce_counter == DEBOUNCE_COUNT) then while !down_switch & (dac_current_value > MIN_DAC_VALUE) loop ; Not at minimum yet. power_on() ; In case it was off. dac_current_value = dac_decrement(dac_current_value) dac_set(dac_current_value) ; Storing in HEF is done later to minize storage writes. value_changed = TRUE device_store_counter = 0 ; Speed up the DAC increase step in voltage as long as key pressed. if(dac_step < MAX_DAC_STEP) then dac_step = dac_step + 1 end if _usec_delay(KEY_REPEAT_TIME) end loop end if ; A preset switch has 2 options, either retrieve the value that was stored ; in HEF or use the current value and store that in HEF. In both cases ; the ADC needs to be set. Note that the actual storing of the value in ; HEF is done at the end of the main loop. ; Check if switched is pressed and filter out the debounce. debounce_counter = 0 while !preset_1_switch & (debounce_counter < DEBOUNCE_COUNT) loop _usec_delay(LOOP_TIME_10_MS) debounce_counter = debounce_counter + 1 end loop if (debounce_counter == DEBOUNCE_COUNT) then ; Pressed, now see if it stays pressed as to store the current value. key_store_counter = 0 power_on() ; In case it was off. while !preset_1_switch & (key_store_counter < KEY_STORE_TIME) loop _usec_delay(LOOP_TIME_10_MS) key_store_counter = key_store_counter + 1 end loop ; Wait for key release. while !preset_1_switch loop end loop ; Check if we need to store this value or just read a value from HEF. if (key_store_counter == KEY_STORE_TIME) then ; Store this as preset setting. hef_write(HEF_PRESET_1_ADDRESS, dac_current_value) ; Also store this as the new current value for the next power up ; and set the high voltage detection when the power is stable again. value_changed = TRUE device_store_counter = 0 else ; If value is changed then set the new value. dac_new_value = get_setting_from_hef(HEF_PRESET_1_ADDRESS) if (dac_new_value != dac_current_value) then dac_current_value = dac_new_value dac_set(dac_current_value) ; Also store this as the new current value for the next power up ; and set the high voltage detection when the power is stable again. value_changed = TRUE device_store_counter = 0 end if end if end if ; Check if switched is pressed and filter out the debounce. debounce_counter = 0 while !preset_2_switch & (debounce_counter < DEBOUNCE_COUNT) loop _usec_delay(LOOP_TIME_10_MS) debounce_counter = debounce_counter + 1 end loop if (debounce_counter == DEBOUNCE_COUNT) then ; Pressed, now see if it stays pressed as to store the current value. power_on() ; In case it was off. key_store_counter = 0 while !preset_2_switch & (key_store_counter < KEY_STORE_TIME) loop _usec_delay(LOOP_TIME_10_MS) key_store_counter = key_store_counter + 1 end loop ; Wait for key release. while !preset_2_switch loop end loop ; Check if we need to store this value or just read a value from HEF. if (key_store_counter == KEY_STORE_TIME) then ; Store this as preset setting. hef_write(HEF_PRESET_2_ADDRESS, dac_current_value) ; Also store this as the new current value for the next power up ; and set the high voltage detection when the power is stable again. value_changed = TRUE device_store_counter = 0 else ; If value is changed then set the new value. dac_new_value = get_setting_from_hef(HEF_PRESET_2_ADDRESS) if (dac_new_value != dac_current_value) then dac_current_value = dac_new_value dac_set(dac_current_value) ; Also store this as the new current value for the next power up ; and set the high voltage detection when the power is stable again. value_changed = TRUE device_store_counter = 0 end if end if end if ; Check if switched is pressed and filter out the debounce. debounce_counter = 0 while !preset_3_switch & (debounce_counter < DEBOUNCE_COUNT) loop _usec_delay(LOOP_TIME_10_MS) debounce_counter = debounce_counter + 1 end loop if (debounce_counter == DEBOUNCE_COUNT) then ; Pressed, now see if it stays pressed as to store the current value. power_on() ; In case it was off. key_store_counter = 0 while !preset_3_switch & (key_store_counter < KEY_STORE_TIME) loop _usec_delay(LOOP_TIME_10_MS) key_store_counter = key_store_counter + 1 end loop ; Wait for key release. while !preset_3_switch loop end loop ; Check if we need to store this value or just read a value from HEF. if (key_store_counter == KEY_STORE_TIME) then ; Store this as preset setting. hef_write(HEF_PRESET_3_ADDRESS, dac_current_value) ; Also store this as the new current value for the next power up ; and set the high voltage detection when the power is stable again. value_changed = TRUE device_store_counter = 0 else ; If value is changed then set the new value. dac_new_value = get_setting_from_hef(HEF_PRESET_3_ADDRESS) if (dac_new_value != dac_current_value) then dac_current_value = dac_new_value dac_set(dac_current_value) ; Also store this as the new current value for the next power up ; and set the high voltage detection when the power is stable again. value_changed = TRUE device_store_counter = 0 end if end if end if ; Check if we need to store a new current value. We do this after some time ; as to prevent writes to the HEF for every increment when using the up ; or down switch. if value_changed then disable_voltage_detection() if (device_store_counter == DEVICE_STORE_TIME) then ; Store new current value in HEF. hef_write(HEF_CURRENT_ADDRESS, dac_current_value) ; Now also get the current ADC reference value for high voltage detection. if power_is_on then set_voltage_detection() end if value_changed = FALSE else device_store_counter = device_store_counter + 1 end if end if ; Start a new ADC conversion. Due to the mainloop looptime there is ; sufficient time for the ADC to complete one conversion cycle. adc_start() end loop