Electronics, Embedded Systems, and Software are my breakfast, lunch, and dinner.
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:
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:
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:
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. The information in this post should enable to you program cheap AVR microcontrollers such as the ATTiny4, ATTiny5, ATTiny9, ATTiny10, or ATTiny20 using a widely available usbasp AVR programmer.
I intend to show exactly the following, no more and no less:
A recent project required me to reuse (once again) my USB HID device driver. This is my third or fourth project using this and I had started to find it annoying to need to hand-modify a heavily-commented, self-referencing array of uint8_t's. I figured there must be a better way, so I decided to try something different.
In this post I will present a script that turns this madness, which lives in a separate file:
1/**
2 * Device descriptor
3 */
4static const USB_DATA_ALIGN uint8_t dev_descriptor[] = {
5 18, //bLength
6 1, //bDescriptorType
7 0x00, 0x02, //bcdUSB
8 0x00, //bDeviceClass (defined by interfaces)
9 0x00, //bDeviceSubClass
10 0x00, //bDeviceProtocl
11 USB_CONTROL_ENDPOINT_SIZE, //bMaxPacketSize0
12 0xc0, 0x16, //idVendor
13 0xdc, 0x05, //idProduct
14 0x11, 0x00, //bcdDevice
15 1, //iManufacturer
16 2, //iProduct
17 0, //iSerialNumber,
18 1, //bNumConfigurations
19};
20
21static const USB_DATA_ALIGN uint8_t hid_report_descriptor[] = {
22 HID_SHORT(0x04, 0x00, 0xFF), //USAGE_PAGE (Vendor Defined)
23 HID_SHORT(0x08, 0x01), //USAGE (Vendor 1)
24 HID_SHORT(0xa0, 0x01), //COLLECTION (Application)
25 HID_SHORT(0x08, 0x01), // USAGE (Vendor 1)
26 HID_SHORT(0x14, 0x00), // LOGICAL_MINIMUM (0)
27 HID_SHORT(0x24, 0xFF, 0x00), //LOGICAL_MAXIMUM (0x00FF)
28 HID_SHORT(0x74, 0x08), // REPORT_SIZE (8)
29 HID_SHORT(0x94, 64), // REPORT_COUNT(64)
30 HID_SHORT(0x80, 0x02), // INPUT (Data, Var, Abs)
31 HID_SHORT(0x08, 0x01), // USAGE (Vendor 1)
32 HID_SHORT(0x90, 0x02), // OUTPUT (Data, Var, Abs)
33 HID_SHORT(0xc0), //END_COLLECTION
34};
35
36/**
37 * Configuration descriptor
38 */
39static const USB_DATA_ALIGN uint8_t cfg_descriptor[] = {
40 9, //bLength
41 2, //bDescriptorType
42 9 + 9 + 9 + 7 + 7, 0x00, //wTotalLength
43 1, //bNumInterfaces
44 1, //bConfigurationValue
45 0, //iConfiguration
46 0x80, //bmAttributes
47 250, //bMaxPower
48 /* INTERFACE 0 BEGIN */
49 9, //bLength
50 4, //bDescriptorType
51 0, //bInterfaceNumber
52 0, //bAlternateSetting
53 2, //bNumEndpoints
54 0x03, //bInterfaceClass (HID)
55 0x00, //bInterfaceSubClass (0: no boot)
56 0x00, //bInterfaceProtocol (0: none)
57 0, //iInterface
58 /* HID Descriptor */
59 9, //bLength
60 0x21, //bDescriptorType (HID)
61 0x11, 0x01, //bcdHID
62 0x00, //bCountryCode
63 1, //bNumDescriptors
64 0x22, //bDescriptorType (Report)
65 sizeof(hid_report_descriptor), 0x00,
66 /* INTERFACE 0, ENDPOINT 1 BEGIN */
67 7, //bLength
68 5, //bDescriptorType
69 0x81, //bEndpointAddress (endpoint 1 IN)
70 0x03, //bmAttributes, interrupt endpoint
71 USB_HID_ENDPOINT_SIZE, 0x00, //wMaxPacketSize,
72 10, //bInterval (10 frames)
73 /* INTERFACE 0, ENDPOINT 1 END */
74 /* INTERFACE 0, ENDPOINT 2 BEGIN */
75 7, //bLength
76 5, //bDescriptorType
77 0x02, //bEndpointAddress (endpoint 2 OUT)
78 0x03, //bmAttributes, interrupt endpoint
79 USB_HID_ENDPOINT_SIZE, 0x00, //wMaxPacketSize
80 10, //bInterval (10 frames)
81 /* INTERFACE 0, ENDPOINT 2 END */
82 /* INTERFACE 0 END */
83};
84
85static const USB_DATA_ALIGN uint8_t lang_descriptor[] = {
86 4, //bLength
87 3, //bDescriptorType
88 0x09, 0x04 //wLANGID[0]
89};
90
91static const USB_DATA_ALIGN uint8_t manuf_descriptor[] = {
92 2 + 15 * 2, //bLength
93 3, //bDescriptorType
94 'k', 0x00, //wString
95 'e', 0x00,
96 'v', 0x00,
97 'i', 0x00,
98 'n', 0x00,
99 'c', 0x00,
100 'u', 0x00,
101 'z', 0x00,
102 'n', 0x00,
103 'e', 0x00,
104 'r', 0x00,
105 '.', 0x00,
106 'c', 0x00,
107 'o', 0x00,
108 'm', 0x00
109};
110
111static const USB_DATA_ALIGN uint8_t product_descriptor[] = {
112 2 + 14 * 2, //bLength
113 3, //bDescriptorType
114 'L', 0x00,
115 'E', 0x00,
116 'D', 0x00,
117 ' ', 0x00,
118 'W', 0x00,
119 'r', 0x00,
120 'i', 0x00,
121 's', 0x00,
122 't', 0x00,
123 'w', 0x00,
124 'a', 0x00,
125 't', 0x00,
126 'c', 0x00,
127 'h', 0x00
128};
129
130const USBDescriptorEntry usb_descriptors[] = {
131 { 0x0100, 0x0000, sizeof(dev_descriptor), dev_descriptor },
132 { 0x0200, 0x0000, sizeof(cfg_descriptor), cfg_descriptor },
133 { 0x0300, 0x0000, sizeof(lang_descriptor), lang_descriptor },
134 { 0x0301, 0x0409, sizeof(manuf_descriptor), manuf_descriptor },
135 { 0x0302, 0x0409, sizeof(product_descriptor), product_descriptor },
136 { 0x2200, 0x0000, sizeof(hid_report_descriptor), hid_report_descriptor },
137 { 0x0000, 0x0000, 0x00, NULL }
138};
Into these comment blocks which can live anywhere in the source and are somewhat more readable:
1/**
2 * <descriptor id="device" type="0x01">
3 * <length name="bLength" size="1" />
4 * <type name="bDescriptorType" size="1" />
5 * <word name="bcdUSB">0x0200</word>
6 * <byte name="bDeviceClass">0</byte>
7 * <byte name="bDeviceSubClass">0</byte>
8 * <byte name="bDeviceProtocol">0</byte>
9 * <byte name="bMaxPacketSize0">USB_CONTROL_ENDPOINT_SIZE</byte>
10 * <word name="idVendor">0x16c0</word>
11 * <word name="idProduct">0x05dc</word>
12 * <word name="bcdDevice">0x0010</word>
13 * <ref name="iManufacturer" type="0x03" refid="manufacturer" size="1" />
14 * <ref name="iProduct" type="0x03" refid="product" size="1" />
15 * <byte name="iSerialNumber">0</byte>
16 * <count name="bNumConfigurations" type="0x02" size="1" />
17 * </descriptor>
18 * <descriptor id="lang" type="0x03" first="first">
19 * <length name="bLength" size="1" />
20 * <type name="bDescriptorType" size="1" />
21 * <foreach type="0x03" unique="unique">
22 * <echo name="wLang" />
23 * </foreach>
24 * </descriptor>
25 * <descriptor id="manufacturer" type="0x03" wIndex="0x0409">
26 * <property name="wLang" size="2">0x0409</property>
27 * <length name="bLength" size="1" />
28 * <type name="bDescriptorType" size="1" />
29 * <string name="wString">kevincuzner.com</string>
30 * </descriptor>
31 * <descriptor id="product" type="0x03" wIndex="0x0409">
32 * <property name="wLang" size="2">0x0409</property>
33 * <length name="bLength" size="1" />
34 * <type name="bDescriptorType" size="1" />
35 * <string name="wString">LED Wristwatch</string>
36 * </descriptor>
37 * <descriptor id="configuration" type="0x02">
38 * <length name="bLength" size="1" />
39 * <type name="bDescriptorType" size="1" />
40 * <length name="wTotalLength" size="2" all="all" />
41 * <count name="bNumInterfaces" type="0x04" associated="associated" size="1" />
42 * <byte name="bConfigurationValue">1</byte>
43 * <byte name="iConfiguration">0</byte>
44 * <byte name="bmAttributes">0x80</byte>
45 * <byte name="bMaxPower">250</byte>
46 * <children type="0x04" />
47 * </descriptor>
48 */
49
50/**
51 * <include>usb_hid.h</include>
52 * <descriptor id="hid_interface" type="0x04" childof="configuration">
53 * <length name="bLength" size="1" />
54 * <type name="bDescriptorType" size="1" />
55 * <index name="bInterfaceNumber" size="1" />
56 * <byte name="bAlternateSetting">0</byte>
57 * <count name="bNumEndpoints" type="0x05" associated="associated" size="1" />
58 * <byte name="bInterfaceClass">0x03</byte>
59 * <byte name="bInterfaceSubClass">0x00</byte>
60 * <byte name="bInterfaceProtocol">0x00</byte>
61 * <byte name="iInterface">0</byte>
62 * <children type="0x21" />
63 * <children type="0x05" />
64 * </descriptor>
65 * <descriptor id="hid" type="0x21" childof="hid_interface">
66 * <length name="bLength" size="1" />
67 * <type name="bDescriptorType" size="1" />
68 * <word name="bcdHID">0x0111</word>
69 * <byte name="bCountryCode">0x00</byte>
70 * <count name="bNumDescriptors" type="0x22" size="1" associated="associated" />
71 * <foreach type="0x22" associated="associated">
72 * <echo name="bDescriptorType" />
73 * <echo name="wLength" />
74 * </foreach>
75 * </descriptor>
76 * <descriptor id="hid_in_endpoint" type="0x05" childof="hid_interface">
77 * <length name="bLength" size="1" />
78 * <type name="bDescriptorType" size="1" />
79 * <inendpoint name="bEndpointAddress" define="HID_IN_ENDPOINT" />
80 * <byte name="bmAttributes">0x03</byte>
81 * <word name="wMaxPacketSize">USB_HID_ENDPOINT_SIZE</word>
82 * <byte name="bInterval">10</byte>
83 * </descriptor>
84 * <descriptor id="hid_out_endpoint" type="0x05" childof="hid_interface">
85 * <length name="bLength" size="1" />
86 * <type name="bDescriptorType" size="1" />
87 * <outendpoint name="bEndpointAddress" define="HID_OUT_ENDPOINT" />
88 * <byte name="bmAttributes">0x03</byte>
89 * <word name="wMaxPacketSize">USB_HID_ENDPOINT_SIZE</word>
90 * <byte name="bInterval">10</byte>
91 * </descriptor>
92 * <descriptor id="hid_report" childof="hid" top="top" type="0x22" order="1" wIndexType="0x04">
93 * <hidden name="bDescriptorType" size="1">0x22</hidden>
94 * <hidden name="wLength" size="2">sizeof(hid_report)</hidden>
95 * <raw>
96 * HID_SHORT(0x04, 0x00, 0xFF), //USAGE_PAGE (Vendor Defined)
97 * HID_SHORT(0x08, 0x01), //USAGE (Vendor 1)
98 * HID_SHORT(0xa0, 0x01), //COLLECTION (Application)
99 * HID_SHORT(0x08, 0x01), // USAGE (Vendor 1)
100 * HID_SHORT(0x14, 0x00), // LOGICAL_MINIMUM (0)
101 * HID_SHORT(0x24, 0xFF, 0x00), //LOGICAL_MAXIMUM (0x00FF)
102 * HID_SHORT(0x74, 0x08), // REPORT_SIZE (8)
103 * HID_SHORT(0x94, 64), // REPORT_COUNT(64)
104 * HID_SHORT(0x80, 0x02), // INPUT (Data, Var, Abs)
105 * HID_SHORT(0x08, 0x01), // USAGE (Vendor 1)
106 * HID_SHORT(0x90, 0x02), // OUTPUT (Data, Var, Abs)
107 * HID_SHORT(0xc0), //END_COLLECTION
108 * </raw>
109 * </descriptor>
110 */
In most of my projects before this one I would have something like the first script shown above sitting in a file by itself, declaring a bunch of uint8_t arrays and a usb_descriptors[] table constant that would be consumed by my USB driver as it searched for USB descriptors. A header file that exposes the usb_descriptors[] table would also be found in the project. Any USB descriptor that had to be returned by the device would be found in this table. To make things more complex, descriptors like the configuration descriptor have to declare all of the device interfaces and so pieces and parts of each separate USB interface component would be interspersed inside of other descriptors.
I've been using this structure for some time after writing my first USB driver after reading through the Teensy driver. This is probably the only structural code that has made it all the way from the Teensy driver into all of my other code.
With this new script I've written there's no more need for manually computing how long a descriptor is or needing to modify the configuration descriptor every time a new interface has been added. All the parts of a descriptor are self-contained in the source file that defines a particular interface and can be easily moved around from project to project.
All the code for this post lives here:
`https://github.com/kcuzner/midi-fader <https://github.com/kcuzner/midi-fader>`__
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):
The complete source can be found here:
http://github.com/kcuzner/iotree
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:
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.
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):
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:
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**
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.
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.
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.
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.
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