├── CMakeLists.txt ├── GUI └── Powercore_gui.py ├── LICENSE ├── pico_sdk_import.cmake └── powercore.c /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | add_executable(powercore 4 | powercore.c 5 | ) 6 | 7 | 8 | # initialize the SDK based on PICO_SDK_PATH 9 | # note: this must happen before project() 10 | include(pico_sdk_import.cmake) 11 | 12 | project(powercore) 13 | 14 | target_link_libraries(powercore pico_stdlib hardware_pwm hardware_adc hardware_dma) 15 | 16 | # initialize the Raspberry Pi Pico SDK 17 | pico_sdk_init() 18 | 19 | pico_enable_stdio_usb(powercore 1) 20 | pico_enable_stdio_uart(powercore 0) 21 | 22 | # create map/bin/hex/uf2 file in addition to ELF. 23 | pico_add_extra_outputs(powercore) 24 | -------------------------------------------------------------------------------- /GUI/Powercore_gui.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk, messagebox 3 | import json 4 | import serial 5 | import serial.tools.list_ports 6 | import time 7 | 8 | # Initialize serial object 9 | ser = None 10 | 11 | def get_available_ports(): 12 | """Get a list of available COM ports.""" 13 | ports = [port.device for port in serial.tools.list_ports.comports()] 14 | return ports 15 | 16 | def connect_serial(): 17 | """Connect to the selected COM port.""" 18 | global ser 19 | port = com_port_combo.get() 20 | try: 21 | ser = serial.Serial(port, 115200, timeout=1) 22 | status_label.config(text="Status: Connected") 23 | except Exception as e: 24 | messagebox.showerror("Error", f"Cannot connect to {port}. {e}") 25 | 26 | def disconnect_serial(): 27 | """Disconnect from the COM port.""" 28 | global ser 29 | if ser and ser.is_open: 30 | ser.close() 31 | status_label.config(text="Status: Disconnected") 32 | 33 | def send_freq_to_powercore(): 34 | # Retrieve the Pulse Frequency value from the GUI 35 | pwm_freq_value = freq_entry.get() 36 | 37 | # Format the data into the custom delimited format 38 | data_string = f"pwm_frequency={pwm_freq_value}" 39 | 40 | # Send the data over the serial connection 41 | if ser.is_open: 42 | ser.write(data_string.encode('utf-8')) 43 | 44 | 45 | def send_coulomb_to_powercore(): 46 | # Retrieve the Max µC per pulse value from the GUI 47 | micro_coulomb_value = max_coulomb_entry.get() 48 | 49 | # Format the data into the custom delimited format 50 | data_string = f"micro_c_per_pulse={micro_coulomb_value}" 51 | 52 | # Send the data over the serial connection 53 | if ser.is_open: 54 | ser.write(data_string.encode('utf-8')) 55 | 56 | def read_from_serial(): 57 | """ 58 | Read data from the serial port, parse it, and update the GUI. 59 | """ 60 | global ser 61 | data_dict = {} # Initialize the dictionary 62 | if ser and ser.is_open: 63 | try: 64 | # Read a line from the serial port 65 | data_string = ser.readline().decode().strip() 66 | 67 | # Parse the custom delimited format 68 | #data_dict = {pair.split('=')[0]: pair.split('=')[1] for pair in data_string.split(',')} 69 | data_items = data_string.split(',') 70 | for item in data_items: 71 | if '=' in item: 72 | key, value = item.split('=') 73 | data_dict[key.strip()] = value.strip() 74 | 75 | # Update GUI components conditionally 76 | if "spark%" in data_dict: 77 | spark_progress["value"] = int(data_dict["spark%"]) 78 | if "short%" in data_dict: 79 | short_progress["value"] = int(data_dict["short%"]) 80 | if "avgPower" in data_dict: 81 | avg_power_value["text"] = data_dict["avgPower"] 82 | if "avgCharge" in data_dict: 83 | avg_charge_value["text"] = data_dict["avgCharge"] 84 | if "pulseFreq" in data_dict: 85 | freq_value["text"] = data_dict["pulseFreq"] 86 | if "maxCoulomb" in data_dict: 87 | max_coulomb_value["text"] = data_dict["maxCoulomb"] 88 | if "mosfetTemp" in data_dict: 89 | mosfetTemp["text"] = data_dict["mosfetTemp"] 90 | if "resistorTemp" in data_dict: 91 | resistorTemp["text"] = data_dict["resistorTemp"] 92 | # Append the message to the message box 93 | message = data_dict.get("message", "") 94 | if message: # Only append if there's a new message 95 | message_box.insert(tk.END, f"\n[{time.strftime('%H:%M:%S')}] {message}") 96 | message_box.see(tk.END) 97 | 98 | except Exception as e: 99 | print(f"Error reading/parsing serial data: {e}") 100 | # Schedule the read_from_serial function to start when the GUI is launched 101 | root.after(100, read_from_serial) 102 | 103 | # Create the main window 104 | root = tk.Tk() 105 | root.title("Powercore EDM Controller") 106 | 107 | # Schedule the read_from_serial function to start when the GUI is launched 108 | root.after(100, read_from_serial) 109 | 110 | # Frame for "Cut Status" section 111 | cut_status_frame = ttk.LabelFrame(root, text="Cut Status") 112 | cut_status_frame.pack(pady=10, padx=20, fill="x") 113 | 114 | # Progress bars and labels for "Spark %" and "Short %" 115 | spark_progress = ttk.Progressbar(cut_status_frame, orient="horizontal", length=200, mode="determinate") 116 | spark_progress.grid(row=0, column=1, pady=5, padx=5) 117 | spark_label = ttk.Label(cut_status_frame, text="Spark %") 118 | spark_label.grid(row=0, column=0, pady=5, padx=5) 119 | 120 | short_progress = ttk.Progressbar(cut_status_frame, orient="horizontal", length=200, mode="determinate") 121 | short_progress.grid(row=1, column=1, pady=5, padx=5) 122 | short_label = ttk.Label(cut_status_frame, text="Short %") 123 | short_label.grid(row=1, column=0, pady=5, padx=5) 124 | 125 | # Readouts for "Average Power" and "Average Spark Charge" 126 | avg_power_label = ttk.Label(cut_status_frame, text="Average Spark Power (W):") 127 | avg_power_label.grid(row=2, column=0, pady=5, padx=5) 128 | avg_power_value = ttk.Label(cut_status_frame, text="0") 129 | avg_power_value.grid(row=2, column=1, pady=5, padx=5) 130 | 131 | avg_charge_label = ttk.Label(cut_status_frame, text="Average Spark Charge (µC):") 132 | avg_charge_label.grid(row=3, column=0, pady=5, padx=5) 133 | avg_charge_value = ttk.Label(cut_status_frame, text="0") 134 | avg_charge_value.grid(row=3, column=1, pady=5, padx=5) 135 | 136 | # MOSFET Temperature 137 | mosfet_temp_label = ttk.Label(cut_status_frame, text="MOSFET Temperature (°C):") 138 | mosfet_temp_label.grid(row=4, column=0, padx=10, pady=5, sticky=tk.W) 139 | 140 | mosfetTemp = ttk.Label(cut_status_frame, text="0") # Initialize with default value 141 | mosfetTemp.grid(row=4, column=1, padx=10, pady=5) 142 | 143 | # Power Resistor Temperature 144 | resistor_temp_label = ttk.Label(cut_status_frame, text="Power Resistor Temperature (°C):") 145 | resistor_temp_label.grid(row=5, column=0, padx=10, pady=5, sticky=tk.W) 146 | 147 | resistorTemp = ttk.Label(cut_status_frame, text="0") # Initialize with default value 148 | resistorTemp.grid(row=5, column=1, padx=10, pady=5) 149 | 150 | 151 | # Frame for "Control" section 152 | control_frame = ttk.LabelFrame(root, text="Control") 153 | control_frame.pack(pady=10, padx=20, fill="x") 154 | 155 | # Headers for Current and Target 156 | ttk.Label(control_frame, text="Current").grid(row=0, column=1, pady=5, padx=5) 157 | ttk.Label(control_frame, text="Target").grid(row=0, column=2, pady=5, padx=5) 158 | 159 | # Pulse Frequency 160 | ttk.Label(control_frame, text="Pulse Frequency (Hz):").grid(row=1, column=0, pady=5, padx=5, sticky=tk.W) 161 | freq_value = ttk.Label(control_frame, text="1500") 162 | freq_value.grid(row=1, column=1, pady=5, padx=5) 163 | freq_entry = ttk.Entry(control_frame) 164 | freq_entry.grid(row=1, column=2, pady=5, padx=5) 165 | update_freq_button = ttk.Button(control_frame, text="Update", command=send_freq_to_powercore) 166 | update_freq_button.grid(row=1, column=3, pady=5, padx=5) 167 | 168 | # Max micro coulomb per pulse 169 | ttk.Label(control_frame, text="Max µC per pulse:").grid(row=2, column=0, pady=5, padx=5, sticky=tk.W) 170 | max_coulomb_value = ttk.Label(control_frame, text="1600") 171 | max_coulomb_value.grid(row=2, column=1, pady=5, padx=5) 172 | max_coulomb_entry = ttk.Entry(control_frame) 173 | max_coulomb_entry.insert(0, "1400") 174 | max_coulomb_entry.grid(row=2, column=2, pady=5, padx=5) 175 | update_coulomb_button = ttk.Button(control_frame, text="Update", command=send_coulomb_to_powercore) 176 | update_coulomb_button.grid(row=2, column=3, pady=5, padx=5) 177 | 178 | 179 | # Frame for "Connection" section 180 | connection_frame = ttk.LabelFrame(root, text="Connection") 181 | connection_frame.pack(pady=10, padx=20, fill="x") 182 | 183 | # COM port selector, connect and disconnect buttons, and status label 184 | com_port_label = ttk.Label(connection_frame, text="COM Port:") 185 | com_port_label.grid(row=0, column=0, pady=5, padx=5) 186 | com_port_combo = ttk.Combobox(connection_frame) 187 | com_port_combo.grid(row=0, column=1, pady=5, padx=5) 188 | connect_button = ttk.Button(connection_frame, text="Connect") 189 | connect_button.grid(row=0, column=2, pady=5, padx=5) 190 | disconnect_button = ttk.Button(connection_frame, text="Disconnect") 191 | disconnect_button.grid(row=0, column=3, pady=5, padx=5) 192 | status_label = ttk.Label(connection_frame, text="Status: Disconnected") 193 | status_label.grid(row=1, column=0, columnspan=4, pady=5, padx=5) 194 | 195 | # Frame for "Messages" section 196 | messages_frame = ttk.LabelFrame(root, text="Messages") 197 | messages_frame.pack(pady=10, padx=20, fill="both", expand=True) 198 | 199 | # Text widget for the message box 200 | message_box = tk.Text(messages_frame, height=4, width=50) # Adjust height and width as needed 201 | message_box.pack(pady=10, padx=10, fill="both", expand=True) 202 | 203 | 204 | # Update COM port dropdown with available ports 205 | com_port_combo["values"] = get_available_ports() 206 | 207 | # Connect buttons to their actions 208 | connect_button.config(command=connect_serial) 209 | disconnect_button.config(command=disconnect_serial) 210 | 211 | 212 | root.mainloop() 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /pico_sdk_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | # GIT_SUBMODULES_RECURSE was added in 3.17 33 | if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") 34 | FetchContent_Declare( 35 | pico_sdk 36 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 37 | GIT_TAG master 38 | GIT_SUBMODULES_RECURSE FALSE 39 | ) 40 | else () 41 | FetchContent_Declare( 42 | pico_sdk 43 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 44 | GIT_TAG master 45 | ) 46 | endif () 47 | 48 | if (NOT pico_sdk) 49 | message("Downloading Raspberry Pi Pico SDK") 50 | FetchContent_Populate(pico_sdk) 51 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 52 | endif () 53 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 54 | else () 55 | message(FATAL_ERROR 56 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 57 | ) 58 | endif () 59 | endif () 60 | 61 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 62 | if (NOT EXISTS ${PICO_SDK_PATH}) 63 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 64 | endif () 65 | 66 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 67 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 68 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 69 | endif () 70 | 71 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 72 | 73 | include(${PICO_SDK_INIT_CMAKE_FILE}) 74 | -------------------------------------------------------------------------------- /powercore.c: -------------------------------------------------------------------------------- 1 | #include "pico/stdlib.h" 2 | #include 3 | #include 4 | #include "hardware/pwm.h" 5 | #include "hardware/adc.h" 6 | #include "hardware/dma.h" 7 | #include "pico/time.h" 8 | #include "tusb.h" 9 | #include 10 | 11 | // ### System Defines ### 12 | // Maximum allowable temperature of power resistor board) 13 | #define maximum_allowable_temperature_of_power_resistor 80.0 // Celsius 14 | // Maximum allowable temperature of power resistor board 15 | #define maximum_allowable_temperature_of_power_MOSFET 80.0 // Celsius 16 | // Minimum allowable temperature used to ensure temp probe connection or sensor isnt faulty 17 | #define minimum_allowable_temperature 0 // Celsius 18 | // The beta coefficient of the NTC 3950 thermister, which is a specification of the thermister used to calculate temperature 19 | #define NTC_3950_thermister_beta_coefficient 3950 20 | // Nominal resistance of the NTC 3950 thermister 21 | #define NTC_3950_thermister_normal_resistance 100000 // ohms 22 | // The pullup resistor for the thermisters on the Powercore motherboard 23 | #define thermister_pullup_resistor_value 10000 // ohms 24 | #define measured_3v3 3.255 // For better ADC accuracy measure your 3v3 rail and enter the value here 25 | #define NTC_Thermister_Number_Of_Samples 2 26 | 27 | // ### Pins Definition ### 28 | #define HIGH_VOLTAGE_MOSFET_PIN 17 29 | #define USER_LED_PIN 25 30 | #define P12 12 31 | #define SHORT_ALERT_MOSFET_PIN 20 32 | #define DATA_LENGTH 256 33 | 34 | // ### DMA configuration ### 35 | uint DMA_CHANNEL = 0; 36 | 37 | // ### Globals Vars ### 38 | uint pulse_counter = 0; 39 | uint spark_counter = 0; 40 | uint short_counter = 0; 41 | uint spark_percent = 0; 42 | uint short_percent = 0; 43 | 44 | double mosfet_temperature = 0; 45 | double power_resistor_temperature = 0; 46 | double avg_power = 0; 47 | double avg_charge = 0; 48 | uint current_pwm_frequency = 0; 49 | uint target_pwm_frequency = 2000; 50 | 51 | // Globals for ADC and DMA 52 | #define ADC_SAMPLE_NUM 10 // each adc sample is 2us so 10 samples will be 20us 53 | uint8_t adc_samples[ADC_SAMPLE_NUM]; 54 | volatile bool adc_samples_ready = false; // This flag tells if ADC samples are ready 55 | volatile double micro_coulomb_per_pulse = 0; 56 | volatile uint adc_samples_sum = 0; 57 | volatile double voltage = 0; 58 | volatile bool pwm_wrap_int = false; // This flag tells if PWM has wraped 59 | volatile double max_micro_coulomb_per_pulse = 2500; 60 | uint slice_num = 0; 61 | uint chan = 0; 62 | uint pwm_compare_level = 0; 63 | bool LowPowerMode = false; 64 | 65 | //This function can be used to set pwm using a fixed on time 66 | uint32_t pwm_set_freq_fixed_on_time(uint slice_num, uint chan, uint32_t f) 67 | { 68 | const double desired_on_time = 0.00002; // 20 us 69 | uint32_t clock = 125000000; 70 | uint32_t divider16 = clock / f / 4096 + 71 | (clock % (f * 4096) != 0); 72 | if (divider16 / 16 == 0) 73 | divider16 = 16; 74 | uint32_t wrap = clock * 16 / divider16 / f - 1; 75 | pwm_set_clkdiv_int_frac(slice_num, divider16 / 16, 76 | divider16 & 0xF); 77 | pwm_set_wrap(slice_num, wrap); 78 | 79 | // Compute the duty cycle based on the desired on-time duration 80 | double T = 1.0 / f; 81 | double duty_cycle = desired_on_time / T; 82 | uint32_t on_time_count = (uint32_t)(wrap * duty_cycle); 83 | 84 | pwm_set_chan_level(slice_num, chan, on_time_count); 85 | pwm_compare_level = on_time_count; 86 | current_pwm_frequency = f; 87 | 88 | return wrap; 89 | } 90 | 91 | //This function can be used to set pwm frequency and duty 92 | uint32_t pwm_set_freq_duty(uint slice_num, uint chan, uint32_t f, int d) 93 | { 94 | uint32_t clock = 125000000; 95 | uint32_t divider16 = clock / f / 4096 + 96 | (clock % (f * 4096) != 0); 97 | if (divider16 / 16 == 0) 98 | divider16 = 16; 99 | uint32_t wrap = clock * 16 / divider16 / f - 1; 100 | pwm_set_clkdiv_int_frac(slice_num, divider16 / 16, 101 | divider16 & 0xF); 102 | pwm_set_wrap(slice_num, wrap); 103 | pwm_set_chan_level(slice_num, chan, wrap * d / 100); 104 | pwm_compare_level = (wrap * d / 100); 105 | current_pwm_frequency = f; 106 | return wrap; 107 | } 108 | 109 | //This Interrupt service routine function gets called when the PWM wraps around. i.e. starts its cycle over 110 | void pwm_isr() 111 | { 112 | pwm_clear_irq(slice_num); // Clear the interrupt 113 | pwm_wrap_int = true; // Toggle the PWM wrap_int flag 114 | gpio_put(P12, 1); // Used to check timing of how long this ISR took 115 | adc_run(false); 116 | adc_select_input(0); 117 | adc_fifo_drain(); 118 | adc_run(true); 119 | dma_channel_transfer_to_buffer_now(DMA_CHANNEL, adc_samples, ADC_SAMPLE_NUM); 120 | dma_channel_wait_for_finish_blocking(DMA_CHANNEL); 121 | 122 | adc_run(false); 123 | // integrate the current 124 | adc_samples_sum = 0; 125 | for (int i = 0; i < ADC_SAMPLE_NUM; i++) 126 | { 127 | adc_samples_sum = adc_samples_sum + adc_samples[i]; 128 | } 129 | micro_coulomb_per_pulse = ((double)adc_samples_sum) * (3.3 / 256.0); // samples to volts (DMA transferred top 8 Bits) 130 | micro_coulomb_per_pulse = micro_coulomb_per_pulse / 50.0; // divide out gain of INA 131 | micro_coulomb_per_pulse = micro_coulomb_per_pulse / 0.001; // I=v/r r = 1mohm 132 | micro_coulomb_per_pulse = micro_coulomb_per_pulse * 20.0; // C = A* S; 20us of total sample time (.00002 * 1,000,000) as we display in micro's 133 | if (micro_coulomb_per_pulse > max_micro_coulomb_per_pulse) 134 | { 135 | if (LowPowerMode == false) 136 | { 137 | // pwm_set_freq_duty(slice_num, chan, 500, 1); 138 | pwm_set_freq_fixed_on_time(slice_num, chan, 500); 139 | LowPowerMode = true; 140 | } 141 | } 142 | else 143 | { 144 | if (LowPowerMode) 145 | { 146 | //pwm_set_freq_duty(slice_num, chan, 1500, 3); 147 | pwm_set_freq_fixed_on_time(slice_num, chan, target_pwm_frequency); 148 | LowPowerMode = false; 149 | } 150 | } 151 | gpio_put(P12, 0); // Used to check timing of how long this ISR took 152 | } 153 | 154 | //This function can be used to get temperature from NTC temperature sensor. 155 | double get_temperature(u_int8_t thermister_pin) 156 | { 157 | adc_select_input(thermister_pin); 158 | double voltage_sum = 0.0; 159 | 160 | for (int i = 0; i < NTC_Thermister_Number_Of_Samples; i++) 161 | { 162 | double voltage_sample = adc_read() * (3.3 / 4096.0); 163 | voltage_sum += voltage_sample; 164 | } 165 | 166 | double average_voltage = voltage_sum / NTC_Thermister_Number_Of_Samples; 167 | double observed_thermister_resistance = (average_voltage * thermister_pullup_resistor_value) / (measured_3v3 - average_voltage); 168 | double steinhart = observed_thermister_resistance / NTC_3950_thermister_normal_resistance; 169 | steinhart = log(steinhart); 170 | steinhart /= NTC_3950_thermister_beta_coefficient; 171 | steinhart += 1.0 / (25.0 + 273.15); 172 | steinhart = 1.0 / steinhart; 173 | steinhart -= 273.15; // convert absolute temp to C 174 | double calc_temperature = steinhart; 175 | 176 | return calc_temperature; 177 | } 178 | 179 | //This function can be used to send a message to the GUI 180 | void send_message(char *message) 181 | { 182 | char data[DATA_LENGTH]; 183 | snprintf(data, DATA_LENGTH, "message=%s\n", message); 184 | tud_cdc_write(data, strlen(data)); 185 | tud_cdc_write_flush(); 186 | } 187 | 188 | //This function handles shutting down the PSU in a safe way 189 | void safety_shutdown() 190 | { 191 | pwm_set_enabled(slice_num, false); 192 | send_message("Temperature Safety Shutdown!!"); 193 | } 194 | 195 | //This function checks that the temperature sensors are not outside acceptable range. If they are call the safety_shutdown function 196 | void thermal_runaway_protection_check() 197 | { 198 | if (power_resistor_temperature > maximum_allowable_temperature_of_power_resistor) 199 | { 200 | safety_shutdown(); 201 | } 202 | if (mosfet_temperature > maximum_allowable_temperature_of_power_MOSFET) 203 | { 204 | safety_shutdown(); 205 | } 206 | if ((mosfet_temperature < minimum_allowable_temperature) || (power_resistor_temperature < minimum_allowable_temperature)) 207 | { 208 | safety_shutdown(); 209 | } 210 | } 211 | 212 | //This function configures the DMA for collecting ADC samples 213 | void setup_dma() 214 | { 215 | // Get a free DMA channel 216 | DMA_CHANNEL = dma_claim_unused_channel(true); 217 | 218 | dma_channel_config c = dma_channel_get_default_config(DMA_CHANNEL); 219 | 220 | // Set DMA to transfer 16-bit data from ADC to adc_samples array 221 | channel_config_set_transfer_data_size(&c, DMA_SIZE_8); 222 | channel_config_set_read_increment(&c, false); 223 | channel_config_set_write_increment(&c, true); 224 | 225 | // Set DMA to trigger on ADC data request 226 | channel_config_set_dreq(&c, DREQ_ADC); 227 | 228 | // Set up the DMA channel with the given configuration 229 | dma_channel_configure( 230 | DMA_CHANNEL, 231 | &c, 232 | adc_samples, // Destination 233 | &adc_hw->fifo, // Source 234 | ADC_SAMPLE_NUM, // Transfer count 235 | false // Don't start immediately 236 | ); 237 | dma_channel_set_irq0_enabled(DMA_CHANNEL, true); 238 | } 239 | 240 | //This function configures the ADC and the setup needed for allowing the DMA to get the data 241 | void setup_adc() 242 | { 243 | 244 | adc_gpio_init(26); // ADC0 is on GPIO 26 245 | adc_gpio_init(27); // ADC1 is on GPIO 27 246 | adc_gpio_init(28); // ADC2 is on GPIO 28 247 | adc_init(); 248 | adc_select_input(0); 249 | adc_fifo_setup(true, // Write to FIFO 250 | true, // Enable DMA data request (DREQ) 251 | 1, // DREQ (data request) threshold (1>=) 252 | true, // Allow overflow on FIFO full 253 | true // Enable ADC 254 | ); 255 | adc_set_clkdiv(0); 256 | } 257 | 258 | //This function sets up the PWM and configures the interrupt on wrap 259 | void setup_pwm() 260 | { 261 | slice_num = pwm_gpio_to_slice_num(HIGH_VOLTAGE_MOSFET_PIN); 262 | chan = pwm_gpio_to_channel(HIGH_VOLTAGE_MOSFET_PIN); 263 | gpio_set_function(17, GPIO_FUNC_PWM); 264 | pwm_set_freq_fixed_on_time(slice_num, chan, target_pwm_frequency); 265 | // Set up the PWM interrupt 266 | pwm_clear_irq(slice_num); // Clear any pending interrupts 267 | pwm_set_irq_enabled(slice_num, true); // Enable PWM slice interrupts 268 | irq_set_exclusive_handler(PWM_IRQ_WRAP, pwm_isr); // Set the ISR for the PWM interrupt 269 | irq_set_enabled(PWM_IRQ_WRAP, true); // Enable the global interrupt 270 | pwm_set_enabled(slice_num, true); 271 | } 272 | 273 | //This function initializes GPIO 274 | void init_gpio() 275 | { 276 | gpio_init(USER_LED_PIN); 277 | gpio_set_dir(USER_LED_PIN, GPIO_OUT); 278 | gpio_init(P12); 279 | gpio_set_dir(P12, GPIO_OUT); 280 | gpio_init(SHORT_ALERT_MOSFET_PIN); 281 | gpio_set_dir(SHORT_ALERT_MOSFET_PIN, GPIO_OUT); 282 | gpio_init(23); 283 | gpio_set_dir(23, GPIO_OUT); 284 | gpio_put(23, 1); // Put VR into FWM mode for better voltage stability 285 | } 286 | 287 | //This function sends the updates to the GUI 288 | void send_status_update() 289 | { 290 | char data[DATA_LENGTH]; 291 | 292 | // Format the data into the custom delimited string 293 | snprintf(data, DATA_LENGTH, "spark%%=%d,short%%=%d,avgPower=%.2f,avgCharge=%.2f,pulseFreq=%d,maxCoulomb=%.0f,resistorTemp=%.0f,mosfetTemp=%.0f\n", 294 | spark_percent, short_percent, avg_power, avg_charge, current_pwm_frequency, max_micro_coulomb_per_pulse, power_resistor_temperature, mosfet_temperature); 295 | 296 | // Send the data over USB serial 297 | tud_cdc_write(data, strlen(data)); 298 | tud_cdc_write_flush(); 299 | } 300 | 301 | //This function handles all the things we need to do after a pwm wrap, that we dont want to take up time in the ISR doing. 302 | void post_pwm_wrap_ops() 303 | { 304 | double filter_old_weight = ((double)current_pwm_frequency - 1.0) / (double)current_pwm_frequency; 305 | double filter_new_weight = 1 / (double)current_pwm_frequency; 306 | pulse_counter++; 307 | filter_old_weight = ((double)current_pwm_frequency - 1) / (double)current_pwm_frequency; 308 | filter_new_weight = 1 / (double)current_pwm_frequency; 309 | if (micro_coulomb_per_pulse > 200) 310 | { 311 | if (micro_coulomb_per_pulse > max_micro_coulomb_per_pulse) 312 | { 313 | short_counter++; 314 | } 315 | else 316 | { 317 | spark_counter++; 318 | 319 | } 320 | } 321 | pwm_wrap_int = false; 322 | avg_charge = ((avg_charge * filter_old_weight) + (micro_coulomb_per_pulse * filter_new_weight)); 323 | avg_power = ((avg_charge / 20) * 72) / 1000; 324 | voltage = get_temperature(2); 325 | if (pulse_counter >= current_pwm_frequency) 326 | { 327 | spark_percent = (spark_counter * 100 / pulse_counter); 328 | short_percent = (short_counter * 100 / pulse_counter); 329 | short_counter = 0; 330 | spark_counter = 0; 331 | pulse_counter = 0; 332 | } 333 | } 334 | 335 | //This function handles when serial data is received from the GUI 336 | void handle_received_data(const char *data_string) { 337 | char modifiable_string[DATA_LENGTH]; 338 | strncpy(modifiable_string, data_string, DATA_LENGTH - 1); 339 | modifiable_string[DATA_LENGTH - 1] = '\0'; // Ensure null termination 340 | // Parse the custom delimited format 341 | char *token = strtok(modifiable_string, ","); 342 | while (token != NULL) { 343 | if (strncmp(token, "pwm_frequency=", 14) == 0) { 344 | int received_freq = atoi(token + 14); 345 | target_pwm_frequency = received_freq; 346 | pwm_set_freq_fixed_on_time(slice_num, chan, target_pwm_frequency); 347 | char tmp_msg[128]; // Buffer to store the formatted message 348 | sprintf(tmp_msg, "PWM frequency set to: %d\n", target_pwm_frequency); 349 | send_message(tmp_msg); 350 | } else if (strncmp(token, "micro_c_per_pulse=", 18) == 0) { 351 | int received_coulomb = atoi(token + 18); 352 | max_micro_coulomb_per_pulse = received_coulomb; 353 | char tmp_msg[128]; // Buffer to store the formatted message 354 | sprintf(tmp_msg, "Max uC per Pulse set to: %.0f\n", max_micro_coulomb_per_pulse); 355 | send_message(tmp_msg); 356 | } 357 | token = strtok(NULL, ","); 358 | } 359 | } 360 | int64_t previousMillisReport = 0; 361 | int64_t currentMillis = 0; 362 | 363 | 364 | int main() 365 | { 366 | 367 | stdio_init_all(); 368 | tusb_init(); 369 | init_gpio(); 370 | setup_adc(); 371 | setup_dma(); 372 | sleep_ms(200); 373 | setup_pwm(); 374 | gpio_put(USER_LED_PIN, 1); 375 | gpio_put(SHORT_ALERT_MOSFET_PIN, 0); 376 | 377 | while (1) 378 | { 379 | currentMillis = to_us_since_boot(get_absolute_time()); 380 | if (pwm_wrap_int) 381 | { 382 | post_pwm_wrap_ops(); 383 | } 384 | 385 | // Check for received data from USB serial 386 | if (tud_cdc_available()) { 387 | char data_string[DATA_LENGTH]; 388 | int num_bytes = tud_cdc_read(data_string, sizeof(data_string) - 1); 389 | data_string[num_bytes] = '\0'; // Null-terminate the string 390 | handle_received_data(data_string); 391 | } 392 | //This if statement is ran every 200ms i.e 5hz 393 | if (currentMillis - previousMillisReport >= 200000) 394 | { 395 | // ** Check for a short ** 396 | if (short_percent > 25) 397 | { 398 | gpio_put(SHORT_ALERT_MOSFET_PIN, 1); 399 | } 400 | else 401 | { 402 | gpio_put(SHORT_ALERT_MOSFET_PIN, 0); 403 | } 404 | 405 | irq_set_enabled(PWM_IRQ_WRAP, false); // Disable interrupt so that the adc does not get interrupted. //FIXME this is not great as we will miss a couple of PWM pulses 406 | power_resistor_temperature = get_temperature(1); 407 | mosfet_temperature = get_temperature(2); 408 | irq_set_enabled(PWM_IRQ_WRAP, true); 409 | 410 | thermal_runaway_protection_check(); 411 | send_status_update(); // ** send status via USB serial 412 | previousMillisReport = to_us_since_boot(get_absolute_time()); 413 | } 414 | } 415 | } 416 | --------------------------------------------------------------------------------