<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Zain Ahmed]]></title><description><![CDATA[Portfolio / Project Logs]]></description><link>https://zahmed.me/</link><image><url>https://zahmed.me/favicon.png</url><title>Zain Ahmed</title><link>https://zahmed.me/</link></image><generator>Ghost 5.24</generator><lastBuildDate>Sun, 05 Apr 2026 00:09:01 GMT</lastBuildDate><atom:link href="https://zahmed.me/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[PDSupply USB-C PD Power Supply]]></title><description><![CDATA[<p>GitHub links: </p><ul><li><a href="https://github.com/zainahm3d/PDSupply-Solo-PCB">KiCad project</a></li><li><a href="https://github.com/zainahm3d/PDSupply-Solo-Firmware">NRF52 firmware</a></li><li><a href="https://github.com/zainahm3d/PDSupply-Solo-iOS">iOS core bluetooth application </a></li></ul><p>This is a project I worked on during June of 2020 which ended up being useful for my extracurricular projects (FSAE and my personal stuff). </p><p>The PDSupply is a USB-C Power Delivery capable adjustable power supply. The board is smaller</p>]]></description><link>https://zahmed.me/pdsupply/</link><guid isPermaLink="false">61f7251b531822064f5b86f6</guid><dc:creator><![CDATA[Zain Ahmed]]></dc:creator><pubDate>Wed, 09 Feb 2022 00:47:00 GMT</pubDate><media:content url="https://zahmed.me/content/images/2022/01/B54E7488-FE8F-4627-901A-8E09A140A51F_1_105_c.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://zahmed.me/content/images/2022/01/B54E7488-FE8F-4627-901A-8E09A140A51F_1_105_c.jpeg" alt="PDSupply USB-C PD Power Supply"><p>GitHub links: </p><ul><li><a href="https://github.com/zainahm3d/PDSupply-Solo-PCB">KiCad project</a></li><li><a href="https://github.com/zainahm3d/PDSupply-Solo-Firmware">NRF52 firmware</a></li><li><a href="https://github.com/zainahm3d/PDSupply-Solo-iOS">iOS core bluetooth application </a></li></ul><p>This is a project I worked on during June of 2020 which ended up being useful for my extracurricular projects (FSAE and my personal stuff). </p><p>The PDSupply is a USB-C Power Delivery capable adjustable power supply. The board is smaller than a credit card and can output anywhere from 1.2v to 12v @ 1A. &#xA0;It works by negotiating a higher voltage from the USB VBUS rail, and then bucking it down to a more precise voltage adjustable by the user. I designed the board with a current limiting IC, but this ended up not working correctly and had to be removed for the board to actually function. I&apos;ll be working on a redesign soon with that and a few other improvements. </p><h4 id="interface">Interface</h4><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/IMG_0073.PNG" class="kg-image" alt="PDSupply USB-C PD Power Supply" loading="lazy" width="2000" height="1397" srcset="https://zahmed.me/content/images/size/w600/2022/01/IMG_0073.PNG 600w, https://zahmed.me/content/images/size/w1000/2022/01/IMG_0073.PNG 1000w, https://zahmed.me/content/images/size/w1600/2022/01/IMG_0073.PNG 1600w, https://zahmed.me/content/images/2022/01/IMG_0073.PNG 2388w" sizes="(min-width: 720px) 720px"><figcaption>The PDSupply iPad/iPhone/Mac app.</figcaption></figure><p>One of my ideas for this project was to focus on keeping the board minimal and handing off all of the controls to a secondary device. In hindsight this was a cool technical challenge but not the best for real world usage. Once I actually started using this thing I realized it would benefit from at least an on/off button to avoid reaching for my phone so often. </p><p>I originally wrote the app for use with my iPhone and iPad Pro and was pleasantly surprised when I selected M1 Mac as a compilation target in Xcode and everything just worked. Xcode spat out a MacOS compatible app, complete with working BLE and a scalable UI. </p><h4 id="firmware">Firmware</h4><p>The PDSupply project was my first dive into the Nordic Semiconductor NRF52 and the NRF-SDK. After finding the right template project to branch off of (linked in the repo), it was easy to send a struct of data back and forth to an iPhone. </p><p>In general, the firmware does these things: </p><ul><li>Update the &quot;central&quot; device (iPhone) every x ms with the latests readings via BLE. </li><li>Read values from the central and use them to command the output voltage and current limits. </li><li>Continuously monitor output current and decide if the output relay should be on. </li><li>Manage USB-PD (when the STUSB4500 isn&apos;t running autonomously). </li><li>Manage the LEDs. </li></ul><pre><code class="language-c">// Supply to iPhone, 16 BYTES
struct SupplyData_struct {
	uint32_t counter;
	uint32_t status;
	float measuredVoltage;
	float measuredCurrent;
};

