Monday, December 21, 2015

BeagleBone Access Point (and working around udhcpd)

Warning: it's easy to screw this stuff up and lose the ability to ssh into the beaglebone when you reboot.  Usually it was as simple as manually setting the IP address on my laptop, but you may not be so lucky.  You may not want to attempt this if you don't have a good handle on TCP/IP networking.

My Keebox W150NU seems to be doing a good job with a BeagleBone black as a wifi access point.  (I get about 3MB/s beaglebone -> laptop).  Beware that lots of other adapters (eg., Edimax and D-Link) work really poorly or not at all with the BeagleBone.

With a newer BeagleBone green, the W150NU was recognized out of the box, but on an older BBB with another adapter I had to update the kernel first:
Update kernel if your wifi adapter isn't detected (or if you just want to be up to date):
First I did 'sudo apt-get update ; sudo apt-get dist-upgrade'
Then I upgraded the kernel so it'd recognize the usb wifi adapter:
'cd /opt/scripts/tools ; git pull ; ./' 
On BeagleBone Green they tweaked a file to say "BBG" instead of "BBB", so I had to revert it with: 'cd /opt/scripts ; git checkout tools/eMMC/' then 'git pull' again before I could run the '' script.
Rebooting, the W150NU appeared as wifi2.

Next, I followed the instructions here to set up hostapd.  

First, 'sudo apt-get install dnsmasq hostapd'

Here's my /etc/hostapd/hostapd.conf (beware leading and trailing spaces, or hostapd.conf will refuse to start):

### Wireless network name ###
### Set your bridge name ###






# # Static WPA2 key configuration
# #1=wpa1, 2=wpa2, 3=both


## Key management algorithms ##
## Set cipher suites (encryption algorithms) ##
## TKIP = Temporal Key Integrity Protocol
## CCMP = AES in Counter mode with CBC-MAC
## Shared Key Authentication ##
## Accept all MAC address ###
#enables/disables broadcasting the ssid
# Needed for Windows clients

And don't forget to set this in /etc/defaults/hostapd:

I couldn't get dnsmasq or isc-dhcp-server to work consistently, though.  Turns out that 'netstat -nlp' showed udhcpd was binding to on port 67 (which is a bug, since it ignores the "interface" option), so the other dhcp servers can't start.

Hint: /var/log/daemon.log is where a lot of the error messages show up.

I fixed that with 'mv /usr/sbin/udhcpd /usr/sbin/udhcpd.disabled', although it would probably have been better to 'apt-get purge udhcpd'.

Here's my /etc/dnsmasq.conf:




And I also added this to /etc/network/interfaces:
auto wlan0
iface wlan0 inet static

That seems to do it, except that I have to "ifup wlan0" after startup on my BeagleBone Green.  The Black doesn't seem to need that for some reason I haven't figured out yet.

Getting BeagleBone to recognize wifi adapters by upgrading the kernel

My beaglebone black wasn't recognizing my wifi adapter.  apt-get update ; apt-get dist-upgrade didn't help, and I noticed that it wasn't upgrading the kernel.

Looks like the way to get kernel updates is to use /opt/scripts/tools/  When I first tried it, I got errors like "The certificate of `' is not trusted".

So the first step was to "git pull" down the latest version of the script, then run it.  Upon reboot, it recognized the wifi adapter.

Also note that beaglebone doesn't always do USB hotplug right, so I made sure to reboot after plugging in the adapter.

Also, even after updating the kernel, my Edimax and D-Link adapters show up but won't associate to an access point.  The Keebox W150NU seems to be working well, though.

Update: Even with the W150NU, I had trouble connecting to public networks.  I noticed this in dmesg: "deauthenticating from by local choice (reason=3)", which led me to a page recommending that I kill wpa_supplicant, and that fixed it.

Wednesday, December 16, 2015

BeagleBone Black/Green bus speeds

