Kevin Cuzner's Personal Blog

Electronics, Embedded Systems, and Software are my breakfast, lunch, and dinner.


A good workflow and build system with OpenSCAD and Makefiles

This is just a quick post, the blog isn't dead, I've just been busy. I've put together a reasonable OpenSCAD workflow that works for me and might work for others.

OpenSCAD is a 3D solid modeling program that performs computational geometry and is frequently used for describing models for 3D printing. It is a description language (and the program for compiling it) for solid shapes, procedurally generating them from primitive shapes like cubes, spheres, etc and mathematical operations such as unions and differences. Being fully text-based, it begs to live in version control and is usable through a CLI, leading to being able to be fully scripted. I started using OpenSCAD with the LED watch project and have since developed a fairly simple build system. It consists of two pieces:

  1. Special comments in a top-level OpenSCAD file designating which modules to build into STL files.
  2. A makefile

I'll start with the Makefile. It's really straightforward and identical for each of my projects:

 1SCAD=openscad
 2BASEFILE=<path to top-level>.scad
 3
 4TARGETS=$(shell sed '/^module [a-z0-9_-]*\(\).*make..\?me.*$$/!d;s/module //;s/().*/.stl/' $(BASEFILE))
 5
 6all: ${TARGETS}
 7
 8.SECONDARY: $(shell echo "${TARGETS}" | sed 's/\.stl/.scad/g')
 9
10include $(wildcard *.deps)
11
12%.scad: Makefile
13     printf 'use <$(BASEFILE)>\n$*();' > $@
14
15%.stl: %.scad
16     openscad -m make -o $@ -d $@.deps $<

In my top-level scad file, I then mark my top-level modules with a special comment:

1module Lid() { // `make` me
2  ...
3}

The way this works is pretty straightforward. Just run "make" with no arguments:

  1. The makefile scans the top-level and creates a list of the names of the modules to be built into stl files. The suffix ".scad" is appended to each module, forming a list the top-level "all" targets
  2. For each of these targets, it generates an scad file that includes the top-level file and instantiates the target module
  3. When invoking openscad to build the stl from the target's scad file, a makefile deps file is generated by openscad. This file is included in the makefile so that later, when a dependency of the particular module changes, the stl will be updated according to the Makefile logic.

Armed with an STL, we can now go ahead and print. Typically, I put a fully assembled version of the top-levels in the top-level instantiation (since that isn't instantiated when including the file in the target's scad file) and point OpenSCAD directly to the top-level when editing.

The only non-automated part of this which is hard to check into version control is slicing the model. However, that's typically dictated per-print (and varies with wear and tear on the printer, the filament or resin used, etc), so I'm less motivated to figure out a good way to track it.

I have several examples of this workflow on github:

AVR TPI Programming With usbasp For Dummies

There is an odd famine of information about this particular subject available in text form. If you google "usbasp tpi" and things like it, you'll find posts on the avr forums filled with incorrect information and schematics. This post is an attempt to remedy this.

I intend to show exactly the following, no more and no less:

  1. What is TPI?
  2. How to reprogram your usbasp to be capable of TPI.
  3. How to wire up a TPI-only microcontroller (such as the ATTiny20) to the usbasp's ISP interface.
  4. Why you might pick one of these newer devices, rather than sticking with the tried-and-true ISP AVRs.

The IoTree: An internet-connected tree

For this Christmas I decided to do something fun with my Christmas tree: Hook it up to the internet. Visit the IoTree here (available through the 1st week of January 2019):

http://kevincuzner.com/iotree

The complete source can be found here:

http://github.com/kcuzner/iotree

Christmas-Tree-in-Action-Cropped.png