// iPhone to Supply, 16 BYTES
struct ControllerData_struct {
	uint32_t commandedStatus;
	uint32_t commandedOutput;
	float commandedVoltage;
	float commandedCurrent;
};</code></pre><p>With these structs, the rest of the firmware can be abstracted away from the Bluetooth bits and simply operate out of the data in these structs. The ControllerData_struct is sent on a timer, notifying the iPhone every time a new value is published.</p><pre><code class="language-c">static void notification_timeout_handler(void *p_context) {
  UNUSED_PARAMETER(p_context);
  ret_code_t err_code;

  SupplyData.counter++;
  memcpy(&amp;dataPacket, &amp;SupplyData, sizeof(SupplyData));

  err_code = ble_cus_custom_value_update(&amp;m_cus, (uint8_t *)&amp;dataPacket);
  APP_ERROR_CHECK(err_code);

  updateStatusLed(SupplyData);

  transferQueued = true;
}</code></pre><p> When the iPhone app is notified, it then adds the latest voltage and current measurements &#xA0;into a queue which is graphed for the user to see. This way we can keep a rolling window of old values to graph along with the latest. In the opposite direction, the iPhone simply sends its respective struct when something noteworthy happens, like the user changing the voltage requirement. </p><p>On the topic of software, it may be a good idea to have a watchdog of sorts - the iPhone sends a value to the supply every 100ms or so, and if the supply doesn&apos;t see a blip for 500ms it shuts off. For now that&apos;s not implemented. As it sits, the power supply won&apos;t even shut off when the phone is disconnected, which is convenient but may one day be very <em>not </em>convenient. </p><h4 id="corebluetooth-application">CoreBluetooth Application</h4><p>This was the most fun part of this project. After working with firmware, the in depth debugging tools and clean interface that iOS provides was a breath of fresh air. The main <a href="https://github.com/zainahm3d/PDSupply-Solo-iOS/blob/master/PDSupply/BluetoothController.swift">Bluetooth controller file </a>exposes a singleton that is used by the UI, and the file itself has a top to bottom life cycle that the CoreBluetooth API hooks into. I even added in little haptics when the supply is turned on or off. </p><pre><code class="language-swift">    public func commandSupply(commandedStatus: UInt32, commandedOutput: UInt32, commandedVoltage: Float32, commandedCurrent: Float32) {
        
        // Mutable values required for passing as pointers
        var _commandedStatus: UInt32 = commandedStatus
        var _commandedOutput: UInt32 = commandedOutput
        var _commandedVoltage: Float32 = commandedVoltage
        var _commandedCurrent: Float32 = commandedCurrent
        // Output data buffer
        var outputData: Array&lt;UInt8&gt; = Array(repeating: 0, count: 16)
        
        memcpy(&amp;outputData, &amp;_commandedStatus, 4)
        memcpy(&amp;outputData[4], &amp;_commandedOutput, 4)
        memcpy(&amp;outputData[8], &amp;_commandedVoltage, 4)
        memcpy(&amp;outputData[12], &amp;_commandedCurrent, 4)
        
        if (connected) {
            self.peripheral.writeValue(Data(outputData), for: dataCharacteristic, type: .withResponse)
            
            if (commandedStatus == PD_COMMAND_OUTPUT_ON) {
                BluetoothController.haptics.poweronHaptic()
            } else if (commandedStatus == PD_COMMAND_OUTPUT_OFF) {
                BluetoothController.haptics.poweroffHaptic()
            }
            
        }
    }</code></pre><p>Here we can see the simple <code>writeValue</code> function exposed by CoreBluetooth which is used to send any arbitrary data to a BLE compatible device. </p><pre><code class="language-swift">    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == CBManagerState.poweredOn {
            central.scanForPeripherals(withServices: nil, options: nil)
            print(&quot;scanning&quot;)
        } else {
            print(&quot;bluetooth not available&quot;)
        }
    }
    
    public func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        print(&quot;State updated&quot;)
    }
    
    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        if peripheral.name == pdsupplyName {
            self.peripheral = peripheral
            central.connect(peripheral, options: nil)
            central.stopScan()
            print(&quot;PDSupply Found!&quot;)
        }
    }
    
    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print(&quot;connected&quot;)
        peripheral.delegate = self
        self.peripheral.discoverServices([serviceUUID])
    }
    </code></pre><p>The whole API is functional and neatly segments code into steps of the BLE device life cycle from connection to disconnection. You just pop the code you want to run between the right brackets, and the OS will call it when the time is right. For example, in the above code we see that every time we discover a peripheral, the App is told to check the name of the device (not what you should use in production). Once we find the right device, the app will then interrogate it - checking for available services it exposes. It then finally finds the service with all of the power supply data that we set up on the firmware side, and subscribes to its notifications. </p><h4 id="hardware">Hardware</h4><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/Screen-Shot-2020-06-17-at-9.31.02-PM.png" class="kg-image" alt="PDSupply USB-C PD Power Supply" loading="lazy" width="2000" height="1421" srcset="https://zahmed.me/content/images/size/w600/2022/01/Screen-Shot-2020-06-17-at-9.31.02-PM.png 600w, https://zahmed.me/content/images/size/w1000/2022/01/Screen-Shot-2020-06-17-at-9.31.02-PM.png 1000w, https://zahmed.me/content/images/size/w1600/2022/01/Screen-Shot-2020-06-17-at-9.31.02-PM.png 1600w, https://zahmed.me/content/images/size/w2400/2022/01/Screen-Shot-2020-06-17-at-9.31.02-PM.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Ray-traced KiCad render.</figcaption></figure><p>The PCB is smaller than a credit and worked decently. There a number of things that I would change:</p><ul><li>This was my first time laying out a switcher (old project), so I chose the LTM8074 with an integrated inductor. This is a great part but severely limits the output compared to the 100W input, as it can only flow 1A @12V. </li><li>I used a chunky switcher with an external inductor for the 5V rail which powers some of the other components of the board, and in hindsight that was a bad decision. This is a better application for the 8074. </li><li>Current limiting didn&apos;t work, and the board only functions when the IC is removed. </li><li>I2C with the STUSB4500 was finicky due to some issue with assembly. </li><li>Switching noise was audible via the capacitor bank on the right side. This may be as simple as low quality caps, or something more. </li></ul><p>Apart from those issues, this board was capable of outputting 1.2-12V @ 1A, while getting fairly hot in the process. The STUSB4500 worked well in &quot;headless&quot; &#xA0;mode. I programmed it one time to favor 20V @ 5A from the source, backing down to 9V @ 2A if that was not available. Due to the finicky I2C comms, I was unable to control this on the fly with via the main NRF52 microcontroller. </p><p>I had the boards assembled by PCBWay for a total cost of about $400 for two boards. This wasn&apos;t the best deal but I figured it was worth it to give the boards the best chance of working. Unfortunately, both LTM8074s were soldered incorrectly with BGA balls shorted under the package (measurable via dmm). I was able to remove the parts with a hot air gun and replace them after cleaning the area to get both boards to work successfully. I know that these board houses don&apos;t typically love doing BGA parts, might be best to avoid them unless an x-ray step is specified.</p><h4 id="usability">Usability</h4><p>In closing, the PDSupply was good as a first attempt at a portable power supply. Although the current limiting didn&apos;t totally work as I wanted it to, I still use it often with somewhat proven devices that I know won&apos;t immediatly explode upon power application. When I do a redesign with working current limiting, more power capabilities, and working current limiting, I&apos;ll link to it here. </p><p>WIP of the new design: </p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://zahmed.me/content/images/2022/03/image-4.png" class="kg-image" alt="PDSupply USB-C PD Power Supply" loading="lazy" width="2000" height="1142" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-4.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-4.png 1000w, https://zahmed.me/content/images/size/w1600/2022/03/image-4.png 1600w, https://zahmed.me/content/images/size/w2400/2022/03/image-4.png 2400w" sizes="(min-width: 1200px) 1200px"></figure>]]></content:encoded></item><item><title><![CDATA[Long Range Wireless CAN Bus Bridge]]></title><description><![CDATA[<p>During my time on the WMU Formula SAE racing team, I started to learn the value of dumping nearly all data we could ever need onto the vehicle&apos;s CAN bus. This made firmware development super easy becuase any module could control or query data from any other module.</p>]]></description><link>https://zahmed.me/can-bus-bridge/</link><guid isPermaLink="false">61f736e6531822064f5b8790</guid><dc:creator><![CDATA[Zain Ahmed]]></dc:creator><pubDate>Tue, 01 Feb 2022 01:11:00 GMT</pubDate><media:content url="https://zahmed.me/content/images/2022/01/IMG_0513.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://zahmed.me/content/images/2022/01/IMG_0513.jpeg" alt="Long Range Wireless CAN Bus Bridge"><p>During my time on the WMU Formula SAE racing team, I started to learn the value of dumping nearly all data we could ever need onto the vehicle&apos;s CAN bus. This made firmware development super easy becuase any module could control or query data from any other module. Additionally, we could plug in a laptop with a USB to CAN tool and graph the data live with a tool I made that leverages Grafana. </p><p>I wanted to use this exact same tool while not directly connected to the car, so that we could monitor software states, coolant temps, exhaust gas temperatures, and tons of other data live. This way, we don&apos;t use two different tools when connected to the car over a CAN wire or over the wireless telemetry system. </p><p>To accomplish this, I designed a transciever board, transmitter firmware, receiver firmware, and a small helper library. </p><p>Links: </p><ul><li><a href="https://github.com/broncoracing/canlink-transmitter-firmware">canlink tx firmware</a></li><li><a href="https://github.com/broncoracing/canlink-receiver-firmware">canlink rx firmware</a></li><li><a href="https://github.com/zainahm3d/canSerializer">canSerializer library</a></li><li><a href="https://github.com/broncoracing/telemetry-module-board">Altium hardware files</a></li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/image-11.png" class="kg-image" alt="Long Range Wireless CAN Bus Bridge" loading="lazy" width="2000" height="858" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-11.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-11.png 1000w, https://zahmed.me/content/images/size/w1600/2022/03/image-11.png 1600w, https://zahmed.me/content/images/2022/03/image-11.png 2066w" sizes="(min-width: 720px) 720px"><figcaption>top side</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/image-10.png" class="kg-image" alt="Long Range Wireless CAN Bus Bridge" loading="lazy" width="2000" height="856" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-10.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-10.png 1000w, https://zahmed.me/content/images/size/w1600/2022/03/image-10.png 1600w, https://zahmed.me/content/images/2022/03/image-10.png 2164w" sizes="(min-width: 720px) 720px"><figcaption>bottom side</figcaption></figure><p>The hardware is based on a low frequency, high range 900Mhz XBEE radio transceiver. This module is stupid simple to use, and basically acts like a wireless UART cable. We managed to get over half a mile of range through trees and over the elevation changes of the WMU engineering campus with small and cheap 2.1dBi gain antennas on either end. The whole thing has 4 pins that connect to the car: CAN high, CAN low, +12V, and gnd. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/image-8.png" class="kg-image" alt="Long Range Wireless CAN Bus Bridge" loading="lazy" width="2000" height="1407" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-8.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-8.png 1000w, https://zahmed.me/content/images/size/w1600/2022/03/image-8.png 1600w, https://zahmed.me/content/images/2022/03/image-8.png 2288w" sizes="(min-width: 720px) 720px"><figcaption>Range test&#xA0;</figcaption></figure><p>This system proved to be more than reliable enough for our testing, as most of the lots we tested in were wide open, and had us posted near the center of the track to pull telemetry while the driver ran laps. </p><h3 id="firmware">Firmware</h3><p>For this project, there were a few challenges with the firmware. First, due to the limited bandwidth of the XBEEs, I wanted to only send a subset of the car&apos;s available CAN bus frames. Next, I needed to buffer CAN data in order to pack it all into one large binary string and send it in a single packet. This is due to the inter frame wait time that the XBEE requires, along with the relatively small max packet size of 256 bytes of the XBEE. </p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/image-12.png" class="kg-image" alt="Long Range Wireless CAN Bus Bridge" loading="lazy" width="2000" height="831" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-12.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-12.png 1000w, https://zahmed.me/content/images/size/w1600/2022/03/image-12.png 1600w, https://zahmed.me/content/images/size/w2400/2022/03/image-12.png 2400w" sizes="(min-width: 1200px) 1200px"><figcaption>testing to confirm the 256 byte packet maximum</figcaption></figure><p>For the above test, I set up a micro to continously send data to the transmitter, but we see on the receiver that the data is chunked into 256 byte packets with a 6ms inter frame delay. This means we can&apos;t just stream CAN data without any pre planning. </p><p>To deal with this, the transmitter firmware builds a map containing only the CAN IDs that I define as relevant. For code readability I chose to use a C++ map which requires dynamic memory allocation (I did not want to use a custom allocator here). This is frowned upon, but I was aware of the application&apos;s boundaries and knew this wouldn&apos;t lead to any issues, especially with the huge RAM size of the STM32F303RE we are using here. </p><pre><code class="language-cpp">// Takes an MBed style CAN message and updates the map if we care about the ID