USB Host (big type A jack): 20MB/s writing to a Seagate USB3 2TB portable (spinning) hard disk (required plugging a 5V 4A power supply into the BeagleBone Black's power jack).  On BeagleBone Green, I got corruption with the Seagate disk, even when I powered the board from a bench supply.  With this Samsung 64GB USB flash drive I get 14-18MB/s write on both BeagleBone Green and Black.

Disk: 4.3MB/s writing to onboard flash, and 7.1MB/s to a SanDisk Ultra 64GB microSD card.  On BeagleBone Green, I get 9.4MB/s to onboard flash, and 6.8MB/s to the same SanDisk microSD card.

Network: Using netcat with the USB ethernet interface, I get 7.6MB/s upstream (to my laptop).  With the 100baseT jack I get 11.2MB/s upstream.  If I use ssh with its default cipher, I get about 10MB/s, but that goes back up to 11.1MB/s if I use "-c arcfour".

Compression: gzip -1 gives me 4.1MB/s on text generated by "cat /dev/urandom data | od -x".  I tried lz4 as well and it was almost exactly the same speed.

BeagleBone Black and Green microSD and onboard flash performance

Looks like I get about 4.3MB/sec when writing to the onboard flash on my BeagleBone Black, and about 7.1MB/sec when writing to a 64GB Sandisk Ultra 64GB microSD card.

On BeagleBone Green, I get 9.4MB/s to onboard flash, and 6.8MB/s to the same SanDisk microSD card.

I used this command to test:
$ time ( dd of=foo if=/dev/zero bs=1M count=100 ; sync )

Ignored dd's report, and divided 100 / elapsed time as reported by the time command.

Sunday, September 27, 2015

Closeups of a LED printer head

I picked up some printer heads for an Okidata LED printer and checked them out under the microscope.  

This ebay auction shows what the complete head looks like.  An LED printer is basically a laser printer, except that instead of scanning a laser beam across the page to make the toner stick to the page, an array of LEDs does the work.

Here's the lens assembly and LED array removed from the housing:

The lens assembly has two staggered rows of lenslets.  The head has some sort of tilt arrangement that I suspect they use to vibrate the lens assembly back and forth and then power the LEDs when the lenses are in the desired position.  (But don't quote me on that).

Putting the LED array under the microscope, we can see where the PCB is wire bonded to the LED driver circuitry.  Normally wirebonding is used inside a chip to go from the wafer to the pins, and then the whole chip is sealed up in plastic or ceramic.  But here the tiny gold wires are exposed, making them very easy to damage (which I did when removing the board from the head).

Below is a closeup.  The wires at the top are all going to a common trace on the upper part of the PCB.  The wires at the bottom are address/control lines going to the green/purple wafers.  At first I thought this was the LED array, but it's just the control circuitry.  That wafer is then wirebonded to the actual LED array, which just looks like a black line with dark gray squares between the top and middle rows of wires.

So you can see they had to run a wire for each and every LED in the array, and they're too densely packed to be able to run the wires to pads on the PCB, so instead they go wafer to wafer.  Then they just need to run control lines out to the PCB so it can tell the control wafer which LEDs to turn on.

Wednesday, September 16, 2015

BeagleBone maximum PWM frequency

Using a PWM channel to get square waves (don't care about duty cycle) from my BeagleBone, looks like I can get up to 50MHz with:

root@beaglebone:~# echo 10 > /sys/devices/ocp.3/pwm_test_P9_14.12/period
root@beaglebone:~# echo 5 > /sys/devices/ocp.3/pwm_test_P9_14.12/duty

Monday, September 14, 2015

Beaglebone PRU DDR memory access

Here's some C and PRU assembly code I wrote to see how fast the PRU can write to system (DDR) memory.

 // Loads a .bin file into a BeagleBone PRU and then interacts with it  
 // in shared PRU memory and (system-wide) DDR memory.  
 // Pass in the filename of the .bin file on the command line, eg:  
 // $ ./pru_loader foo.bin  
 // Compile with:  
 // gcc -std=gnu99 -o pru_loader pru_loader.c -lprussdrv  
 #include <unistd.h>  
 #include <stdio.h>  
 #include <inttypes.h>  
 #include <prussdrv.h>  
 #include <pruss_intc_mapping.h>  
 int main(int argc, char **argv) {  
  if (argc != 2) {  
   printf("Usage: %s pru_code.bin\n", argv[0]);  
   return 1;  
  // If this segfaults, make sure you're executing as root.  
  if (prussdrv_open(PRU_EVTOUT_0) == -1) {  
   printf("prussdrv_open() failed\n");  
   return 1;  
  tpruss_intc_initdata pruss_intc_initdata = PRUSS_INTC_INITDATA;  
  // Pointer into the 8KB of shared PRU DRAM   
  volatile void *shared_memory_void = NULL;  
  // Useful if we're storing data there in 4-byte chunks  
  volatile uint32_t *shared_memory = NULL;  
  prussdrv_map_prumem(PRUSS0_SHARED_DATARAM, (void **) &shared_memory_void);  
  shared_memory = (uint32_t *) shared_memory_void;  
  // Pointer into the DDR RAM mapped by the uio_pruss kernel module.  
  volatile void *shared_ddr = NULL;  
  prussdrv_map_extmem((void **) &shared_ddr);  
  unsigned int shared_ddr_len = prussdrv_extmem_size();  
  unsigned int physical_address = prussdrv_get_phys_addr((void *) shared_ddr);  
  printf("%u bytes of shared DDR available.\n Physical (PRU-side) address:%x\n",  
      shared_ddr_len, physical_address);  
  printf("Virtual (linux-side) address: %p\n\n", shared_ddr);  
  // We'll use the first 8 bytes of PRU memory to tell it where the  
  // shared segment of system memory is.  
  shared_memory[0] = physical_address;  
  shared_memory[1] = shared_ddr_len;  
  // Change to 0 to use PRU0  
  int which_pru = 1;  
  prussdrv_exec_program(which_pru, argv[1]);  
  for (int i = 0; i < 10; i++) {  
   // See if it's successfully writing the physical address of each word at  
   // the (virtual, from our viewpoint) address  
   printf("DDR[%d] is: %p / 0x%x\n", i, ((unsigned int *)shared_ddr) + i,   
       ((unsigned int *) shared_ddr)[i]);  
   int passes = shared_memory[0];  
   int bytes_written = passes * shared_ddr_len;  
   printf("Bytes written: %d\n", bytes_written);  
  // Wait for the PRU to let us know it's done  
  printf("All done\n");  
  return 0;  

And here's the assembly:
 .origin 0  
 .entrypoint TOP  
 #define DDR r29  
 #define DDR_SIZE r28  
 #define SHARED_RAM r27  
 #define SHARED_RAM_ADDRESS 0x10000  
  // Enable OCP master ports in SYSCFG register  
  LBCO r0, C4, 4, 4  
  CLR r0, r0, 4  
  SBCO r0, C4, 4, 4  
  // From shared RAM, grab the address of the shared DDR segment  
  // And the size of the segment from SHARED_RAM + 4  
  // BIGLOOP is one pass overwriting the shared DDR memory segment  
  mov r12, 0  
  mov r14, 10000  
  // Start at the beginning of the segment  
  MOV r10, DDR  
  ADD r11, DDR, DDR_SIZE  
  // Tight loop writing the physical address of each word into that word  
  SBBO r10, r10, 0, 4  
  ADD r10, r10, 4  
  // XXX: This means r10 < r11, opposite what I expected!  
  QBLT LOOP0, r11, r10  
  ADD r12, r12, 1  
  SBBO r12, SHARED_RAM, 0, 4  
  QBGT BIGLOOP, r12, r14  
  // Interrupt the host so it knows we're done  
  MOV r31.b0, 19 + 16  
 // Don't forget to halt!   

Here's the output I get, about 200MB/sec:

 262144 bytes of shared DDR available.  
  Physical (PRU-side) address:9e6c0000  
 Virtual (linux-side) address: 0xb6d78000  
 DDR[0] is: 0xb6d78000 / 0x9e6c0000  
 Bytes written: 200540160  
 DDR[1] is: 0xb6d78004 / 0x9e6c0004  
 Bytes written: 401342464  
 DDR[2] is: 0xb6d78008 / 0x9e6c0008  
 Bytes written: 601882624  
 DDR[3] is: 0xb6d7800c / 0x9e6c000c  
 Bytes written: 802160640  
 DDR[4] is: 0xb6d78010 / 0x9e6c0010  
 Bytes written: 1002176512  
 DDR[5] is: 0xb6d78014 / 0x9e6c0014  
 Bytes written: 1202454528  
 DDR[6] is: 0xb6d78018 / 0x9e6c0018  
 Bytes written: 1402470400  
 DDR[7] is: 0xb6d7801c / 0x9e6c001c  
 Bytes written: 1602748416  
 DDR[8] is: 0xb6d78020 / 0x9e6c0020  
 Bytes written: 1802764288  
 DDR[9] is: 0xb6d78024 / 0x9e6c0024  
 Bytes written: 2003042304  
 All done  

If I crank up the number of bytes written by SBBO from 4 to 8 (in the SBBO and ADD after LOOP0), then I think it ends up writing the contents of r10 and r11 into memory, and I get 320MB/sec.  If I crank it up to 16 bytes per write, I get 450MB/sec.

So the PRU really can write very quickly to system RAM.

Friday, September 11, 2015

Beaglebone PRU GPIO example

Executive Summary

If you're just trying to do ordinary GPIO on your beaglebone, this is not the page you're looking for.

This is about how to use certain GPIO pins on the beaglebone using the two embedded 200MHz PRU microcontrollers using their super-fast Enhanced GPIO mode.  The PRUs can also be used to access other GPIO pins, but not as quickly, and I don't cover that here.

Reading all the way through chapters 6 and 13 of Exploring BeagleBone was the best resource I found for understanding all this, but at the end of the day, here's what I had to do:

  1. sudo apt-get update && sudo apt-get dist-upgrade
  2. Create a device tree overlay, compile it, reboot, and enable it
  3. Assemble my PRU code (just 3 instructions!)
  4. Compile a tiny C program to send the code to the PRU.

One challenge is that most of the info out there is from 2013 or 2014, when you had to install PRUSS manually.  Fortunately, that stuff all came by default on my BeagleBone Green and BeagleBone Black.  So you don't have to worry about installing am335x_pru_package to get pasm and libprussdrv!

Turns out programming the PRU is the easy part.  The hard part is sorting out all the different ways of doing GPIO and getting the right mode enabled in the device tree.

Choosing the Right Pins (it's harder than you think)

This table shows which GPIO pins you can access from the PRUs using Enhanced GPIO (EGP).  The "BB Header" column shows you the physical header pin on the BeagleBone.  The R30 and R31 columns show you which pins you'll be writing or reading when you access that bit on those registers from the PRU0 or PRU1 microcontrollers.

But it's not the whole story -- if you  have a BeagleBone Black, all of the pins for PRU1 are used by the HDMI or onboard flash (emmc2).  So to use those pins, you have to disable HDMI or onboard flash (and thereafter boot from a microSD card) first, which requires editing the uEnv.txt file and rebooting.  (BeagleBone Green doesn't come with HDMI, freeing up those pins by default.)

These tables for headers P8 and P9 from Exploring BeagleBone highlight those reserved pins in red, and then show in the rightmost column that they're reserved for HDMI or emmc2.

Note from the first table that even though each PRU supports 16 EGP pins, the BeagleBone headers don't expose them all.  So don't expect to do fast 16-bit parallel I/O from a PRU on your BeagleBone.

Finally, it looks like pins 41 and 42 on P9 are yet another special case, and are overloaded with other GPIO pins somehow, and so you have to set those pins as inputs before using them from the PRU.  (I didn't try).

Let's avoid all those special cases and pick two pins that aren't already spoken for.  We'll use pin 11 on header P8 for output.  The tables show us that P8_11 correspond to PRU 0, register 30, bit 15.  In the PRUs, R30 is a "magic" register, and writing to it lets us set the state of output pins.

Let's also pick a pin for input, pin 16 on P8.  There the tables show us that P8_16 corresponds to PRU 0, register 31 bit 14.  R31 is the other "magic" PRU register, and reading from it reads input pins.

Get the Pin Configuration Right!

This is really easy to screw up.  The first thing we need to find is the multiplexer mode, which determines whether the pin will be hooked up to the PRU, HDMI port, etc.

The modes are nicely laid out as 7 columns in the P8 and P9 tables from the book.  At first you might think that Mode6 is "PRU input" mode, since there are lots of green cells like "pru_pruX_r31_XX", and r31 is the magic PRU input register.  But P8_11 and P8_12 break this rule.  So it's better to assume the pinmux modes were assigned at random, and check carefully in the tables.  I'm glad I printed them out, since I've had to stare at them a lot.

We want P8_11 to be our output pin, because the P8 table doesn't list it as colliding with anything important, and because it's highlighted in green, showing that it can work with PRU EGP.  Its name in that cell is pr1_pru0_pru_r30_15.  "pru0" tells us it's for PRU0.  "r30" tells us it can be used as an output, since r30 is the magic output register.  And it's in the "mode6" column, so we know we need to set that pin to mode 6 if we want to use it from the PRU0's register 30.

We can also get that from the table, since the "Pinmux Mode" to the right of the R30(output) column is Mode_6.

For our input pin P8_16, when we follow the tables we see that it's also mode 6 in this case.

But as explained in the Exploring BeagleBone book (and less clearly in table 9-60 of the TRM), we're not done yet!  There are 4 other settings that can apply to each pin:

Bits 0..2 are the multiplexer mode (these two pins are both 6)
Bit 3 enables (0) or disables (1) the internal pullup/pulldown resistor
Bit 4 is 0 for pulldown, 1 for pullup
Bit 5 is 0 to disable input, 1 to enable input.
Bit 6 is 1 if you want slow rise/fall times (for long i2c buses)

So for our output pin, P8_11, our configuration value is just 0x6, since none of the other bits are set.

But for our input pin P8_16, we need to turn on bit 5, so the value is 0x26  (10000 binary ORed with 110 binary).

Device Tree Overlays (there is NO escape!)

I looked all over, and there doesn't seem to be a way around creating and editing device tree overlays.

Now that we've picked what pins we want to use with the PRU, we have to use the device tree to enable the PRUs and put those pins into the right mode.

Here's a DTS file that sets up P8_11 for output via PRU EGP, and P8_16 for input via PRU EGP.  It's much shorter than it looks because I added a lot of comments:

 // This DTS overlay sets up one input and one output pin for use by  
 // PRU0 via its Enhanced GPIO mode, which will let us access those pins  
 // by writing to R30 bit 15 or reading from R31 bit 14.  
 // Save this file wherever you want (but I recommend /lib/firmware), as  
 // "PRU-GPIO-EXAMPLE-00A0.dts".
 // Compile with:
 // dtc -O dtb -I dts -o /lib/firmware/PRU-GPIO-EXAMPLE-00A0.dtbo -b 0 -@ PRU-GPIO-EXAMPLE-00A0.dts  
 // You'll have to reboot, after which you can do this as root to activate it:  
 // echo PRU-GPIO-EXAMPLE > /sys/devices/bone_capemgr.?/slots  
 / {  
   // This determines which boards can use this DTS overlay  
   compatible = "ti,beaglebone", "ti,beaglebone-green", "ti,beaglebone-black";  
   // I think part-number is supposed to correspond with the filename,  
   // so we'd save this as "PRU-GPIO-EXAMPLE-00A0.dts".  
   part-number = "PRU-GPIO-EXAMPLE";  
   // This always seems to be 00A0, and all the .dtbo files in /lib/firmware  
   // seem to be named foo-00A0.dtbo, but then are loaded without that suffix.  So
   // for foo-00A0.dtbo we'd do 'echo foo > /sys/devices/bone_capemgr.?/slots'
   version = "00A0";
   // List the pins and resources we'll be using. This table:  
   // shows which pins can be used with PRU0 and PRU1 for input and output via  
   // registers R31 and R30.  
   // Our output pin, P8_11, corresponds to PRU 0, register 30, bit 15  
   // Our input pin, P8_16, corresponds to PRU 0, register 31, bit 14  
   // Beware: Many other PRU EGP pins are reserved by HDMI or onboard flash, which  
   // would need to be disabled first by editing uEnv.txt and rebooting.  
   exclusive-use =  
      "P8.11", "P8.16", "pru0";  
   fragment@0 {  
    target = <&am33xx_pinmux>;  
    __overlay__ {  
      example_pins: pinmux_pru_pru_pins {  
       // The offset and mode for pins P8_11 and P8_16 also come from the table linked above.
       // That table gives offset 0x34 for P8_11, and 0x38 for P8_16.
       // It also shows us we want pinmux mode 6 for P8_11 in output mode,  
       // and again pinmux mode 6 for P8_16 in input mode.
       // Table 9-60 in the TRM:  
       // helps us calculate the rest of the configuration value.  
       // For P8_11, the other fields are all 0, so the value is just 0x06.  
       // For P8_16, we want it to be an input, so we also set bit 5, yielding  
       // a value of 0x26. We could also set bits 3 and 4 to enable a pullup  
       // or pulldown.  
       pinctrl-single,pins = <  
         0x34 0x06  
         0x38 0x26  
   // This enables the PRU and assigns the GPIO pins to it for use in EGP mode.  
   fragment@1 {  
    target = <&pruss>;  
    __overlay__ {  
      status = "okay";  
      pinctrl-names = "default";  
      pinctrl-0 = <&example_pins>;  

After saving that to /lib/firmware/PRU-GPIO-EXAMPLE-00A0.dts, I compiled it:

 root@beaglebone:/lib/firmware# dtc -O dtb -I dts -o /lib/firmware/PRU-GPIO-EXAMPLE-00A0.dtbo -b 0 -@ PRU-GPIO-EXAMPLE-00A0.dts  

And then I had to reboot before the system would let me load it.  The "PRU-GPIO-EXAMPLE" and the  "L" in "P-O-L" shows me that the overlay loaded successfully.

 root@beaglebone:/lib/firmware# echo PRU-GPIO-EXAMPLE > /sys/devices/bone_capemgr.?/slots  
 root@beaglebone:/lib/firmware# cat /sys/devices/bone_capemgr.?/slots  
  0: 54:PF---   
  1: 55:PF---   
  2: 56:PF---   
  3: 57:PF---   
  4: ff:P-O-L Bone-LT-eMMC-2G,00A0,Texas Instrument,BB-BONE-EMMC-2G  
  5: ff:P-O-L Override Board Name,00A0,Override Manuf,BB-UART2  
  6: ff:P-O-L Override Board Name,00A0,Override Manuf,PRU-GPIO-EXAMPLE  

Writing Code (finally!)

Now that the hard part's done, we can write some assembly code for the PRU and some C code to load it.  Here's the C code that runs on the main CPU and lets us load an arbitrary PRU .bin file into PRU 0:

 // Loads an arbitrary .bin file into PRU0 and waits for it to signal  
 // that it has finished.  
 // Pass in the filename of the .bin file on the command line, eg:  
 // $ ./pru_loader foo.bin  
 // Compile with:  
 // gcc -o pru_loader pru_loader.c -lprussdrv  
 #include <stdio.h>  
 #include <prussdrv.h>  
 #include <pruss_intc_mapping.h>  
 int main(int argc, char **argv) {  
  if (argc != 2) {  
   printf("Usage: %s pru_code.bin\n", argv[0]);  
   return 1;  
  // If this segfaults, make sure you're executing as root.  
  if (prussdrv_open(PRU_EVTOUT_0) == -1) {  
   printf("prussdrv_open() failed\n");  
   return 1;  
  tpruss_intc_initdata pruss_intc_initdata = PRUSS_INTC_INITDATA;  
  // Change to 1 to use PRU1  
  int which_pru = 0;  
  printf("Executing program and waiting for termination\n");  
  prussdrv_exec_program(which_pru, argv[1]);  
  // Wait for the PRU to let us know it's done  
  printf("All done\n");  
  return 0;  

And here, finally, is the assembly code that runs on the PRU.  "set r30, r30, 15" is all it takes to turn on our pin, and "clr r30, r30, 15" is all it takes to shut it off!

Here's a lovely PRU instruction set quick reference, also from the book.  Print it out too.

 // Demonstrates using Enhanced GPIO (EGP), the fast way to  
 // do GPIO on certain pins with a PRU.  
 // Writing to r30 with PRU0 or PRU1 sets the pins given in this table:  
 // But only if the Pinmux Mode has been set correctly with a device  
 // tree overlay!  
 // Assemble with:  
 // pasm -b pru_egp_output.p  
 // Boilerplate  
 .origin 0  
 .entrypoint TOP  
  // Writing bit 15 in the magic PRU GPIO output register  
  // PRU0, register 30, bit 15 turns on pin 11 on BeagleBone  
  // header P8.  
  set r30, r30, 15  
  // Uncomment to turn the pin off instead.  
  //clr r30, r30, 15  
  // Interrupt the host so it knows we're done  
  mov r31.b0, 19 + 16  
 // Don't forget to halt or the PRU will keep executing and probably  
 // require rebooting the system before it'll work again!  

I assembled with "pasm -b pru_egp_output.p", loaded it with "sudo ./pru_loader pru_egp_output.bin", and verified with my voltmeter that P8_11 showed 3.3v.  Then I uncommented the clr, reassembled, re-ran and verified that it dropped to 0.  Success!

Now we're ready for the big leagues, 5 whole instructions to copy the value of the input pin to the output pin:

 // Demonstrates using Enhanced GPIO (EGP), the fast way to  
 // do GPIO on certain pins with a PRU.  
 // Writing to r30 or reading from r31 with PRU0 or PRU1 sets or reads the pins  
 // given in this table:  
 // But only if the Pinmux Mode has been set correctly with a device  
 // tree overlay!  
 // Assemble with:  
 // pasm -b pru_egp_io.p  
 // Boilerplate  
 .origin 0  
 .entrypoint TOP  
  // Reading bit 14 in the magic PRU GPIO input register 31  
  // bit 14 for PRU0 reads pin 16 on BeagleBone header P8.  
  // If the input bit is high, set the output bit high, and vice versa.  
  QBBS HIGH, r31, 14  
  QBBC LOW, r31, 14  
  // Writing bit 15 in the magic PRU GPIO output register  
  // register 30, bit 15 for PRU0 turns on pin 11 on BeagleBone  
  // header P8.  
  set r30, r30, 15  
  clr r30, r30, 15  
  // Interrupt the host so it knows we're done  
  mov r31.b0, 19 + 16  
 // Don't forget to halt or the PRU will keep executing and probably  
 // require rebooting the system before it'll work again!  

I tested it by installing a jumper from P9_01 (GND) or P9_03 (DC_3.3V) to P8_16.  Be sure not to connect to P9_05 or P9_06 though, since those are at 5V and could blow up your board!

Where to now?

If you're still with me, you probably have something fancier in mind than just turning a pin on or off.  Even though am335x_pru_package came installed on my BBB, the examples weren't there.  You can find them here on github:

In particular, PRU_memAccessPRUDataRam was super helpful.  It doesn't use any GPIO pins, so it only requires that the PRUs are enabled (which you get if you use my overlay above).  I was trying to get some assembly code to work, and couldn't get any signal back from the PRU to know if it wasn't working or if it just couldn't toggle any GPIO pins.  I had deleted that pesky "interrupt the host" instruction at the end of my listing above, so I was running the code totally blind.  When I discovered that PRU_memAccessPRUDataRam worked fine on a clean boot, but would no longer work after running my code, I quickly realized that I had forgotten to put a HALT instruction at the end of my code.

One other thing to explore is the new C compiler for the PRUs, PRUSS-C.  It's also installed by default on my beaglebones.  It looks pretty neat, but I haven't managed to get the code onto a PRU yet.  Something to do with .cmd scripts for hexpru, I think.

Finally, TI also has a GUI development suite for their processors.  I was tempted to try it, but they make you create a login to myTI first, and I'd rather use command line tools anyway.

Thursday, September 10, 2015

Getting Beaglebone black and green to work with Edimax, Keebox and D-Link wifi adapters

Wifi adapters: No luck out of the box with an Edimax EW-7811UN, Keebox W150NUIEEE, or dlink DWA-121.  The latter two were recognized by my Beaglebone Green, but wouldn't associate with my access point.  The Edimax showed up on my Beaglebone Black after an apt-get dist-upgrade, and associated once, but had tons of packet loss and never associated again.

Error message from the Keebox and D-Link usb wifi adapters:

# iwconfig wlan0 essid MyWifiName
Error for wireless request "Set ESSID" (8B1A) :
    SET failed on device wlan0 ; Operation not permitted.


# ifconfig wlan0 up

Then "iwconfig wlan0 essid MyWifiName" works.

I tried this after connecting via ethernet and running apt-get update ; apt-get dist-upgrade, so I'm not sure if it'd work out of the box.

Tuesday, September 08, 2015

Edimax (RTL8192CU) USB wifi adapter with Beaglebone Black

Out of the box, the stock debian distro for my Beaglebone Black didn't recognize my Edimax USB wifi adapter.  But after plugging it into wired ethernet and doing an apt-get update ; apt-get dist-upgrade, it shows up just fine.

I got it to associate with an access point once, but I got a lot of packet loss, and haven't gotten it to work since.  After an hour or two of fiddling, I'm just going to buy a different adapter.

Update: Looks like "ifconfig wlan0 up" gets it to associate with an AP.  See

Friday, September 04, 2015

Arduino Due: toggle open drain GPIO pin

I wanted a pin on my Arduino Due to switch between pulling down toward ground and turning off (going high impedance).

I tried using pinMode(x, INPUT);, but there's a bug in the arduino libraries that sets the output high when I switch back to pinMode(x, OUTPUT).  That's no good!

So I read up in the datasheet and found the register that enables open drain mode.  The pinout was also handy so I could see that pin 53 (according to Arduino) is port B, pin 14 according to the ARM chip.

Here's the code snippet.

 // Sketch for Arduino Due that enables open drain mode for pin B14  
 void setup() {  
  pinMode(53, OUTPUT);  
  digitalWrite(53, 0);  
  // Enable open drain mode on pin 14 of port B using the Multi-Driver Enable Register  
  REG_PIOB_MDER = 1 << 14;  
  // To switch it back to a normal output:  
  // REG_PIOB_MDDR = 1 << 14;  
 void loop() {  
  // Turn on B14 (should be about equivalent to digitalWrite(53, 1))  
  REG_PIOB_SODR = 1 << 14;  
  // Pause 1ms so it's easy to see on the scope  
  // Turn off B14  
  REG_PIOB_CODR = 1 << 14;  
  // Pause 2ms  

Wednesday, September 02, 2015

libfann bugs

Looking for a general purpose neural network library, fann seemed reasonable.  Jury's still out, but I see a number of shortcomings:

  • I see people warning against using its builtin input/output scaling.  So make sure your training set (inputs and outputs) is all scaled to [0,1]

  • fann_create_train(...) shows up in the docs but doesn't appear to actually exist (version 2.1.0)

  • I see no examples of fann_create_train_from_callback(), and the docs are unclear, but it looks like it allocates the memory itself and then calls the callback num_data times.  So the num_data value that gets passed to the callback is 1..n, and the two pointers point directly to the elements to be filled in.
  • #include <doublefann.h> with gcc -lfann caused data corruption for me, because the internal library calls had fann_type = float while my main program had fann_type = double.  So either use #include <fann.h> with gcc -lfann  or  #include <doublefann.h> with gcc -ldoublefann!

Saturday, June 13, 2015

Simple pump from a juice bottle

In some cars, the only way to add or check transmission fluid is to remove a hard-to-reach plug and add fluid until it starts pouring out the hole.  Since the hole is in the side of the transmission, there's no way to get a funnel into the hole.  You have to pump the fluid into the hole.

Auto parts stores will sell you a pump, but I discovered that all I needed was a water bottle and some tubing.  Drill two holes in the cap for the tubes, fill the bottle about halfway with fluid and force air into the short tube.  I was worried about air leaking between the hole and the tubing, but it came out just right, nice and tight.  It can still work without a perfect seal but it'll require more air.

This tubing was the perfect diameter to thread into the nozzle of the air gun on my compressor, but I think it would have worked just as well to simply blow into the short tube.  (I tried it with water just now and it seemed to work fine, but watch out for fumes and don't let any fluid come back up the short tube!)

The tubing shown here is for the ice maker in my fridge.

Thursday, May 28, 2015

Mary Meeker Internet Trends 2015

Slides of interest to me:

p.47: 6 of top 10 mobile apps are for messaging
p.68: Social network use in 12-24 year olds: Instagram 32%,Twitter 24%, Facebook 14% (down from 35% in 2013)
p.69: 78% of millennials spend >2h per day on smartphone
p.70: 44% of millennials use smartphone camera at least once / day
p.103: Americans receiving government benefits 50% in 2012 vs. 30% in 1983.
p.110: #2 top work value for millenials is "Flexible Working Hours"
p.113: Millenials seen as more narcissistic, open to change, creative than Gen X
p.117: USA Smartphone penetration 64%
p.132: 72% of NYC airbnb hosts depend on it for rent/mortgage
p.163: 7% of Xaomi phone buyers (China) buy a Xaomi home product
p.165: India starting to take off in Internet penetration (very cool chart)
p.169: India #2 in % of internet traffic via mobile
p.170: 41% of India e-commerce is via mobile

Monday, March 23, 2015

Steam Linux audio problems

No audio at all from steam games?  Try 'pavucontrol'.  It'll show you when games are trying to play audio and where they're playing it to. In the "configuration" tab I had to disable built-in audio and switch my "HDA NVidia" device to "Digital Surround 5.1 (HDMI) Output" from "Digital Stereo (HDMI) Output".

Sunday, March 22, 2015

GQL to CSV exporter for Google App Engine

The App Engine admin panel will let you run GQL queries against your datastore, but it won't let you download the results as a CSV. So I wrote a [very] quick and [very] dirty handler that does, which also has the handy side effect that you can put your own access controls on it. That means you can give someone on your team read-only access to your datastore without also giving them access to the admin panel.

If you're using db instead of ndb,  I believe the main thing to change is ndb.gql() to GqlQuery().

 # Very quick and dirty example of how to provide unfettered read  
 # access to your datastore with export to CSV. Be sure to add appropriate  
 # access controls and watch out for security risks (like XSS)  
 # Don't forget to:  
 # from google.appengine.ext import ndb  
 # from google.appengine.ext.ndb import metadata  
 class GqlPage(webapp2.RequestHandler):  
  def get(self):  
   limited = True  
   row_limit = 1000  
   # Tricky to distinguish absence of 'limit' checkbox when you  
   # first hit the URL from when you submitted with an unchecked box  
   if self.request.get('download', 'nope') != 'nope' and \  
     self.request.get("limit", "nope") == "nope":  
    limited = False  
   query = self.request.get('query', "empty")  
   is_csv = False  
   if self.request.get('download') == 'Download':  
    is_csv = True  
    self.response.headers['Content-Type'] = "text/csv"  
    self.response.write('<form action="/gql" method=POST>')  
    self.response.write('<textarea name=query rows=10 cols=80 placeholder="select * from...">')  
    if query != "empty":  
    self.response.write("<input type=submit name=download value=View>")  
    self.response.write('<input id=foo type=submit name=download value=Download')  
    self.response.write(' onclick="document.getElementById(\'results_div\').innerHTML=\'\';">')  
    self.response.write("Limit response to " + str(row_limit) + " rows:")  
    self.response.write("<input name=limit type=checkbox ")  
    if limited:  
    self.response.write("Examples for available tables:<br>")  
    for kind in metadata.get_kinds():  
     self.response.write("select * from " + kind + "<br>")  
    self.response.write('<br><div id="results_div"><pre>')  
   if query != 'empty' and query != '':  
    results = []  
    if limited:  
     results = ndb.gql(query).fetch(row_limit)  
     results = ndb.gql(query).fetch()  
    writer = csv.writer(self.response.out)  
    row_count = 0  
    first_row = True  
    for row in results:  
     row_dict = row.to_dict()  
     keys = sorted(row_dict.keys())  
     # Write column labels as first row  
     if first_row:  
      first_row = False  
     values = []  
     for k in keys:  
      value = str(row_dict[k])  
      if is_csv:  
     row_count += 1  
     if not is_csv and limited and row_count == row_limit:  
      self.response.write("\n[Truncated at " + str(row_limit) + " lines]")  
   if not is_csv:  
  def post(self):  
   return self.get()  

Friday, March 20, 2015

Linux data acquisition with an Agilent Infiniium MSO9404A oscilloscope

I need to capture some analog data.  My coworker has a National Instruments USB data acquisition module, but as far as I can tell, the drivers are all proprietary and focused on using Labview.

So instead I used a fancy 9000-series Agilent Infiniium scope.  I plugged my laptop's ethernet cable into the scope, then used the Windows control panel on the scope to set its IPv4 address to  I set my laptop to, and found that I could ping the scope.  So far so good.

Back in the day, test equipment was controllable over a serial GPIB bus.  Today's fancy gear uses the same conventions over TCP/IP.  There's a confusing mess of acronyms like VXI-11 and VISA, and a bunch of half-baked libraries and crappy-looking system drivers that appear to be required to use them.  pyvisa looks nice, but wants a proprietary National Instruments driver distributed as a .iso(!).  Not my cup of tea.

Then I ran across this MATLAB example that's basically just chatting with the device over TCP/IP. That led me to Agilent's Programmer's Reference guide for the 9000-series scopes.

After fighting with the 1100-page manual for a few hours, I came up with the following settings that let me get samples at a specific rate like I would from an ADC.

Note that you can also interactively talk to the scope using nc or telnet to port 5025 while you're experimenting.

 # Using ethernet to talk to an Agilent Infiniium MSO9404A  
 # See also the "Infiniium 9000A Programmer's Reference"  
 import socket  
 import sys  
 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
 sock.connect(("", 5025))  
 def cmd(s):  
  sock.send(s + "\n")  
 def resp():  
  r = sock.recv(1048576)  
  while not r.endswith("\n"):  
   r += sock.recv(1048576)  
  return r  
 print "Querying scope"  
 print "Scope identifies as: ", resp(),  
 # This doesn't seem to affect the samples we receive  
 cmd(":timebase:range 1E-6")  
 cmd(":timebase:delay 0")  
 cmd(":channel1:range 5")  
 cmd(":channel1:offset 2.5")  
 cmd(":channel1:input dc")  
 cmd(":trigger:mode edge")  
 cmd(":trigger:slope positive")  
 cmd(":trigger:level chan1,2.5")  
 cmd(":system:header off")  
 cmd(":acquire:mode rtime") # Realtime mode; don't average multiple passes  
 cmd(":acquire:complete 100")  
 cmd(":waveform:source channel1")  
 cmd(":waveform:format ascii")  
 cmd(":acquire:count 1")  
 cmd(":acquire:srate:auto off")  
 cmd(":acquire:srate 4000")  
 cmd(":acquire:points:auto off")  
 cmd(":acquire:points 16")  
 # This was on by default, and took me a long time to figure out why  
 # I was getting ~16x the number of samples I requested.  
 cmd(":acquire:interpolate off")  
 cmd(":digitize channel1")
# This should block until the capture is done, since we used :digitize
 sample_string = resp().rstrip().rstrip(",")  
 ascii_samples = sample_string.split(",")  
 samples = []  
 for f in ascii_samples:  
   print "Couldn't convert:", f  
 print "Got ", len(samples), " samples. Values 1-10:", samples[:10]  

Monday, February 16, 2015

Empirically testing the "three hats game"

After reading the brain teaser here, I couldn't convince myself that the solution was correct:
It still seems impossible to me that any player should be able to do better than 50% at guessing the color of his own hat. So I wrote a C program to try out the strategy across 10000 games and report what it found. And indeed it does seem to work. I'd paste the code, but blogger sucks at code formatting. So here it is instead:

Friday, February 06, 2015

Ubuntu: use syndaemon disable touchpad while typing

As this page describes, syndaemon solves the problem of trying to type on a laptop and having the cursor jump somewhere else because it thinks you touched the touchpad:

Wednesday, January 28, 2015

Intuition for Young's Modulus

Today I was trying to remember how to use Young's Modulus.  I eventually figured it out (I think), but when I went to check my intuition, nobody else was describing it in these terms.  So it's odd that this wouldn't be a well-known intuitive aid, which I guess means I might just be wrong about it.  If so, please point that out in the comments.

But the mental shortcut is this: Young's Modulus (aka the Elastic Modulus) tells us how much pressure we have to apply to a material to double its length.

For example: Aluminum has a modulus of elasticity of 10 million PSI.  So if you had a piece of aluminum bar stock 1 inch square, and you pulled on it with a force of 10 million pounds, it'd end up twice as long as it started.  

Now, in reality the bar would tear before it'd stretch that far, because the maximum elongation for aluminum tends to be less than 30%.  But remembering that "E = pressure required to double the length" gets you into the right place to use the units correctly (for example, to say that 1% of 10 million PSI would elongate our bar by 1%).

Friday, January 23, 2015

Paperclip Maximizer 1.0

The AI does not hate you, nor does it love you, but you are made out of atoms which it can use for something else. 
—Eliezer Yudkowsky
A Paperclip Maximizer is an artificial intelligence whose sole purpose is to make as many paperclips as possible, which to the chagrin of its creator, ends up destroying the world in the process.  It's a way of explaining why the folks at MIRI think it's important to study ways of making AI that will benefit rather than harm us.
I have several friends who study AI, so I decided to creatively misunderstand the notion of a Paperclip Maximizer, and thus conclude that AI researchers are inexplicably preoccupied with paper clips.  So to ensure they never wanted for fasteners, I ordered a whole panoply of paperclips from around the internet and had them shipped to their desks over the course of a few weeks.

This amused me greatly, but after a few weeks it was time to take it to the next level.  Thus was born Paperclip Maximizer 1.0:

Here it is in action, with early prototype clips on display that didn't quite have that je ne sais quoi:

The mechanism is relatively simple, but taught me a lot about the subtleties of wire bending.  Two GWS S125 1T sail winch servos provide all the locomotion, controlled by an Arduino Leonardo and about 2 pages of code.

These particular servos have a lot more range of motion than normal servos, and are also easy to modify for continuous rotation.  That was important for the wire feed servo, which always needs to turn the same direction.  When you disassemble this particular servo, you can just remove the gear that connects the axle to the potentiometer.  Then when you drive it, if you tell it to move clockwise of where you left the pot, it'll turn clockwise forever, and vice versa.

The feed wheel is a piece of brass rod I knurled on the lathe to give it some bite.  It's pressed onto the servo, which means I needed to cut down a servo horn to a perfect cylinder.  To do that, I wrote a program for the Arduino to make it rotate continuously, then clamped the servo upright in the mill.  Then while the servo was spinning, I moved in with a spinning end mill.

Next to the feed wheel, you can see a bearing of about the same diameter.  Its job is to hold the wire against the feed wheel.  Originally I bolted the bearing in place, but the wire wouldn't feed consistently.  Turns out there's enough eccentricity in the feed wheel that I needed to let the bearing move in and out slightly.  So that's why you can see a big coil spring overhead, tensioning the bearing against the feed wheel and compressing the wire.  If you watch the video carefully, you can see the arm move in and out slightly on the long feeds, broadcasting to the world exactly how bad my lathe skills are.

Next comes the feed block.  Its job is to keep the wire straight and hold it in place while the bending unit bends the wire.  Like the rest of the project, this turns out to be much more constrained than I expected: if there's too much space between the block and the feed wheel, then when the bender pushes back on the wire, it can bow and kink.  And if there's too much space between the block and the bender, you can't get a tight bend in the wire.

I'd have liked to make the feed block longer to give me more room for the other components.  But it needs a small diameter hole to pass the wire, and small drill bits also tend to be short.  I think I even had to drill the block from both ends to reach all the way through with the bit I had available.

The bending head itself ended up also being pretty cramped.  It needed to extend high enough to leave room for the bearing that does the bending.  It's hard to see, but imagine the head starting out twice as thick as you see here:

I milled out all but a collar from the bottom half of the piece, and that's where it's attached to the servo.  You can just barely see that collar between the head and the servo, to the right of the bearing.  Then it had to reach over to the center of the bearing, but not crash into the feed block.  So I had the mill cut that nice curved shape that you see leading to the bearing nut.  But it wasn't a particularly critical dimension, so grinding the shape by hand would have worked just as well.  Then the head on the 1/4"-20 bolt that holds the bearing turned out to be a bit too thick, so I had to grind that to about half its normal thickness.

If you watch real wire bending machines, you'll see that they create different curves by feeding the wire while they adjust the benders.

Turns out it takes a lot of force to feed wire into a tight bend, and my feeder wasn't up to it.  So that's why I have to alternate feeding and bending, which I actually find quite delightful, since it adds to the quirky personality of the machine.  If I had known at the start that I wouldn't be able to feed while bending, I could have designed the machine to use a fixed bending finger instead of a bearing.  That would have made it a lot easier to make the tight bends needed for a paper clip -- since the bearing is round, it likes to deflect the wire back through the block as it approaches.

Since everything had to be jammed in close together, I was glad I chose to tap the mounting holes for the servos.  If I had to do the project over again, I'd save a lot of time by using acrylic on a laser.  But then I suppose I'd have to rearrange things, tap the acrylic, or maybe use some sort of press-in insert.

What about the big DC motor sitting on top of the bender?   That's the cutoff wheel:

I pressed down the factory plastic gear on the motor to expose more of the shaft, then hot glued on a dremel cutoff wheel.  The motor itself is held onto the bending head with double stick tape; not a great long-term solution.

The way it was supposed to work was to not reset the bender on the last bend, but instead keep moving past the wire.  Then I'd feed out the last leg of the paperclip, and then keep rotating the bending head clockwise until it brought around the cutoff wheel in to finish the job.

That's why you can see a dished out spot on top of the feed block -- I had to spend about 15 minutes manually and very gradually feeding the spinning wheel into the block so that it'd clear when moving into place, yet still end up close enough to the block to cut the wire instead of just deflecting it out of the way.

It's too bad that feature didn't work out, since the sparks you get with steel MIG welder wire make a great grand finale to the process!  But the motor is easy to stall against the wire, which generally also browns out the Arduino.  And about half the time, right at the end, the tiny sliver of remaining wire bends instead of cutting, leaving the finished paperclip attached to the next one.  Oh, and the cut also ends up being razor sharp: I lightly grazed my fingertip with one of the cutoff pieces of wire and ended up with a very clean, deep cut.  So that was the final nail in the coffin for the cutoff wheel.

Writing the Arduino code was one of the easiest parts of the project.  I invited my AI researcher friend over on a Saturday afternoon and we coded and tuned it over the course of an hour or two, making lots of misshapen proto-clips that you can see littered around in the video.  Everything is open loop: move the servo, wait, then move again.  For the wire feed, there are one or two values where the servo will stop because that's where I left the potentiometer when I modified the servo.  But the much better solution is to tell the servo library to detach, which stops the PWM signal to the servo, stopping it no matter where it thinks it is.

Sunday, January 04, 2015

Building new drawers

The crappy drawers in my kitchen were falling apart, so I took the faceplaces off and built new drawers.  I used 3/4" melamine-backed plywood for the sides, and 1/4" melamine-backed MDF for the bottom.  You can see a notch in the lowest tenon of each box joint where I cut the dado for the bottom.

I'm happy with how solid they turned out, but I managed to chip the melamine and sand through it in places when I was sanding the tails of the box joints flush.  So that's a cosmetic annoyance.

Everything's glued and nailed in place.  The nails through the box joints are another cosmetic issue, but I wanted the strength.  I guess if I cared enough, I could fill and paint it all smooth.

Thursday, January 01, 2015

Radial Mill

I see lots of small CNC milling machines these days, generally built like 3d printers: made of plastic or wood, or aluminum extrusion and bearings.  But unlike a 3d printer, subtractive milling involves huge forces, especially if you're trying to remove lots of metal.

The other thing I observe is that almost all the machines use cartesian axes: linear guides that move in perpendicular directions.  This makes a lot of sense in a manual mill where you're moving the axes by hand, but when it's computer-controlled, it's easy to do the math for all sorts of crazy coordinate systems.  So it makes sense to improve the machine mechanically in exchange for software complexity that's easy to share and replicate.

So I set out to make a machine that:

  • Is small enough to fit on a benchtop.  (36,000 pound machining centers are great, but it's always seemed funny to me that such huge machines usually work on workpieces only a few inches across)
  • Is stiff enough to accurately and quickly remove lots of metal.  I don't mind if it's a few hundred pounds as long as it's compact.
  • Exchanges as much mechanical complexity as possible to software.

Here's the result:

The two discs are flywheels from a Chevy smallblock, driven directly by steppers.  The upper disc can move the spindle on an arc from the center to the edge of the lower disc, where the workpiece is mounted.  So by rotating the two discs appropriately, we can put the spindle over any point on the lower disc.

One nifty side effect of this approach is that we get more torque and more accuracy in the lower table the closer we get to the center.

The complexity front was a huge success: the whole machine ends up being about a dozen parts in all.  8 non-off-the-shelf parts, 5 of which are used twice.

  • Table and two legs
  • Neck
  • Upper table
  • Stepper mounting plate (2)
  • Flywheels (2)
  • Wheel bearings (4), axle (2), top cap (2), bottom cap (2)

The flywheels are cast iron, which is great for vibration damping.  And the wheel bearings are from a Ford F-450 truck, and rated for thousands of pounds of load.  The axle is a length of 2 inch steel bar, and the top and bottom caps preload the bearings and hold the assembly together.  Here's the assembly, with prototype MDF end caps:

By comparison, here's a MakerSlide carriage picture I found at  It has the custom aluminum extrusion, two mounting plates, six bearings (which typically use eccentric mounts so that they can be tensioned against the extrusion) and a long drive belt:

Way less rigid, and way more complicated.

All of the parts are straightforward to mill on a manual milling machine.  The top table is the only part with a curved contour, but it's not a critical dimension, and I just did it because I happened to be using a CNC Bridgeport.  Here you can see all the dimension labels I added in Sketchup to help me program the Bridgeport by hand:

Not particularly relevant to the machine itself, but when it came to choosing a controller, I opted to port GRBL to Raspberry Pi rather than using an Arduino.  The board costs about the same and has a lot more power, which I figured might be handy if I needed to implement the coordinate transformation in using a CPU-intensive hack.

In the next section I'll cover the many things that are still missing, but here's the end result for the machine in its current state.  I taped a marker to the spindle and had it draw the Hackaday skull and wrenches.  No Z-axis means no lifting the pen between segments, and no coordinate transform means that the image gets distorted in interesting ways, although it's still quite recognizable when drawn in that part of the space:

Things to improve

This is just a prototype.  Here's what needs improvement:

  • Backlash.  I measured about 0.010" backlash in the ring and spur gears, which is great for a starter motor on an engine, but terrible for a CNC machine.  I found a worm gear that will drive those threads with more torque (and more steps per inch), but it'd increase the mechanical complexity and I'm not sure if it'd improve the lash.
  • There's no Z axis!  Fittingly for a machine that's all about rotary axes, the up-down Z-axis, which needs to be linear, ended up taking more time than the rest of the project combined.  I never did manage to create linear motion that I was happy with, so to get it into some semblance of working I finally just bored out a piece of aluminum and held the spindle in place with a big set screw.
  • Software support.  The math for rectangular->polar coordinates really is quite simple, but integrating it with a G-code interpreter is another matter.  I managed to port GRBL to Raspberry Pi, but its assumptions about rectangular coordinates are baked in pretty deeply.  My research into TinyG and LinuxCNC don't make me optimistic.
  • Most of the machine is still made out of prototype MDF parts.  The plan was to start with MDF, then re-do with aluminum, then use steel or cast iron for the final draft.  I never got that far, so the black parts you see are just spray-painted MDF.
  • Work-holding, tuning and calibration are completely unaddressed.