The IoTree is an interface that allows anyone to control the pattern of lights shown on the small Christmas tree on my workbench. It consists of the following components:

  • My Kinetis KL26 breakout board (http://github.com/kcuzner/kl2-dev). This controls a string of 50 WS2811 LEDs which are zip-tied to the tree.
  • A Raspberry Pi (mid-2012 vintage) which parses pattern commands coming from the cloud into LED sequences which are downloaded to the KL26 over SPI. It also hosts a webcam and periodically throws the image back up to the cloud so that the masses can see the results of their labors.
  • A cloud server which hosts a Redis instance and python application to facilitate the user interface and communication down to the Raspberry Pi.

I'm going to go into brief detail about each of these pieces and some of the challenges I had with getting everything to work.

Bootloader for ARM Cortex-M0: No VTOR

In my most recent project I selected an ARM Cortex-M0 microcontroller (the STM32F042). I soon realized that there is a key architectural piece missing from the Cortex-M0 which the M0+ does not have: The vector table offset register (VTOR).

I want to talk about how I overcame the lack of a VTOR to write a USB bootloader which supports a semi-safe fallback mode. The source for this post can be found here (look in the "bootloader" folder):

https://github.com/kcuzner/midi-fader/tree/master/firmware

Building a USB bootloader for an STM32

As my final installment for the posts about my LED Wristwatch project I wanted to write about the self-programming bootloader I made for an STM32L052 and describe how it works. So far it has shown itself to be fairly robust and I haven't had to get out my STLink to reprogram the watch for quite some time.

The main object of this bootloader is to facilitate reprogramming of the device without requiring a external programmer. There are two ways that a microcontroller can accomplish this generally:

  1. Include a binary image in every compiled program that is copied into RAM and runs a bootloader program that allows for self-reprogramming.
  2. Reserve a section of flash for a bootloader that can reprogram the rest of flash.

Each of these ways has their pros and cons. Option 1 allows for the user program to use all available flash (aside from the blob size and bootstrapping code). It also might not require a relocatable interrupt vector table (something that some ARM Cortex microcontrollers lack). However, it also means that there is no recovery without using JTAG or SWD to reflash the microcontroller if you somehow mess up the switchover into the bootloader. Option 2 allows for a fairly fail-safe bootloader. The bootloader is always there, even if the user program is not working right. So long as the device provides a hardware method for entering bootloader mode, the device can always be recovered. However, Option 2 is difficult to update (you have to flash it with a special program that overwrites the bootloader), wastes unused space in the bootloader-reserved section, and also requires some features that not all microcontrollers have.

Because the STM32L052 has a large amount of flash (64K) and implements the vector-table-offset register (allowing the interrupt vector table to be relocated), I decided to go with Option 2. Example code for this post can be found here:

**https://github.com/kcuzner/led-watch**

Contents

Cross-platform driverless USB: The Human Interface Device

During my LED Wristwatch project, I decided early on that I wanted to do something different with the way my USB stuff was implemented. In the past, I have almost exclusively used libusb to talk to my devices in terms of raw bulk packets or raw setup requests. While this is ok, it isn't quite as easy to do once you cross out of the fruited plains of Linux-land into the barren desert of Windows. This project instead made the watch identify itself (enumerate) as a USB Human Interface Device (HID).

What I would like to do in this post is a step-by-step tutorial for modifying a USB device to enumerate as a human interface device. I'll start with an overview of HID, then move on to modifying the USB descriptors and setting up your device endpoints so that it sends reports, followed by a few notes on writing host software for Windows and Linux that communicates to devices using raw reports. With a little bit of work, you should be able to replace many things done exclusively with libusb with a cross-platform system that requires no drivers. Example code for this post can be found here:

**https://github.com/kcuzner/led-watch**

One thing to note is that since I'm using my LED Watch as an example, I'm going to be extending using my API, which I describe a little bit here. The main source code files for this can be found in common/src/usb.c and common/src/usb_hid.c.

Contents

Bare metal STM32: Writing a USB driver

A couple years ago I wrote a post about writing a bare metal USB driver for the Teensy 3.1, which uses Freescale Kinetis K20 microcontroller. Over the past couple years I've switched over to instead using the STM32 series of microcontrollers since they are cheaper to program the "right" way (the dirt-cheap STLink v2 enables that). I almost always prefer to use the microcontroller IC by itself, rather than building around a development kit since I find that to be much more interesting.

IMG_20170415_194157.jpg

One of my recent (or not so recent) projects was an LED Wristwatch which utilized an STM32L052. This microcontroller is optimized for low power, but contains a USB peripheral which I used for talking to the wristwatch from my PC, both for setting the time and for reflashing the firmware. This was one of my first hobby projects where I designed something without any prior breadboarding (beyond the battery charger circuit). The USB and such was all rather "cross your fingers and hope it works" and it just so happened to work without a problem.

In this post I'm going to only cover a small portion of what I learned from the USB portion of the watch. There will be a further followup on making the watch show up as a HID Device and writing a USB bootloader.

Example code for this post can be found here:

**https://github.com/kcuzner/led-watch**

(mainly in common/src/usb.c and common/include/usb.h)

My objective here is to walk quickly through the operation of the USB Peripheral, specifically the Packet Memory Area, then talk a bit about how the USB Peripheral does transfers, and move on to how I structured my code to abstract the USB packetizing logic away from the application.

Arranging components in a circle with Kicad

I've been using kicad for just about all of my designs for a little over 5 years now. It took a little bit of a learning curve, but I've really come to love it, especially with the improvements by CERN that came out in version 4. One of the greatest features, in my opinion, is the Python Scripting Console in the PCB editor (pcbnew). It gives (more or less) complete access to the design hierarchy so that things like footprints can be manipulated in a scripted fashion.

In my most recent design, the LED Watch, I used this to script myself a tool for arranging footprints in a circle. What I want to show today was how I did it and how to use it so that you can make your own scripting tools (or just arrange stuff in a circle).

*The python console can be found in pcbnew under Tools->Scripting Console. *

Step 1: Write the script

When writing a script for pcbnew, it is usually helpful to have some documentation. Some can be found here, though I mostly used "dir" a whole bunch and had it print me the structure of the various things once I found the points to hook in. The documentation is fairly spartan at this point, so that made things easier.

Here's my script:

 1#!/usr/bin/env python2
 2
 3# Random placement helpers because I'm tired of using spreadsheets for doing this
 4#
 5# Kevin Cuzner
 6
 7import math
 8from pcbnew import *
 9
10def place_circle(refdes, start_angle, center, radius, component_offset=0, hide_ref=True, lock=False):
11    """
12    Places components in a circle
13    refdes: List of component references
14    start_angle: Starting angle
15    center: Tuple of (x, y) mils of circle center
16    radius: Radius of the circle in mils
17    component_offset: Offset in degrees for each component to add to angle
18    hide_ref: Hides the reference if true, leaves it be if None
19    lock: Locks the footprint if true
20    """
21    pcb = GetBoard()
22    deg_per_idx = 360 / len(refdes)
23    for idx, rd in enumerate(refdes):
24        part = pcb.FindModuleByReference(rd)
25        angle = (deg_per_idx * idx + start_angle) % 360;
26        print "{0}: {1}".format(rd, angle)
27        xmils = center[0] + math.cos(math.radians(angle)) * radius
28        ymils = center[1] + math.sin(math.radians(angle)) * radius
29        part.SetPosition(wxPoint(FromMils(xmils), FromMils(ymils)))
30        part.SetOrientation(angle * -10)
31        if hide_ref is not None:
32            part.Reference().SetVisible(not hide_ref)
33    print "Placement finished. Press F11 to refresh."

There are several arguments to this function: a list of reference designators (["D1", "D2", "D3"] etc), the angle at which the first component should be placed, the position in mils for the center of the circle, and the radius of the circle in mils. Once the function is invoked, it will find all of the components indicated in the reference designator list and arrange them into the desired circle.

Step 2: Save the script

In order to make life easier, it is best if the script is saved somewhere that the pcbnew python interpreter knows where to look. I found a good location at "/usr/share/kicad/scripting/plugins", but the list of all paths that will be searched can be easily found by opening the python console and executing "import sys" followed by "print(sys.path)". Pick a path that makes sense and save your script there. I saved mine as "placement_helpers.py" since I intend to add more functions to it as situations require.

Step 3: Open your PCB and run the script

Before you can use the scripts on your footprints, they need to be imported. Make sure you execute the "Read Netlist" command before continuing.

The scripting console can be found under Tools->Scripting Console. Once it is opened you will see a standard python (2) command prompt. If you placed your script in a location where the Scripting Console will search, you should be able to do something like the following:

 1PyCrust 0.9.8 - KiCAD Python Shell
 2Python 2.7.13 (default, Feb 11 2017, 12:22:40)
 3[GCC 6.3.1 20170109] on linux2
 4Type "help", "copyright", "credits" or "license" for more information.
 5>>> import placement_helpers
 6>>> placement_helpers.place_circle(["D1", "D2"], 0, (500, 500), 1000)
 7D1: 0
 8D2: 180
 9Placement finished. Press F11 to refresh.
10>>>

Now, pcbnew may not recognize that your PCB has changed and enable the save button. You should do something like lay a trace or some other board modification so that you can save any changes the script made. I'm sure there's a way to trigger this in Python, but I haven't got around to trying it yet.

Conclusion

Hopefully this brief tutorial will either help you to place components in circles in Kicad/pcbnew or will help you to write your own scripts for easing PCB layout. Kicad can be a very capable tool and with its new expanded scripting functionality, the sky seems to be the limit.

The LED Wristwatch: A (more or less) completed project!

About 2009 I saw an article written by Dr. Paul Pounds in which he detailed a pocketwatch he had designed that fit inside a standard pocketwatch case and used LEDs as the dial. While the article has since disappeared, the youtube video remains. The wayback machine has a cached version of the page. Anyway, the idea has kind of stuck with me for a while and so a year or so ago I decided that I wanted to build a wristwatch inspired by that idea.

Although the project started out as an AVR project, I decided after my escapades with the STM32 in August that I really wanted to make it an STM32 project, so around November I started making a new design that used the STM32L052C8 ARM Cortex-M0+ ultra-low power USB microcontroller. The basic concept of the design is to mock up an analog watch face using a ring of LEDs for the hours, minutes, and seconds. I found three full rings to be expecting a bit much if I wanted to keep this small, so I ended up using two rings: One for the hours and another for combined minutes and seconds (the second hand is recognized by the fact that it is "moving" perceptibly).

In this post I'm going to go over my general design, some things I was happy with, and some things that I wasn't happy with. I'll make some follow-up posts for the following topics:

The complete design files can be found here:

https://github.com/kcuzner/led-watch

IMG_20170409_222521.jpg IMG_20170415_194157.jpg

Quick-n-dirty data acquisition with a Teensy 3.1

The Problem

I am working on a project that involves a Li-Ion battery charger. I've never built one of these circuits before and I wanted to test the battery over its entire charge-discharge cycle to make sure it wasn't going to burst into flame because I set some resistor wrong. The battery itself is very tiny (100mAH, 2.5mm thick) and is going to be powering an extremely low-power circuit, hopefully over the course of many weeks between charges.

Battery-Charger.jpg

After about 2 days of taking meter measurements every 6 hours or so to see what the voltage level had dropped to, I decided to try to automate this process. I had my trusty Teensy 3.1 lying around, so I thought that it should be pretty simple to turn it into a simple data logger, measuring the voltage at a very slow rate (maybe 1 measurement per 5 seconds). Thus was born the EZDAQ.

All code for this project is located in the repository at `https://github.com/kcuzner/ezdaq <https://github.com/kcuzner/ezdaq>`__

Setting up the Teensy 3.1 ADC

I've never used the ADC before on the Teensy 3.1. I don't use the Teensy Cores HAL/Arduino Library because I find it more fun to twiddle the bits and write the makefiles myself. Of course, this also means that I don't always get a project working within 30 minutes.

The ADC on the Teensy 3.1 (or the Kinetis MK20DX256) is capable of doing 16-bit conversions at 400-ish ksps. It is also quite complex and can do conversions in many different ways. It is one of the larger and more configurable peripherals on the device, probably rivaled only by the USB module. The module does not come pre-calibrated and requires a calibration cycle to be performed before its accuracy will match that specified in the datasheet. My initialization code is as follows:

 1//Enable ADC0 module
 2SIM_SCGC6 |= SIM_SCGC6_ADC0_MASK;
 3
 4//Set up conversion precision and clock speed for calibration
 5ADC0_CFG1 = ADC_CFG1_MODE(0x1) | ADC_CFG1_ADIV(0x1) | ADC_CFG1_ADICLK(0x3); //12 bit conversion, adc async clock, div by 2 (<3MHz)
 6ADC0_CFG2 = ADC_CFG2_ADACKEN_MASK; //enable async clock
 7
 8//Enable hardware averaging and set up for calibration
 9ADC0_SC3 = ADC_SC3_CAL_MASK | ADC_SC3_AVGS(0x3);
10while (ADC0_SC3 & ADC_SC3_CAL_MASK) { }
11if (ADC0_SC3 & ADC_SC3_CALF_MASK) //calibration failed. Quit now while we're ahead.
12    return;
13temp = ADC0_CLP0 + ADC0_CLP1 + ADC0_CLP2 + ADC0_CLP3 + ADC0_CLP4 + ADC0_CLPS;
14temp /= 2;
15temp |= 0x1000;
16ADC0_PG = temp;
17temp = ADC0_CLM0 + ADC0_CLM1 + ADC0_CLM2 + ADC0_CLM3 + ADC0_CLM4 + ADC0_CLMS;
18temp /= 2;
19temp |= 0x1000;
20ADC0_MG = temp;
21
22//Set clock speed for measurements (no division)
23ADC0_CFG1 = ADC_CFG1_MODE(0x1) | ADC_CFG1_ADICLK(0x3); //12 bit conversion, adc async clock, no divide

Following the recommendations in the datasheet, I selected a clock that would bring the ADC clock speed down to <4MHz and turned on hardware averaging before starting the calibration. The calibration is initiated by setting a flag in ADC0_SC3 and when completed, the calibration results will be found in the several ADC0_CL* registers. I'm not 100% certain how this calibration works, but I believe what it is doing is computing some values which will trim some value in the SAR logic (probably something in the internal DAC) in order to shift the converted values into spec.

One thing to note is that I did not end up using the 16-bit conversion capability. I was a little rushed and was put off by the fact that I could not get it to use the full 0-65535 dynamic range of a 16-bit result variable. It was more like 0-10000. This made figuring out my "volts-per-value" value a little difficult. However, the 12-bit mode gave me 0-4095 with no problems whatsoever. Perhaps I'll read a little further and figure out what is wrong with the way I was doing the 16-bit conversions, but for now 12 bits is more than sufficient. I'm just measuring some voltages.

Since I planned to measure the voltages coming off a Li-Ion battery, I needed to make sure I could handle the range of 3.0V-4.2V. Most of this is outside the Teensy's ADC range (max is 3.3V), so I had to make myself a resistor divider attenuator (with a parallel capacitor for added stability). It might have been better to use some sort of active circuit, but this is supposed to be a quick and dirty DAQ. I'll talk a little more about handling issues spawning from the use of this resistor divider in the section about the host software.

Quick and dirty USB device-side driver

For this project I used my device-side USB driver software that I wrote in this project. Since we are gathering data quite slowly, I figured that a simple control transfer should be enough to handle the requisite bandwidth.

 1static uint8_t tx_buffer[256];
 2
 3/**
 4 * Endpoint 0 setup handler
 5 */
 6static void usb_endp0_handle_setup(setup_t* packet)
 7{
 8    const descriptor_entry_t* entry;
 9    const uint8_t* data = NULL;
10    uint8_t data_length = 0;
11    uint32_t size = 0;
12    uint16_t *arryBuf = (uint16_t*)tx_buffer;
13    uint8_t i = 0;
14
15    switch(packet->wRequestAndType)
16    {
17...USB Protocol Stuff...
18    case 0x01c0: //get adc channel value (wIndex)
19        *((uint16_t*)tx_buffer) = adc_get_value(packet->wIndex);
20        data = tx_buffer;
21        data_length = 2;
22        break;
23    default:
24        goto stall;
25    }
26
27    //if we are sent here, we need to send some data
28    send:
29...Send Logic...
30
31    //if we make it here, we are not able to send data and have stalled
32    stall:
33...Stall logic...
34}

I added a control request (0x01) which uses the wIndex (not to be confused with the cleaning product) value to select a channel to read. The host software can now issue a vendor control request 0x01, setting the wIndex value accordingly, and get the raw value last read from a particular analog channel. In order to keep things easy, I labeled the analog channels using the same format as the standard Teensy 3.1 layout. Thus, wIndex 0 corresponds to A0, wIndex 1 corresponds to A1, and so forth. The adc_get_value function reads the last read ADC value for a particular channel. Sampling is done by the ADC continuously, so the USB read doesn't initiate a conversion or anything like that. It just reads what happened on the channel during the most recent conversion.

Host software

Since libusb is easy to use with Python, via PyUSB, I decided to write out the whole thing in Python. Originally I planned on some sort of fancy gui until I realized that it would far simpler just to output a CSV and use MATLAB or Excel to process the data. The software is simple enough that I can just put the entire thing here:

 1#!/usr/bin/env python3
 2
 3# Python Host for EZDAQ
 4# Kevin Cuzner
 5#
 6# Requires PyUSB
 7
 8import usb.core, usb.util
 9import argparse, time, struct
10
11idVendor = 0x16c0
12idProduct = 0x05dc
13sManufacturer = 'kevincuzner.com'
14sProduct = 'EZDAQ'
15
16VOLTS_PER = 3.3/4096 # 3.3V reference is being used
17
18def find_device():
19    for dev in usb.core.find(find_all=True, idVendor=idVendor, idProduct=idProduct):
20        if usb.util.get_string(dev, dev.iManufacturer) == sManufacturer and \
21                usb.util.get_string(dev, dev.iProduct) == sProduct:
22            return dev
23
24def get_value(dev, channel):
25    rt = usb.util.build_request_type(usb.util.CTRL_IN, usb.util.CTRL_TYPE_VENDOR, usb.util.CTRL_RECIPIENT_DEVICE)
26    raw_data = dev.ctrl_transfer(rt, 0x01, wIndex=channel, data_or_wLength=256)
27    data = struct.unpack('H', raw_data)
28    return data[0] * VOLTS_PER;
29
30def get_values(dev, channels):
31    return [get_value(dev, ch) for ch in channels]
32
33def main():
34    # Parse arguments
35    parser = argparse.ArgumentParser(description='EZDAQ host software writing values to stdout in CSV format')
36    parser.add_argument('-t', '--time', help='Set time between samples', type=float, default=0.5)
37    parser.add_argument('-a', '--attenuation', help='Set channel attentuation level', type=float, nargs=2, default=[], action='append', metavar=('CHANNEL', 'ATTENUATION'))
38    parser.add_argument('channels', help='Channel number to record', type=int, nargs='+', choices=range(0, 10))
39    args = parser.parse_args()
40
41    # Set up attentuation dictionary
42    att = args.attenuation if len(args.attenuation) else [[ch, 1] for ch in args.channels]
43    att = dict([(l[0], l[1]) for l in att])
44    for ch in args.channels:
45        if ch not in att:
46            att[ch] = 1
47
48    # Perform data logging
49    dev = find_device()
50    if dev is None:
51        raise ValueError('No EZDAQ Found')
52    dev.set_configuration()
53    print(','.join(['Time']+['Channel ' + str(ch) for ch in args.channels]))
54    while True:
55        values = get_values(dev, args.channels)
56        print(','.join([str(time.time())] + [str(v[1] * (1/att[v[0]])) for v in zip(args.channels, values)]))
57        time.sleep(args.time)
58
59if __name__ == '__main__':
60    main()

Basically, I just use the argparse module to take some command line inputs, find the device using PyUSB, and spit out the requested channel values in a CSV format to stdout every so often.

In addition to simply displaying the data, the program also processes the raw ADC values into some useful voltage values. I contemplated doing this on the device, but it was simpler to configure if I didn't have to reflash it every time I wanted to make an adjustment. One thing this lets me do is a sort of calibration using the "attenuation" values that I put into the host. The idea with these values is to compensate for a voltage divider in front of the analog input in order so that I can measure higher voltages, even though the Teensy 3.1 only supports voltages up to 3.3V.

For example, if I plugged my 50%-ish resistor divider on channel A0 into 3.3V, I would run the following command:

1$ ./ezdaq 0
2Time,Channel 0
31467771464.9665403,1.7990478515625
4...

We now have 1.799 for the "voltage" seen at the pin with an attenuation factor of 1. If we divide 1.799 by 3.3 we get 0.545 for our attenuation value. Now we run the following to get our newly calibrated value:

1$ ./ezdaq -a 0 0.545 0
2Time,Channel 0
31467771571.2447994,3.301005232
4...

This process highlights an issue with using standard resistors. Unless the resistors are precision resistors, the values will not ever really match up very well. I used 4 1meg resistors to make two voltage dividers. One of them had about a 46% division and the other was close to 48%. Sure, those seem close, but in this circuit I needed to be accurate to at least 50mV. The difference between 46% and 48% is enough to throw this off. So, when doing something like this with trying to derive an input voltage after using an imprecise voltage divider, some form of calibration is definitely needed.

Conclusion

Battery-Charger-with-EZDAQ.jpg

After hooking everything up and getting everything to run, it was fairly simple for me to take some two-channel measurements:

1$ ./ezdaq -t 5 -a 0 0.465 -a 1 0.477 0 1 > ~/Projects/AVR/the-project/test/charge.csv

This will dump the output of my program into the charge.csv file (which is measuring the charge cycle on the battery). I will get samples every 5 seconds. Later, I can use this data to make sure my circuit is working properly and observe its behavior over long periods of time. While crude, this quick and dirty DAQ solution works quite well for my purposes.