void updateCanMap(CANMessage *msg) {
  CANFrame tempFrame = {0};

  // Don&apos;t add any new values that weren&apos;t in the map when we initialized it
  if (canData.find(uint32_t(msg-&gt;id)) != canData.end()) {
    // Copy data to serializable frame format
    if (msg-&gt;format == CANExtended) {
      tempFrame.extended = true;
    } else {
      tempFrame.extended = false;
    }

    tempFrame.id = msg-&gt;id;
    tempFrame.dlc = msg-&gt;len;
    memcpy(tempFrame.data, msg-&gt;data, msg-&gt;len);

    fillChecksum(&amp;tempFrame);
    canData[tempFrame.id] = tempFrame;
  }
}</code></pre><p>This way we can read from the map at any moment and know we have the latest data frame of each ID. This is especially important because each of the frames comes in at a random time, and at any frequency from 10hz to 100hz on this race car. </p><pre><code class="language-cpp">  while (1) {
    // if a message is available, log it.
    if (can.read(inmsg)) {
      updateCanMap(&amp;inmsg);
    }

    // time to transmit data
    if (timer.read_ms() &gt;= TRANSMIT_INTERVAL_MS) {
      timer.reset();

      // light LED while transmitting a packet
      led.write(1);

      // iterate through entire map
      uint8_t i = 0;
      for (const auto &amp;[key, value] : canData) {
        // copy each CANFrame struct to the output buffer
        memcpy(packetBuf + i, &amp;value, sizeof(value));
        // offset output buffer destination by the size of the frame we just
        // added
        i += sizeof(CANFrame);
      }
      xbee.write(packetBuf, sizeof(packetBuf));

      led.write(0);
    }
  }
}</code></pre><p>When it&apos;s time to transmit, we copy a bunch of CANFrame structs to the output buffer, then shoot it off with the XBEE. </p><p>To keep this code simple, I made a portable can serializer module used here. This contains a platform agnostic CANFrame struct and means to checksum and validate the data. The cpp file is small, and works well: </p><figure class="kg-card kg-code-card"><pre><code class="language-cpp">/**
 * @file canSerializer.c
 * @author Zain Ahmed
*/

