For the past year, I’ve been working with the MIT Media Lab Conformable Decoders group on ultrasound controlled drug delivery systems.

We’re looking at ways of using sonophoresis for small-molecule drug delivery. We were looking at cosmeceutical applications before, but we’ve pivoted since. I’ve worked on characterizing piezoelectric transducer behavior based on relative location, researching new sonophoresis applications, and now building the piezoelectric driver for the system itself for the past semester.

It’s been a really exciting ride so far. I’ve learned so much from my grad student mentors and Lab Director, and have become a better engineer because of them.

Building the piezodriver

There were a few phases of prototyping this driver. Our overall goal was to design a driver that we could program to sweep around the resonant frequencies of the piezos—this way, we can continuously update resonance and ensure that maximum efficiency of the drug delivery.

The circuit consists of 3 main parts: an H-bridge, an LC filter, and the piezo. To check resonance, we add a current sense resistor next to the piezo. Here’s the schematic I made on LTSpice:

ltspice schematic

Before we tested the circuit with the H-bridge, we wanted to confirm that the current sense resistor portion was working as expected. We breadboarded the differential op amp and RC LPF for testing with a power supply and scope first. I worked with another classmate on breadboarding and testing, asking our grad student mentor about any hardware bugs we were facing that we couldn’t solve ourselves.

testing the diff op amp

We ultimately saw expected behavior and went on to combine the differential op amp circuit with the H-bridge.

simulation with hbridge

The code

As we worked on the electronics, I would spend time outside the lab on the programming and controls portion with MPLAB X IDE and C.

After discussing with my grad student mentor on the structure of the code, we decided that the overall functionality would have two modes: scanning and pulsing.

In the scanning mode, we systematically test different frequencies around a center frequency (starting at 206kHz), take measurements of current at each frequency using ADC, scan frequencies in both directions (±10kHz from center by default, in 1kHz steps), then store current measurements in an array for analysis.

The pulsing mode activates after completing a scan to give time to update the resonant frequency. We turn the output on and off with 50ms intervals, set the Complementary Waveform Generator (CWG) to disable output, and stay for a defined period.

The basic operation flow:

  • System starts in SCANNING mode
  • Tests 21 different frequencies (10 below center, center, 10 above)
  • For each frequency: Sets NCO (Numerically Controlled Oscillator) to generate the test frequency, measure the current response, and store result in array
  • After collecting all measurements, I wrote a function called evalArray() to analyze the data to find the resonant peak
  • Updates the center frequency if a resonant peak is found
  • Switches to PULSING mode for the specified period
  • Cycle repeats

More on the evalArray() function: it looks for three possible patterns in the current measurements:

  • Peak in middle (increasing then decreasing)
  • Decreasing (peak at start)
  • Increasing (peak at end) to help track and maintain operation at the resonant frequency, which can shift over time due to temperature or other factors.
 /*
 * MAIN Generated Driver File
 * 
 * @file main.c
 * 
 * @defgroup main MAIN
 * 
 * @brief This is the generated driver implementation file for the MAIN driver.
 *
 * @version MAIN Driver Version 1.0.2
 *
 * @version Package Version: 3.1.2
*/

#include "mcc_generated_files/system/system.h"
#include "config.h"

/*
    Main application
*/

int currentScenario = SCANNING;  // Start with scanning for resonance