#include &quot;canSerializer.h&quot;

// Calcuates a checksum
uint8_t calculateChecksum(CANFrame *pFrame)
{
    uint8_t computedChecksum = 0;                                           // will hold our checksum as we calculate
    uint8_t bytesToSum = sizeof(*pFrame) - sizeof(pFrame-&gt;checksum);        // do not include the checksum when calculating the checksum

    uint8_t *bytePtr = (uint8_t *)pFrame;                                   // get a pointer to struct so we can access individual bytes

    for (int i = 0; i &lt; bytesToSum; i++) {
      computedChecksum += *(bytePtr + i);                                   // dereference and sum each byte
    }

    return computedChecksum;
}

/**
 * @brief adds checksum and syncWord to packet. 
 * @param pFrame: Pointer to a CAN frame.
 * @note: Ensure all other data of frame is filled before calling fillChecksum().
*/
void fillChecksum(CANFrame *pFrame)
{
    pFrame-&gt;syncWord = 0xA55A;                                              // set the sync word before summing
    pFrame-&gt;checksum = calculateChecksum(pFrame);                           // append the checksum to the packet
}

/**
 * @param pFrame: Pointer to a CAN frame.
 * Returns true if the pFrame struct is valid, false if it is invalid.
*/
bool validateChecksum(CANFrame *pFrame)
{
    if (pFrame-&gt;checksum == calculateChecksum(pFrame))
    {
        return true;
    }
    else
    {
        return false;
    }
}</code></pre><figcaption>canSerializer</figcaption></figure><p>When receiving data, there is one tricky thing to deal with. When signal drops, or if the transmitter is power cycled, the tx and rx devices will no longer be synchronized to the start of a packet. This means that the receiver needs to iterate over every incoming byte until it finds a valid start of packet. Once it finds a start of packet, it will build a trial CANFrame, then calculate the checksum of this trial frame. If the checksum is good, we continue. If not, the loop will iterate one single byte forwards and try again. This way it never takes more than 1 dropped frame to recover from signal loss. </p><figure class="kg-card kg-code-card"><pre><code class="language-cpp">int main()
{
    initBoard();
    timer.start();
    xbee.attach(xbeeISR, SerialBase::RxIrq);                                // dynamically set receive interrupt for XBEE transceiver

    while (1)
    {
        can.read(inmsg);                                                    // throw away any incoming CAN frames

        if (xbeeQueue.size() &gt;= 512)                                        // if we fall this far behind there is an issue
        {
            xbeeQueue = {};                                                 // wipe the buffer and attempt to recover
        }

        if (xbeeQueue.size() &gt;= sizeof(CANFrame) * 2)                       // make sure we have enough data to build a packet
        {
            CANFrame tempFrame = {0};                                       // empty trial frame

            for (int i = 0; i &lt; sizeof(CANFrame) - 1; i++)
            {
                frameBuf[i] = frameBuf[i + 1];
            }

            frameBuf[sizeof(frameBuf) - 1] = xbeeQueue.front();             // pull the latest value off the queue
            xbeeQueue.pop();                                                // delete it from queue

            memcpy(&amp;tempFrame, frameBuf, sizeof(CANFrame));                 // build trial frame
            buildFrame(&amp;tempFrame);                                         // build output frame if the checksum is valid
        }

        if (timer.read_ms() &gt;= 500)                                         // we haven&apos;t gotten a frame in a while
        {
            led.write(0);
        }
        else
        {
            led.write(1);
        }
    }
}</code></pre><figcaption>Rx code</figcaption></figure><p>To keep things simple, the receiver chunks incoming bytes onto a queue as they are received, then operates on the data in the superloop. The queue is dynamically allocated, but I have a very conservative boundary (for this micro) of 512 bytes set which causes the whole queue to be wiped out. &#xA0;</p><h3 id="conclusion">Conclusion</h3><p>Starting this project was a little daunting for me as a college student with very little wireless experience. These XBEE modules made it super simple, and provided a nice GUI to set up the modules themselves. After a little bit of code, this system proved to be very useful at testing days. </p>]]></content:encoded></item><item><title><![CDATA[5 Channel Thermocouple to CAN Bridge]]></title><description><![CDATA[<p>This is a super simple device, with moderately bad routing. If I was to remake this board I would downsize the decoupling caps and place the main crystal closer to the MCU. I designed this board during college for my FSAE team, as we needed a way to simultaneously measure</p>]]></description><link>https://zahmed.me/tc-to-can/</link><guid isPermaLink="false">61f7364d531822064f5b8787</guid><dc:creator><![CDATA[Zain Ahmed]]></dc:creator><pubDate>Mon, 31 Jan 2022 01:09:48 GMT</pubDate><media:content url="https://zahmed.me/content/images/2022/01/IMG_1660.JPG" medium="image"/><content:encoded><![CDATA[<img src="https://zahmed.me/content/images/2022/01/IMG_1660.JPG" alt="5 Channel Thermocouple to CAN Bridge"><p>This is a super simple device, with moderately bad routing. If I was to remake this board I would downsize the decoupling caps and place the main crystal closer to the MCU. I designed this board during college for my FSAE team, as we needed a way to simultaneously measure 4x exhaust gas temperatures (EGTs in the car world) to ensure all 4 cylinders of our CBR600RR engine are burning in a similar manner. There&apos;s an additional 5th channel that is used to measure the average EGT at the collector where all four exhaust pipes converge. </p><p>The main component here is the MAX31855K thermocouple to SPI amplifier. This thing includes the ADC, cold junction compensation, and a SPI interface where I can just ask it nicely for the data. </p><figure class="kg-card kg-image-card"><img src="https://zahmed.me/content/images/2022/03/image-5.png" class="kg-image" alt="5 Channel Thermocouple to CAN Bridge" loading="lazy" width="1314" height="1402" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-5.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-5.png 1000w, https://zahmed.me/content/images/2022/03/image-5.png 1314w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/image-6.png" class="kg-image" alt="5 Channel Thermocouple to CAN Bridge" loading="lazy" width="940" height="1392" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-6.png 600w, https://zahmed.me/content/images/2022/03/image-6.png 940w" sizes="(min-width: 720px) 720px"><figcaption>Altium screenshot</figcaption></figure><p>All five amps are simply dropped onto the same SPI bus, and then connected to the STM32. Note there is no MOSI/COPI pin as the MAX31855K is a read only device. It only takes 14 bits to pull the hot side data from the 14 bit amplifier. </p><p>The STM32 acts as a bridge to the CAN bus, continuously polling the amplifiers and dumping the resulting data to the race car&apos;s CAN bus, to be picked up by the telemetry system located on the nose of the car. </p><p>Here is the code that reads the thermocouples and broadcasts the data: </p><pre><code class="language-c">// inside superloop
if (timer.read_ms() &gt;= CAN_MSG_PERIOD_MS) {
  readThermocouples();
  sendCANmessages();

  timer.reset();
  timer.start();
}

// Reads each thermocouple channel and populates output array
void readThermocouples() {
  for (int i = 0; i &lt; NUM_CHANNELS; i++) { // loop through all MAX31855K thermocouple amps

    // get value
    if (maxArray[i].ready()) {
      uint16_t tempBits = maxArray[i].read_temp();
      // MAX31855K indicates errors are values &gt; 2000
      if (tempBits &lt; 2000 &amp;&amp; tempBits &gt; 0) {
        // value is good
        tempArray[i] = ((tempBits * (9.0 / 5.0)) + 32.0); // convert value to F
        ledArray[i].write(1);
      } else {
        // value is bad
        // set breakpoints here to find issues
        switch (tempBits) {
          case MAX31855_ERR_NO_TC:
            ledArray[i].write(0); // turn off LED corresponding to thermocouple
            break;
          case MAX31855_ERR_SHORT_GND:
            ledArray[i].write(0);
            break;
          case MAX31855_ERR_SHORT_VCC:
            ledArray[i].write(0);
            break;
          default:
            ledArray[i].write(0);
            break;
        }
        // indicate error by setting value to 0
        tempArray[i] = 0;
      }
    }
  }
}

// Fill and send CAN messages, MSByte first
void sendCANmessages() {
  outMsgA.data[0] = (tempArray[0] &gt;&gt; 8) &amp; 0xFF;
  outMsgA.data[1] = tempArray[0] &amp; 0xFF;

  outMsgA.data[2] = (tempArray[1] &gt;&gt; 8) &amp; 0xFF;
  outMsgA.data[3] = tempArray[1] &amp; 0xFF;

  outMsgA.data[4] = (tempArray[2] &gt;&gt; 8) &amp; 0xFF;
  outMsgA.data[5] = tempArray[2] &amp; 0xFF;

  outMsgA.data[6] = (tempArray[3] &gt;&gt; 8) &amp; 0xFF;
  outMsgA.data[7] = tempArray[3] &amp; 0xFF;

  outMsgB.data[0] = (tempArray[4] &gt;&gt; 8) &amp; 0xFF;
  outMsgB.data[1] = tempArray[4] &amp; 0xFF;

  // If message is successfully pushed to CAN FIFO, toggle LED.
  // Note: this does not mean the message made it to the CAN bus
  if (can.write(outMsgA) &amp;&amp; can.write(outMsgB)) {
    txLED = !txLED;
  } else {
    txLED = 0;
  }
}</code></pre><p><code>sendCANmessages()</code> could be cleaned up to remove code repetition, but I wanted this code to be approachable by people new to CAN and found it easier to explain when it was all explicitly typed out. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/IMG_1878.JPG" class="kg-image" alt="5 Channel Thermocouple to CAN Bridge" loading="lazy" width="2000" height="2667" srcset="https://zahmed.me/content/images/size/w600/2022/03/IMG_1878.JPG 600w, https://zahmed.me/content/images/size/w1000/2022/03/IMG_1878.JPG 1000w, https://zahmed.me/content/images/size/w1600/2022/03/IMG_1878.JPG 1600w, https://zahmed.me/content/images/size/w2400/2022/03/IMG_1878.JPG 2400w" sizes="(min-width: 720px) 720px"><figcaption>I don&apos;t have any picures with thermocouples connected. Ignore the extra solder flux.&#xA0;</figcaption></figure><p>This board worked great and was used on multiple dyno days, and even pointed out some fueling issues that led to incomplete fuel burn, and therefore colder cylinders. We were able to modify the control map on the ECU to individually compensate on these cylinders to bring all four cylinders within a few degress of each other and fully burning their allotted fuel. </p>]]></content:encoded></item><item><title><![CDATA[IMU Tester Senior Design Project]]></title><description><![CDATA[<p>The total design report for this project spans 80 pages. I am using this quick post to share some parts of the project I found fun or interesting rather than regurgitating the design report. This project was originally presented at Western Michigan University&apos;s 2021 Senior Design Conference at</p>]]></description><link>https://zahmed.me/imu-tester/</link><guid isPermaLink="false">61f735f3531822064f5b877e</guid><dc:creator><![CDATA[Zain Ahmed]]></dc:creator><pubDate>Mon, 31 Jan 2022 01:06:54 GMT</pubDate><media:content url="https://zahmed.me/content/images/2022/01/A9994296-852A-4437-9285-293E49136678_1_105_c.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://zahmed.me/content/images/2022/01/A9994296-852A-4437-9285-293E49136678_1_105_c.jpeg" alt="IMU Tester Senior Design Project"><p>The total design report for this project spans 80 pages. I am using this quick post to share some parts of the project I found fun or interesting rather than regurgitating the design report. This project was originally presented at Western Michigan University&apos;s 2021 Senior Design Conference at the College of Engineering. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/page15image41823200.png" class="kg-image" alt="IMU Tester Senior Design Project" loading="lazy" width="1428" height="578" srcset="https://zahmed.me/content/images/size/w600/2022/03/page15image41823200.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/page15image41823200.png 1000w, https://zahmed.me/content/images/2022/03/page15image41823200.png 1428w" sizes="(min-width: 720px) 720px"><figcaption>The CAD with 3D printed mounting ring</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/IMG_0047.jpeg" class="kg-image" alt="IMU Tester Senior Design Project" loading="lazy" width="2000" height="1500" srcset="https://zahmed.me/content/images/size/w600/2022/03/IMG_0047.jpeg 600w, https://zahmed.me/content/images/size/w1000/2022/03/IMG_0047.jpeg 1000w, https://zahmed.me/content/images/size/w1600/2022/03/IMG_0047.jpeg 1600w, https://zahmed.me/content/images/size/w2400/2022/03/IMG_0047.jpeg 2400w" sizes="(min-width: 720px) 720px"><figcaption>Looks just like the CAD &#x1F60E;</figcaption></figure><p>While working as an intern at Stryker, an engineering partner and I designed hardware, firmware, and software to run a fully automated 6 axis IMU characterization and calibration rig. The rig consists of a high speed 3 axis rate table, a PCB housing the device under test, a dummy PCB acting as a transceiver, and a host laptop to control the rate table and log data to MATLAB. The project was kept just under the budget of $5000. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/image-1.png" class="kg-image" alt="IMU Tester Senior Design Project" loading="lazy" width="1686" height="1402" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-1.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-1.png 1000w, https://zahmed.me/content/images/size/w1600/2022/03/image-1.png 1600w, https://zahmed.me/content/images/2022/03/image-1.png 1686w" sizes="(min-width: 720px) 720px"><figcaption>Block diagram</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/image-2.png" class="kg-image" alt="IMU Tester Senior Design Project" loading="lazy" width="1960" height="1036" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-2.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-2.png 1000w, https://zahmed.me/content/images/size/w1600/2022/03/image-2.png 1600w, https://zahmed.me/content/images/2022/03/image-2.png 1960w" sizes="(min-width: 720px) 720px"><figcaption>Mentor PADS screenshot, internal layers hidden.</figcaption></figure><p>This system needed to log 6 axis data from an ADIS16470 IMU and single axis data from an ADXRS642 gyro + ADS1178 ADC at a 2Khz frame rate, along with some additional data for syncing to the motion system and ensuring data integrity. </p><pre><code class="language-c">// Burst read data format. Holds raw data for checksumming
// all values are in raw 16 bit 2&apos;s complement form. 
struct burstRead_s 
{
    uint16_t	diagStat;