int main(void)
{
    SYSTEM_Initialize();

    // Enable the Global Interrupts 
    INTERRUPT_GlobalInterruptEnable(); 

    // Disable the Global Interrupts 
    //INTERRUPT_GlobalInterruptDisable(); 
    
    int modFreqCenter = 206; // In KHz, Starting Center Frequency
    int modFreq; // Holds current value of frequency being used
    int freqCount = 0; // Keeps track of which perturbed frequency we are at
    int ncoMultiplier = 20;
    uint32_t ncoFreq = (uint32_t)ncoMultiplier * (uint32_t)modFreqCenter * 1000;
    
    int scanPeriod = 3000; // Time between two scanning actions (100ms units)
    int freqBound = 10; // Number of frequencies to be scanned in one direction
    int freqStep = 1; // In KHz, Step size of frequency change
    int currentArray[21]; // Stores currents at the different frequencies

    int scanningTimer = 0; // Tell us time (100ms units) elapsed since last scan
    int samplePhase = 0; // Tells us which clock cycle of the NCO we want to sample on
    int interruptBypass = 0; // Helps bypass unwanted interrupts
    int adcSample = 0; // Initializing the ADC sample value
    
    uint32_t ncoInc = 0; // Increment value to set NCO frequency
    double ncoScaling = 0.02184531; // 1048575/48000000 = 0.0218453125 - rounded down
    double scaledFreq; 
    
    while(1)
    {
        if ((scanningTimer >= scanPeriod)){
            currentScenario = SCANNING;
            INTERRUPT_GlobalInterruptEnable(); // Enable the Global Interrupts 
            if (samplePhase >= ncoMultiplier){
                currentScenario = PULSING;
                currentArray[freqCount] = adcSample;
                if (freqCount < freqBound - 1){
                    modFreq = modFreqCenter - freqStep*(freqCount+1);
                }
                else if (freqCount == freqBound){
                    modFreq = modFreqCenter;
                }
                else if (freqCount > freqBound){
                    modFreq = modFreqCenter + freqStep*(freqCount+1);
                }
                
                ncoFreq = (uint32_t)modFreq * (uint32_t)ncoMultiplier * 1000; // Ensure multiplication happens in 32-bit space
                scaledFreq = (uint32_t)(2 * ncoFreq * ncoScaling);     // Perform scaling and ensure result is in 32-bit space
                ncoInc = (uint32_t)scaledFreq;                         // This assignment should now handle the result correctly
 
                // code to change frequency of pulsing
                CWG1AS0 = 0xBC;            // Disable CWG output
                NCO2CONbits.NCO2EN = 0;  
                // Set the NCO frequency registers
                NCO1INCL = (uint8_t)(ncoInc & 0xFF);        // Lower 8 bits
                NCO1INCH = (uint8_t)((ncoInc >> 8) & 0xFF); // Middle 8 bits
                NCO1INCU = (uint8_t)((ncoInc >> 16) & 0xF); // Upper 4 bits
                NCO2CONbits.NCO2EN = 1;
                CWG1AS0 = 0x3C;            // Enable CWG output
                freqCount = freqCount + 1;
                
                if(freqCount >= 2*freqBound){
                    // code to evaluate array and reset center frequency var
                    evalArray(currentArray, modFreqCenter, freqBound, freqStep);
                    freqCount = 0;
                    currentScenario = PULSING;
                    scanningTimer = 0; // Resetting to allow Pulsing to occur
                }
                else{
                    currentScenario = SCANNING;
                }
                adcSample = 0;
                samplePhase = 0;
            }
        }
        
        if (currentScenario == PULSING){
            // Disable the Global Interrupts 
            INTERRUPT_GlobalInterruptDisable(); 
            
            // Turn CWG ON
            CWG1AS0 = 0x3C;            // Enable CWG output
            __delay_ms(50);          // Wait for 1 second (adjust as needed)
            // Turn CWG OFF
            CWG1AS0 = 0xBC;            // Disable CWG output
            __delay_ms(50);          // Wait for 1 second (adjust as needed)
            scanningTimer = scanningTimer + 1;
        }
    }
}

void evalArray(int currentArray[], int modFreqCenter, int freqBound, int freqStep){
    // Track if we found a valid peak pattern
    int peakFound = 0;
    int newCenterFreq = modFreqCenter; // Default to current if no valid pattern found

    // First check for case 1: peak in middle (increasing then decreasing)
    for (int i = 1; i < 2*freqBound; i++) {
        // Check if this point is higher than previous
        if (currentArray[i] > currentArray[i-1]) {
            // Look for decreasing pattern after this potential peak
            int isPeak = 1;
            for (int j = i+1; j <= 2*freqBound; j++) {
                if (currentArray[j] >= currentArray[j-1]) {
                    isPeak = 0;
                    break;
                }
            }
            if (isPeak) {
                // Found peak pattern
                // Calculate corresponding frequency
                if (i < freqBound) {
                    newCenterFreq = modFreqCenter - freqStep*(freqBound-i);
                } else if (i == freqBound) {
                    newCenterFreq = modFreqCenter;
                } else {
                    newCenterFreq = modFreqCenter + freqStep*(i-freqBound);
                }
                peakFound = 1;
                break;
            }
        }
    }

    // If no peak found, check case 2: decreasing (peak at start)
    if (!peakFound) {
        int isDecreasing = 1;
        for (int i = 1; i <= 2*freqBound; i++) {
            if (currentArray[i] >= currentArray[i-1]) {
                isDecreasing = 0;
                break;
            }
        }
        if (isDecreasing) {
            newCenterFreq = modFreqCenter - freqStep*freqBound; // First value
            peakFound = 1;
        }
    }

    // If still no peak, check case 3: increasing (peak at end)
    if (!peakFound) {
        int isIncreasing = 1;
        for (int i = 1; i <= 2*freqBound; i++) {
            if (currentArray[i] <= currentArray[i-1]) {
                isIncreasing = 0;
                break;
            }
        }
        if (isIncreasing) {
            newCenterFreq = modFreqCenter + freqStep*freqBound; // Last value
            peakFound = 1;
        }
    }

    // Update center frequency if we found a valid pattern
    if (peakFound) {
        modFreqCenter = newCenterFreq;
    }
}