    uint16_t	xGyroOut;
    uint16_t	yGyroOut;
    uint16_t	zGyroOut;

    uint16_t	xAcclOut;
    uint16_t	yAcclOut;
    uint16_t	zAcclOut;

    uint16_t	tempOut;

    uint16_t	dataCntr;

    uint16_t	checksum;

} __attribute__((packed)); // no padding</code></pre><p>To keep things simple, we packed this struct up at 2Khz and shot it off over the LVDS link to MATLAB. There, the struct was checked for integrity, unpacked, and plotted in the custom GUI. </p><p>Interfacing with the ADIS16470 micro was fun because we needed (ok we didn&apos;t actually <em>need </em>to) to do all of the SPI data transfer using the DMA controller. The goal was to write this firmware in a manner that could be used in a real product, so we needed to leave crucial CPU bandwidth available for other tasks. We were using the TI Tiva TM4C due to historical reasons at the company, and with this comes a limitation. The SPI peripheral only has a 16 byte hardware FIFO strapped to it, but we needed to read a 22 byte packet from the IMU during every frame. Normally, we could allow the SPI peripheral to trigger additional DMA transfers after the FIFO is freed, but in this case we were triggering the DMA controller off of the hardware &quot;data ready&quot; pin of the IMU, and the DMA can only have a single trigger at a time. </p><p>The way we dealt with this was to implement a delay <em>using the DMA</em>. This is done by adding a couple precisely timed dummy transfers to the DMA control table, burning some time while the first few SPI transfers complete. The icing on the cake is that the final step of the DMA transacation is the set the DMA controller up for the next transaction. The result? &#xA0;A 100% CPU free method of reading high speed data from our IMU. We could literally halt the CPU via a debugger and still be updating internal structs in RAM at full speed. It&apos;s my favorite chunk of embedded code I&apos;ve written so far.</p><p>Here&apos;s the DMA control table with a large block comment explaining the process. </p><pre><code class="language-c">/**
 *	@brief Perform one time setup for IMU SPI DMA control. Once called, DMA will continue transfers indefinitely. 
 * 	
 * 	@details
 * 	In order to perform all steps without CPU intervention, the DMA must execute delay cycles. 
 * 	This is because the SSI TX and SSI RX FIFO&apos;s are only 8 uint16&apos;s deep, and we need to send 11 u16s per frame. 
 * 	In order to solve this without CPU intervention, the following steps are executed in the task table: 
 * 
 * 	1. Fill SSI TX FIFO with 8 out of the 11 u16s
 * 	2. Dummy transfer in order to spin the DMA until the full 8 u16s are shifted out. 
 * 			-- while this occurs, the RX FIFO is filling
 * 	3. Read out the now filled SSI RX FIFO
 * 	4. Place the 3 remaining u16s in the SSI TX FIFO
 * 	5. Spin the DMA while the 3 u16s are shifted out.
 * 			-- while this occurs, the RX FIFO is filling
 *	6. Read out the partially filled (3/8 u16s) RX FIFO
 *			-- at this point the SPI transfers are complete, and memory should have IMU data in it. 
 *	7. Set dma.dataReady[0] to 1, to indicate that the transfer is complete. 
 *	   It is up to software to crunch the data and set it back to 0. 
 *	   This must be a PER (peripheral) based transfer. Otherwise the transfer repeats without the GPIO event. 
 *	8. Put this control table back into the DMA controller memory space. 
 *	9. Do a 1 byte dummy transfer. This tells the DMA controller that the scatter gather task list is complete because it a UDMA_MODE_AUTO transfer. 
 *	   Only the final transfer in the table can be UDMA_MODE_AUTO. 
 *	
 *	For steps 2 and 5 (dummy transfers) the number of transfers is tied to the number of bits to shift out and the SSI speed. 
 *	A good way to check your timing on this is to set a dma.dataReady[0] to 1 immediatly after spinning and do a pin toggle 
 *	in the superloop based on that value. Ensure the flip happens after relevant data is fully shifted out by the pin flip. 
 */
static tDMAControlTable taskTableSSI[] = {
    uDMATaskStructEntry(8, UDMA_SIZE_16,
                        UDMA_SRC_INC_16, dma.txBuff,
                        UDMA_DST_INC_NONE, (void *)(SSI0_BASE + SSI_O_DR),
                        UDMA_ARB_8, UDMA_MODE_MEM_SCATTER_GATHER),

    uDMATaskStructEntry(1000, UDMA_SIZE_8,
                        UDMA_SRC_INC_8, delayBuf,			
                        UDMA_DST_INC_NONE, delayBuf,
                        UDMA_ARB_8, UDMA_MODE_MEM_SCATTER_GATHER),

    uDMATaskStructEntry(750, UDMA_SIZE_8,
                        UDMA_SRC_INC_8, delayBuf,
                        UDMA_DST_INC_NONE, delayBuf,
                        UDMA_ARB_8, UDMA_MODE_MEM_SCATTER_GATHER),

    uDMATaskStructEntry(8, UDMA_SIZE_16,
                        UDMA_SRC_INC_NONE, (void *)(SSI0_BASE + SSI_O_DR),
                        UDMA_DST_INC_16, dma.rxGyroSensorData,
                        UDMA_ARB_8, UDMA_MODE_MEM_SCATTER_GATHER),

    uDMATaskStructEntry(3, UDMA_SIZE_16,
                        UDMA_SRC_INC_16, &amp;(dma.txBuff[8]),
                        UDMA_DST_INC_NONE, (void *)(SSI0_BASE + SSI_O_DR),
                        UDMA_ARB_4, UDMA_MODE_MEM_SCATTER_GATHER),

    uDMATaskStructEntry(800, UDMA_SIZE_8,
                        UDMA_SRC_INC_8, delayBuf,
                        UDMA_DST_INC_NONE, delayBuf,
                        UDMA_ARB_8, UDMA_MODE_MEM_SCATTER_GATHER),

    uDMATaskStructEntry(4, UDMA_SIZE_16,
                        UDMA_SRC_INC_NONE, (void *)(SSI0_BASE + SSI_O_DR),
                        UDMA_DST_INC_16, &amp;(dma.rxGyroSensorData[7]),
                        UDMA_ARB_4, UDMA_MODE_MEM_SCATTER_GATHER),

    uDMATaskStructEntry(1, UDMA_SIZE_8,
                        UDMA_SRC_INC_NONE, dma.dataReadyConst,
                        UDMA_DST_INC_NONE, dma.dataReady,
                        UDMA_ARB_1, UDMA_MODE_PER_SCATTER_GATHER),

    uDMATaskStructEntry(PRIMARY_CONTROL_STRUCT_SIZE, UDMA_SIZE_8,
                        UDMA_SRC_INC_8, dma.txCircularBuff,
                        UDMA_DST_INC_8, &amp;(controlTable[DMA_CH7_PRIM_CONTROL_OFFSET]),
                        UDMA_ARB_16, UDMA_MODE_MEM_SCATTER_GATHER),

    uDMATaskStructEntry(1, UDMA_SIZE_16,												
                        UDMA_SRC_INC_NONE, dma.txBuff,
                        UDMA_DST_INC_NONE, dma.rxDummyBuff,
                        UDMA_ARB_1, UDMA_MODE_AUTO)

};</code></pre><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/03/image-3.png" class="kg-image" alt="IMU Tester Senior Design Project" loading="lazy" width="2000" height="880" srcset="https://zahmed.me/content/images/size/w600/2022/03/image-3.png 600w, https://zahmed.me/content/images/size/w1000/2022/03/image-3.png 1000w, https://zahmed.me/content/images/size/w1600/2022/03/image-3.png 1600w, https://zahmed.me/content/images/size/w2400/2022/03/image-3.png 2400w" sizes="(min-width: 1200px) 1200px"><figcaption>A single comm frame</figcaption></figure><p>This communication happened without a hitch, running for days without any dropped frames. If this was something to be used in production, we&apos;d have had to think about what to do when a frame drops. Use the last good frame? Extrapolate what the next frame should be based on the last few frames? This ends up being quite the application specific problem. </p><p>In the end, we managed to deliver this project a couple weeks ahead of time and the live demos went well. The only casualty was one of the IMUs, which snapped off of the PCBA when the table was commanded to rotate a little farther than it should have. We promptly implemented some software end stops using the table&apos;s bundled software. </p>]]></content:encoded></item><item><title><![CDATA[USB-C Powered Dual Slot LiFeP04 18650 Charger]]></title><description><![CDATA[<p>See the files on <a href="https://github.com/zainahm3d/18650-charger">GitHub</a>.</p><p>A couple of years ago, I was gifted about 50 unused <a href="https://k2battery.com">K2 Energy</a> LiFeP04 cells, originally intented to be used in autoclavable surgical instruments. They were gifted to my university&apos;s solar car team, who were unable to use the cells due to their</p>]]></description><link>https://zahmed.me/18650-charger/</link><guid isPermaLink="false">61f608445c5a6e19d5c7eb1f</guid><dc:creator><![CDATA[Zain Ahmed]]></dc:creator><pubDate>Sun, 30 Jan 2022 03:38:55 GMT</pubDate><media:content url="https://zahmed.me/content/images/2022/01/full_board.png" medium="image"/><content:encoded><![CDATA[<img src="https://zahmed.me/content/images/2022/01/full_board.png" alt="USB-C Powered Dual Slot LiFeP04 18650 Charger"><p>See the files on <a href="https://github.com/zainahm3d/18650-charger">GitHub</a>.</p><p>A couple of years ago, I was gifted about 50 unused <a href="https://k2battery.com">K2 Energy</a> LiFeP04 cells, originally intented to be used in autoclavable surgical instruments. They were gifted to my university&apos;s solar car team, who were unable to use the cells due to their low-capacity-but-high-punch nature, exactly the opposite of what a long distance solar car needs. </p><p>I also have a bit of an obsession with converting nearly everything I own to USB-C, and wanted to build a charger using quality parts. The charger is based on 2x Linear Technology LTC4121 synchronous step down lithium battery charger ICs, with a maximum charge current of 400mA. Since we are charging two cells up to about 3.65v each, we should theoretically only ever see a maximum current of a little over 600mA at the 5v USB input. In reality, charging current drops hard after the battery reaches about 3.4v, so even 600mA is more than what we should ever see. </p><h4 id="schematic">Schematic</h4><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/image-6.png" class="kg-image" alt="USB-C Powered Dual Slot LiFeP04 18650 Charger" loading="lazy" width="1458" height="1190" srcset="https://zahmed.me/content/images/size/w600/2022/01/image-6.png 600w, https://zahmed.me/content/images/size/w1000/2022/01/image-6.png 1000w, https://zahmed.me/content/images/2022/01/image-6.png 1458w" sizes="(min-width: 1200px) 1200px"><figcaption>Main charging circuit.</figcaption></figure><p>This charger schematic is copied twice into the design, and is a perfect reflection of the typical non-MPPT application in the LTC4121 datasheet. Routing this board isn&apos;t super sensitive save for the main switching inductor and a few of the caps. It&apos;s worth keeping the feedback resistor group tightly placed and routed as well. The datasheet is very clear on routing specifics for this IC, and I followed the instructions closely to avoid any excess EMC issues (maybe one day I&apos;ll actually be able to measure this). </p><h3 id="routing">Routing</h3><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/image-10.png" class="kg-image" alt="USB-C Powered Dual Slot LiFeP04 18650 Charger" loading="lazy" width="1976" height="1408" srcset="https://zahmed.me/content/images/size/w600/2022/01/image-10.png 600w, https://zahmed.me/content/images/size/w1000/2022/01/image-10.png 1000w, https://zahmed.me/content/images/size/w1600/2022/01/image-10.png 1600w, https://zahmed.me/content/images/2022/01/image-10.png 1976w" sizes="(min-width: 1200px) 1200px"><figcaption>B side charging circuit.</figcaption></figure><p>Since I am only making a few of these I went with Coilcraft LPC4018 series shielded inductors. These have the benefit of being lower tolerance than cheaper alternatives, and also have a great package for pairing with this particular charging IC. The inductor pins match up nicely with the Linear Technology chip, making decent routing easy. </p><h4 id="testing">Testing</h4><p>In order to monitor the cell charge rate, I added some<a href="https://www.digikey.com/en/products/detail/te-connectivity-amp-connectors/RCU-0C/2366048"> clip on test points</a> from TE Connectivity. These made it super easy to clip in a Saleae logic analyzer to get a good sense of the charge curve. </p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/business_end.png" class="kg-image" alt="USB-C Powered Dual Slot LiFeP04 18650 Charger" loading="lazy" width="2000" height="1264" srcset="https://zahmed.me/content/images/size/w600/2022/01/business_end.png 600w, https://zahmed.me/content/images/size/w1000/2022/01/business_end.png 1000w, https://zahmed.me/content/images/size/w1600/2022/01/business_end.png 1600w, https://zahmed.me/content/images/size/w2400/2022/01/business_end.png 2400w" sizes="(min-width: 1200px) 1200px"><figcaption>The business end of the PCB, with the test points visible.</figcaption></figure><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/IMG_0535.jpeg" class="kg-image" alt="USB-C Powered Dual Slot LiFeP04 18650 Charger" loading="lazy" width="2000" height="1500" srcset="https://zahmed.me/content/images/size/w600/2022/01/IMG_0535.jpeg 600w, https://zahmed.me/content/images/size/w1000/2022/01/IMG_0535.jpeg 1000w, https://zahmed.me/content/images/size/w1600/2022/01/IMG_0535.jpeg 1600w, https://zahmed.me/content/images/size/w2400/2022/01/IMG_0535.jpeg 2400w" sizes="(min-width: 1200px) 1200px"><figcaption>Test points in use with Saleae&apos;s awesome clip on probes.</figcaption></figure><p>With the analyzer leads clipped on, I set the analog sample rate as low as possible, and left popped in a couple batteries. I was surprised to see just how rapidly the voltage flies from 2.4v to 3.2v here, and was also surprised at how slowly it continued on about 3.4v before the final jump to 3.6v (set by a couple resistors). It&apos;s pretty obvious that there isn&apos;t much real capacity in that last 0.2 or so volts. </p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/chargetest-1.png" class="kg-image" alt="USB-C Powered Dual Slot LiFeP04 18650 Charger" loading="lazy" width="2000" height="1744" srcset="https://zahmed.me/content/images/size/w600/2022/01/chargetest-1.png 600w, https://zahmed.me/content/images/size/w1000/2022/01/chargetest-1.png 1000w, https://zahmed.me/content/images/size/w1600/2022/01/chargetest-1.png 1600w, https://zahmed.me/content/images/2022/01/chargetest-1.png 2092w" sizes="(min-width: 1200px) 1200px"><figcaption>First test charge of two cells.</figcaption></figure><p>Looking closely at the above screenshot, we see some little drops in voltage every 28 seconds, on the dot. </p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/image-12.png" class="kg-image" alt="USB-C Powered Dual Slot LiFeP04 18650 Charger" loading="lazy" width="2000" height="1589" srcset="https://zahmed.me/content/images/size/w600/2022/01/image-12.png 600w, https://zahmed.me/content/images/size/w1000/2022/01/image-12.png 1000w, https://zahmed.me/content/images/size/w1600/2022/01/image-12.png 1600w, https://zahmed.me/content/images/2022/01/image-12.png 2000w" sizes="(min-width: 1200px) 1200px"><figcaption>Zoomed in real deep.</figcaption></figure><p>Each drop lasts exactly 36ms (verified by datasheet). As it turns out, this is when the charging IC unhooks itself to measure the open circuit voltage of the lithium cell. This way it is guaranteed to have a good measurement of the current battery voltage.</p><h3 id="issues">Issues</h3><p>So far, I have only found one issue with the board, which in hindsight was a lazy oversight. The datasheet specifies that if needed you can use a P-FET to prevent battery voltage from flowing back into the supply (USB port in this case). Of course, when designing the board I didn&apos;t think I needed this. As it turns out, Apple&apos;s (and maybe also some other) USB-C charger bricks check VBUS voltage before enabling power on the USB port. If the batteries are inserted into the charger before pluggin in the USB plug, Apple bricks will never enable power to the charger. I didn&apos;t have this problem with any of my other chargers (mostly RavPower banks and bricks). For now, I am inserting batteries after plugging in the USB port with Apple chargers, otherwise it works as designed. </p><h4 id="conclusion">Conclusion</h4><p>Apart from the laziness on missing a P-FET, the board works great and gets just warm enough to feel like it&apos;s working properly. It took me a bit before I felt safe letting this thing out of my eyesight while charging, but now it&apos;s a useful tool that lets me take advantage of these LiFePo4 cells. It was also a good intro to KiCad 6, which is leaps and bounds better than previous versions. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://zahmed.me/content/images/2022/01/66276802780__F7857B6E-E5B0-467C-B920-B33E342CBCD8-1.jpeg" class="kg-image" alt="USB-C Powered Dual Slot LiFeP04 18650 Charger" loading="lazy" width="2000" height="1175" srcset="https://zahmed.me/content/images/size/w600/2022/01/66276802780__F7857B6E-E5B0-467C-B920-B33E342CBCD8-1.jpeg 600w, https://zahmed.me/content/images/size/w1000/2022/01/66276802780__F7857B6E-E5B0-467C-B920-B33E342CBCD8-1.jpeg 1000w, https://zahmed.me/content/images/size/w1600/2022/01/66276802780__F7857B6E-E5B0-467C-B920-B33E342CBCD8-1.jpeg 1600w, https://zahmed.me/content/images/size/w2400/2022/01/66276802780__F7857B6E-E5B0-467C-B920-B33E342CBCD8-1.jpeg 2400w" sizes="(min-width: 720px) 720px"><figcaption>Some final proof that I actually made the damn thing.</figcaption></figure>]]></content:encoded></item></channel></rss>