Kevin Cuzner's Personal Blog

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


Dev boards? Where we're going we won't need dev boards...

A complete tutorial for using an STM32 without a dev board

Introduction

IMG_20160521_213623.jpg

About two years ago I started working with the Teensy 3.1 (which uses a Freescale Kinetis ARM-Cortex microcontroller) and I was super impressed with the ARM processor, both for its power and relative simplicity (it is not simple...its just relatively simple for the amount of power you get for the cost IMO). Almost all my projects before that point had consisted of AVRs and PICs (I'm in the AVR camp now), but now ARM-based microcontrollers had become serious contenders for something that I could go to instead. I soon began working on a small development board project also involving some Freescale Kinetis microcontrollers since those are what I have become the most familiar with. Sadly, I have had little success since I have been trying to make a programmer myself (the official one is a minimum of $200). During the course of this project I came across a LOT of STM32 stuff and it seemed that it was actually quite easy to set up. Lots of the projects used the STM32 Discovery and similar dev boards, which are a great tools and provide an easy introduction to ARM microcontrollers. However, my interest is more towards doing very bare metal development. Like soldering the chip to a board and hooking it up to a programmer. Who needs any of that dev board stuff? For some reason I just find doing embedded development without a development board absolutely fascinating. Some people might interpret doing things this way as a form of masochism. Perhaps I should start seeing a doctor...

Having seen how common the STM32 family was (both in dev boards and in commercial products) and noting that they were similarly priced to the Freescale Kinetis series, I looked in to exactly what I would need to program these, saw that the stuff was cheap, and bought it. After receiving my parts and soldering everything up, I plugged everything into my computer and had a program running on the STM32 in a matter of hours. Contrast that to a year spent trying to program a Kinetis KL26 with only partial success.

This post is a complete step-by-step tutorial on getting an STM32 microcontroller up and running without using a single dev board (breakout boards don't count as dev boards for the purposes of this tutorial). I'm writing this because I could not find a standalone tutorial for doing this with an ARM microcontroller and I ended up having to piece it together bit by bit with a lot of googling. My objective is to walk you through the process of purchasing the appropriate parts, writing a small program, and uploading it to the microcontroller.

I make the following assumptions:

  • The reader is familiar with electronics and soldering.
  • The reader is familiar with flash-based microcontrollers in general (PIC, AVR, ARM, etc) and has programmed a few using a separate standalone programmer before.
  • The reader knows how to read a datasheet.
  • The reader knows C and is at least passingly familiar with the overall embedded build process of compilation-linking-flashing.
  • The reader knows about makefiles.
  • The reader is ridiculously excited about ARM microcontrollers and is strongly motivated to overlook any mistakes here and try this out for themselves (srsly tho...if you see a problem or have a suggestion, leave it in the comments. I really do appreciate feedback.)

All code, makefiles, and configuration stuff can be found in the project repository on github. Project Repository: `https://github.com/kcuzner/stm32f103c8-blink <https://github.com/kcuzner/stm32f103c8-blink>`__

Materials

You will require the following materials:

  • A computer running Linux. If you run Windows only, please don't be dissuaded. I'm just lazy and don't want to test this for Windows. It may require some finagling. Manufacturer support is actually better for Windows since they provide some interesting configuration and programming software that is Windows only...but who needs that stuff anyway?
  • A STLinkv2 Clone from eBay. Here's one very similar to the one I bought. ~$3
  • Some STM32F103C8's from eBay. Try going with the TQFP-48 package. Why this microcontroller? Because for some reason it is all over the place on eBay. I suspect that the lot I bought (and all of the ones on eBay) is probably not authentic from ST. I hear that Chinese STM32 clones abound nowadays. I got 10 for $12.80.
  • A breakout board for a TQFP-48 with 0.5mm pitch. Yes, you will need to solder surface mount. I found mine for $1. I'm sure you can find one for a similar price.
  • 4x 0.1uF capacitors for decoupling. Mine are surface mount in the 0603 package. These will be soldered creatively to the breakout board between the power pins to provide some decoupling since we will probably have wires flying all over. I had mine lying around in a parts bin, left over from my development board project. Digikey is great for getting these, but I'm sure you could find them on eBay or Amazon as well.
  • Some dupont wires for connecting the programmer to the STM32. You will need at least 4. These are the ones that are sold for Arduinos. These came with my programmer, but you may have some in your parts box. They are dang cheap on Amazon.
  • Regular wires.
  • An LED and a resistor.

I was able to acquire all of these parts for less than $20. Now, I did have stuff like the capacitors, led, resistor, and wires lying around in parts boxes, but those are quite cheap anyway.

Side note: Here is an excellent video by the EE guru Dave Jones on surface mount soldering if the prospect is less than palatable to you: https://www.youtube.com/watch?v=b9FC9fAlfQE

Step 1: Download the datasheets

Above we decided to use the STM32F103C8 ARM Cortex-M3 microcontroller in a TQFP-48 package. This microcontroller has so many peripherals its no wonder its the one all over eBay. I could see this microcontroller easily satisfying the requirements for all of my projects. Among other things it has:

  • 64K flash, 20K RAM
  • 72MHz capability with an internal PLL
  • USB
  • CAN
  • I2C & SPI
  • Lots of timers
  • Lots of PWM
  • Lots of GPIO

All this for ~$1.20/part no less! Of course, its like $6 on digikey, but for my purposes having an eBay-sourced part is just fine.

Ok, so when messing with any microcontroller we need to look at its datasheet to know where to plug stuff in. For almost all ARM Microcontrollers there will be no less than 2 datasheet-like documents you will need: The part datasheet and the family reference manual . The datasheet contains information such as the specific pinouts and electrical characteristics and the family reference manual contains the detailed information on how the microcontroller works (core and peripherals). These are both extremely important and will be indispensable for doing anything at all with one of these microcontrollers bare metal.

Find the STM32F103C8 datasheet and family reference manual here (datasheet is at the top of the page, reference manual is at the bottom): http://www.st.com/en/microcontrollers/stm32f103c8.html. They are also found in the "ref" folder of the repository.

Step 2: Figure out where to solder and do it

STM32F103Pins-1.png

After getting the datasheet we need to solder the microcontroller down to the breakout board so that we can start working with it on a standard breadboard. If you prefer to go build your own PCB and all that (I usually do actually) then do that instead of this. However, you will still need to know which pins to hook up.

On the pin diagram posted here you will find the highlighted pins of interest for hooking this thing up. We need the following pins at a minimum:

  • Shown in Red/Blue:  All power pins, VDD, VSS, AVDD, and AVSS. There are four pairs: 3 for the VDD/VSS and one AVDD/AVSS. The AVDD/AVSS pair is specifically used to power the analog/mixed signal circuitry and is separate to give us the opportunity to perform some additional filtering on those lines and remove supply noise induced by all the switching going on inside the microcontroller; an opportunity I won't take for now.
  • Shown in Yellow/Green: The SWD (Serial Wire Debug) pins. These are used to connect to the STLinkV2 programmer that you purchased earlier. These can be used for so much more than just programming (debugging complete with breakpoints, for a start), but for now we will just use it to talk to the flash on the microcontroller.
  • Shown in Cyan: Two fun GPIOs to blink our LEDs with. I chose PB0 and PB1. You could choose others if you would like, but just make sure that they are actually GPIOs and not something unexpected.

Below you will find a picture of my breakout board. I soldered a couple extra pins since I want to experiment with USB.

IMG_20160521_211614.jpg

Very important: You may notice that I have some little tiny capacitors (0.1uF) soldered between the power pins (the one on the top is the most visible in the picture). You need to mount your capacitors between each pair of VDD/VSS pins (including AVDD/AVSS) . How you do this is completely up to you, but it must be done and *they should be rather close to the microcontroller itself* . If you don't it is entirely possible that when the microcontroller first turns on and powers up (specifically at the first falling edge of the internal clock cycle), the inductance created by the flying power wires we have will create a voltage spike that will either cause a malfunction or damage. I've broken microcontrollers by forgetting the decoupling caps and I'm not eager to do it again.

Step 3: Connect the breadboard and programmer

IMG_20160521_213137.jpg

Don't do this with the programmer plugged in.

On the right you will see my STLinkV2 clone which I will use for this project. Barely visible is the pinout. We will need the following pins connected from the programmer onto our breadboard. These come off the header on the non-USB end of the programmer. Pinouts may vary. Double check your programmer!

  • 3.3V: We will be using the programmer to actually power the microcontroller since that is the simplest option. I believe this pin is Pin 7 on my header.
  • GND: Obviously we need the ground. On mine this was Pin 4.
  • SWDIO: This is the data for the SWD bus. Mine has this at Pin 2.
  • SWCLK: This is the clock for the SWD bus. Mine has this at Pin 6.

You may notice in the above picture that I have an IDC cable coming off my programmer rather than the dupont wires. I borrowed the cable from my AVR USBASP programmer since it was more available at the time rather than finding the dupont cables that came with the STLinkV2.

Next, we need to connect the following pins on the breadboard:

  • STM32 [A]VSS pins 8, 23, 35, and 47 connected to ground.
  • STM32 [A]VDD pins 9, 24, 36, and 48 connected to 3.3V.
  • STM32 pin 34 to SWDIO.
  • STM32 pin 37 to SWCLK.
  • STM32 PB0 pin 18 to a resistor connected to the anode of an LED. The cathode of the LED goes to ground. Pin 19 (PB1) can also be connected in a similar fashion if you should so choose.

Here is my breadboard setup:

IMG_20160521_211048-1.jpg

Step 4: Download the STM32F1xx C headers

Project Repository: `https://github.com/kcuzner/stm32f103c8-blink <https://github.com/kcuzner/stm32f103c8-blink>`__

Since we are going to write a program, we need the headers. These are part of the STM32CubeF1 library found here.

Visit the page and download the STM32CubeF1 zip file. It will ask for an email address. If you really don't want to give them your email address, the necessary headers can be found in the project github repository.

Alternately, just clone the repository. You'll miss all the fun of poking around the zip file, but sometimes doing less work is better.

The STM32CubeF1 zip file contains several components which are designed to help people get started quickly when programming STM32s. This is one thing that ST definitely does better than Freescale. It was so difficult to find the headers for the Kinetis microcontrollers that almost gave up at that point. Anyway, inside the zip file we are only interested in the following:

  • The contents of Drivers/CMSIS/Device/ST/STM32F1xx/Include. These headers contain the register definitions among other things which we will use in our program to reference the peripherals on the device.
  • Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s. This contains the assembly code used to initialize the microcontroller immediately after reset. We could easily write this ourselves, but why reinvent the wheel?
  • Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c. This contains the common system startup routines referenced by the assembly file above.
  • Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/linker/STM32F103XB_FLASH.ld. This is the linker script for the next model up of the microcontroller we have (we just have to change the "128K" to a "64K" near the beginning of the file in the MEMORY section (line 43 in my file) and we are good to go). This is used to tell the linker where to put all the parts of the program inside the microcontroller's flash and RAM. Mine had a "0" on every blank line. If you see this in yours, delete those "0"s. They will cause errors.
  • The contents of Drivers/CMSIS/Include. These are the core header files for the ARM Cortex-M3 and the definitions contained therein are used in all the other header files we reference.

I copied all the files referenced above to various places in my project structure so they could be compiled into the final program. Please visit the repository for the exact locations and such. My objective with this tutorial isn't really to talk too much about project structure, and so I think that's best left as an exercise for the reader.

Step 5: Install the required software

We need to be able to compile the program and flash the resulting binary file to the microcontroller. In order to do this, we will require the following programs to be installed:

  • The arm-none-eabi toolchain. I use arch linux and had to install "arm-none-eabi-gcc". On Ubuntu this is called "gcc-arm-none-eabi". This is the cross-compiler for the ARM Cortex cores. The naming "none-eabi" comes from the fact that it is designed to compile for an environment where the program is the only thing running on the target processor. There is no underlying operating system talking to the application binary file (ABI = application binary interface, none-eabi = No ABI) in order to load it into memory and execute it. This means that it is ok with outputting raw binary executable programs. Contrast this with Linux which likes to use the ELF format (which is a part of an ABI specification) and the OS will interpret that file format and load the program from it.
  • arm-none-eabi binutils. In Arch the package is "arm-none-eabi-binutils". In Ubuntu this is "binutils-arm-none-eabi". This contains some utilities such as "objdump" and "objcopy" which we use to convert the output ELF format into the raw binary format we will use for flashing the microcontroller.
  • Make. We will be using a makefile, so obviously you will need make installed.
  • OpenOCD. I'm using 0.9.0, which I believe is available for both Arch and Ubuntu. This is the program that we will use to talk to the STLinkV2 which in turn talks to the microcontroller. While we are just going to use it to flash the microcontroller, it can be also used for debugging a program on the processor using gdb.

Once you have installed all of the above programs, you should be good to go for ARM development. As for an editor or IDE, I use vim. You can use whatever. It doesn't matter really.

Step 6: Write and compile the program

Ok, so we need to write a program for this microcontroller. We are going to simply toggle on and off a GPIO pin (PB0). After reset, the processor uses the internal RC oscillator as its system clock and so it runs at a reasonable 8MHz or so I believe. There are a few steps that we need to go through in order to actually write to the GPIO, however:

  1. Enable the clock to PORTB. Most ARM microcontrollers, the STM32 included, have a clock gating system that actually turns off the clock to pretty much all peripherals after system reset. This is a power saving measure as it allows parts of the microcontroller to remain dormant and not consume power until needed. So, we need to turn on the GPIO port before we can use it.
  2. Set PB0 to a push-pull output. This microcontroller has many different options for the pins including analog input, an open-drain output, a push-pull output, and an alternate function (usually the output of a peripheral such as a timer PWM). We don't want to run our LED open drain for now (though we certainly could), so we choose the push-pull output. Most microcontrollers have push-pull as the default method for driving their outputs.
  3. Toggle the output state on. Once we get to this point, it's success! We can control the GPIO by just flipping a bit in a register.
  4. Toggle the output state off. Just like the previous step.

Here is my super-simple main program that does all of the above:

 1/**
 2 * STM32F103C8 Blink Demonstration
 3 *
 4 * Kevin Cuzner
 5 */
 6
 7#include "stm32f1xx.h"
 8
 9int main(void)
10{
11    //Step 1: Enable the clock to PORT B
12    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
13
14    //Step 2: Change PB0's mode to 0x3 (output) and cfg to 0x0 (push-pull)
15    GPIOB->CRL = GPIO_CRL_MODE0_0 | GPIO_CRL_MODE0_1;
16
17    while (1)
18    {
19        //Step 3: Set PB0 high
20        GPIOB->BSRR = GPIO_BSRR_BS0;
21        for (uint16_t i = 0; i != 0xffff; i++) { }
22        //Step 4: Reset PB0 low
23        GPIOB->BSRR = GPIO_BSRR_BR0;
24        for (uint16_t i = 0; i != 0xffff; i++) { }
25    }
26
27    return 0;
28}

If we turn to our trusty family reference manual, we will see that the clock gating functionality is located in the Reset and Clock Control (RCC) module (section 7 of the manual). The gates to the various peripherals are sorted by the exact data bus they are connected to and have appropriately named registers. The PORTB module is located on the APB2 bus, and so we use the RCC->APB2ENR to turn on the clock for port B (section 7.3.7 of the manual).

The GPIO block is documented in section 9. We first talk to the low control register (CRL) which controls pins 0-7 of the 16-pin port. There are 4 bits per pin which describe the configuration grouped in to two 2-bit (see how many "2" sounding words I had there?) sections: The Mode and Configuration. The Mode sets the analog/input/output state and the Configuration handles the specifics of the particular mode. We have chosen output (Mode is 0b11) and the 50MHZ-capable output mode (Cfg is 0b00). I'm not fully sure what the 50MHz refers to yet, so I just kept it at 50MHz because that was the default value.

After talking to the CRL, we get to talk to the BSRR register. This register allows us to write a "1" to a bit in the register in order to either set or reset the pin's output value. We start by writing to the BS0 bit to set PB0 high and then writing to the BR0 bit to reset PB0 low. Pretty straightfoward.

It's not a complicated program. Half the battle is knowing where all the pieces fit. The STM32F1Cube zip file contains some examples which could prove quite revealing into the specifics on using the various peripherals on the device. In fact, it includes an entire hardware abstraction layer (HAL) which you could compile into your program if you wanted to. However, I have heard some bad things about it from a software engineering perspective (apparently it's badly written and quite ugly). I'm sure it works, though.

So, the next step is to compile the program. See the makefile in the repository. Basically what we are going to do is first compile the main source file, the assembly file we pulled in from the STM32Cube library, and the C file we pulled in from the STM32Cube library. We will then link them using the linker script from the STM32Cube and then dump the output into a binary file.

  1# Makefile for the STM32F103C8 blink program
  2#
  3# Kevin Cuzner
  4#
  5
  6PROJECT = blink
  7
  8# Project Structure
  9SRCDIR = src
 10COMDIR = common
 11BINDIR = bin
 12OBJDIR = obj
 13INCDIR = include
 14
 15# Project target
 16CPU = cortex-m3
 17
 18# Sources
 19SRC = $(wildcard $(SRCDIR)/*.c) $(wildcard $(COMDIR)/*.c)
 20ASM = $(wildcard $(SRCDIR)/*.s) $(wildcard $(COMDIR)/*.s)
 21
 22# Include directories
 23INCLUDE  = -I$(INCDIR) -Icmsis
 24
 25# Linker
 26LSCRIPT = STM32F103X8_FLASH.ld
 27
 28# C Flags
 29GCFLAGS  = -Wall -fno-common -mthumb -mcpu=$(CPU) -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=$(addprefix $(OBJDIR)/,$(notdir $(<:.c=.lst)))
 30GCFLAGS += $(INCLUDE)
 31LDFLAGS += -T$(LSCRIPT) -mthumb -mcpu=$(CPU) --specs=nosys.specs
 32ASFLAGS += -mcpu=$(CPU)
 33
 34# Flashing
 35OCDFLAGS = -f /usr/share/openocd/scripts/interface/stlink-v2.cfg \
 36                -f /usr/share/openocd/scripts/target/stm32f1x.cfg \
 37                -f openocd.cfg
 38
 39# Tools
 40CC = arm-none-eabi-gcc
 41AS = arm-none-eabi-as
 42AR = arm-none-eabi-ar
 43LD = arm-none-eabi-ld
 44OBJCOPY = arm-none-eabi-objcopy
 45SIZE = arm-none-eabi-size
 46OBJDUMP = arm-none-eabi-objdump
 47OCD = openocd
 48
 49RM = rm -rf
 50
 51## Build process
 52
 53OBJ := $(addprefix $(OBJDIR)/,$(notdir $(SRC:.c=.o)))
 54OBJ += $(addprefix $(OBJDIR)/,$(notdir $(ASM:.s=.o)))
 55
 56
 57all:: $(BINDIR)/$(PROJECT).bin
 58
 59Build: $(BINDIR)/$(PROJECT).bin
 60
 61install: $(BINDIR)/$(PROJECT).bin
 62     $(OCD) $(OCDFLAGS)
 63
 64$(BINDIR)/$(PROJECT).hex: $(BINDIR)/$(PROJECT).elf
 65     $(OBJCOPY) -R .stack -O ihex $(BINDIR)/$(PROJECT).elf $(BINDIR)/$(PROJECT).hex
 66
 67$(BINDIR)/$(PROJECT).bin: $(BINDIR)/$(PROJECT).elf
 68     $(OBJCOPY) -R .stack -O binary $(BINDIR)/$(PROJECT).elf $(BINDIR)/$(PROJECT).bin
 69
 70$(BINDIR)/$(PROJECT).elf: $(OBJ)
 71     @mkdir -p $(dir $@)
 72     $(CC) $(OBJ) $(LDFLAGS) -o $(BINDIR)/$(PROJECT).elf
 73     $(OBJDUMP) -D $(BINDIR)/$(PROJECT).elf > $(BINDIR)/$(PROJECT).lst
 74     $(SIZE) $(BINDIR)/$(PROJECT).elf
 75
 76macros:
 77     $(CC) $(GCFLAGS) -dM -E - < /dev/null
 78
 79cleanBuild: clean
 80
 81clean:
 82     $(RM) $(BINDIR)
 83     $(RM) $(OBJDIR)
 84
 85# Compilation
 86$(OBJDIR)/%.o: $(SRCDIR)/%.c
 87     @mkdir -p $(dir $@)
 88     $(CC) $(GCFLAGS) -c $< -o $@
 89
 90$(OBJDIR)/%.o: $(SRCDIR)/%.s
 91     @mkdir -p $(dir $@)
 92     $(AS) $(ASFLAGS) -o $@ $<
 93
 94
 95$(OBJDIR)/%.o: $(COMDIR)/%.c
 96     @mkdir -p $(dir $@)
 97     $(CC) $(GCFLAGS) -c $< -o $@
 98
 99$(OBJDIR)/%.o: $(COMDIR)/%.s
100     @mkdir -p $(dir $@)
101     $(AS) $(ASFLAGS) -o $@ $<

The result of this makefile is that it will create a file called "bin/blink.bin" which contains our compiled program. We can then flash this to our microcontroller using openocd.

Step 7: Flashing the program to the microcontroller

Source for this step: https://github.com/rogerclarkmelbourne/Arduino_STM32/wiki/Programming-an-STM32F103XXX-with-a-generic-%22ST-Link-V2%22-programmer-from-Linux

This is the very last step. We get to do some openocd configuration. Firstly, we need to write a small configuration script that will tell openocd how to flash our program. Here it is:

1# Configuration for flashing the blink program
2init
3reset halt
4flash write_image erase bin/blink.bin 0x08000000
5reset run
6shutdown

Firstly, we init and halt the processor (reset halt). When the processor is first powered up, it is going to be running whatever program was previously flashed onto the microcontroller. We want to stop this execution before we overwrite the flash. Next we execute "flash write_image erase" which will first erase the flash memory (if needed) and then write our program to it. After writing the program, we then tell the processor to execute the program we just flashed (reset run) and we shutdown openocd.

Now, openocd requires knowledge of a few things. It first needs to know what programmer to use. Next, it needs to know what device is attached to the programmer. Both of these requirements must be satisfied before we can run our script above. We know that we have an stlinkv2 for a programmer and an stm32f1xx attached on the other end. It turns out that openocd actually comes with configuration files for these. On my installation these are located at "/usr/share/openocd/scripts/interface/stlink-v2.cfg" and "/usr/share/openocd/scripts/target/stm32f1x.cfg", respectively. We can execute all three files (stlink, stm32f1, and our flashing routine (which I have named "openocd.cfg")) with openocd as follows:

1openocd -f /usr/share/openocd/scripts/interface/stlink-v2.cfg \
2                -f /usr/share/openocd/scripts/target/stm32f1x.cfg \
3                -f openocd.cfg

So, small sidenote: If we left off the "shutdown" command, openocd would actually continue running in "daemon" mode, listening for connections to it. If you wanted to use gdb to interact with the program running on the microcontroller, that is what you would use to do it. You would tell gdb that there is a "remote target" at port 3333 (or something like that). Openocd will be listening at that port and so when gdb starts talking to it and trying to issue debug commands, openocd will translate those through the STLinkV2 and send back the translated responses from the microcontroller. Isn't that sick?

In the makefile earlier, I actually made this the "install" target, so running "sudo make install" will actually flash the microcontroller. Here is my output from that command for your reference:

 1kcuzner@kcuzner-laptop:~/Projects/ARM/stm32f103-blink$ sudo make install
 2arm-none-eabi-gcc -Wall -fno-common -mthumb -mcpu=cortex-m3 -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=obj/system_stm32f1xx.lst -Iinclude -Icmsis -c src/system_stm32f1xx.c -o obj/system_stm32f1xx.o
 3arm-none-eabi-gcc -Wall -fno-common -mthumb -mcpu=cortex-m3 -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=obj/main.lst -Iinclude -Icmsis -c src/main.c -o obj/main.o
 4arm-none-eabi-as -mcpu=cortex-m3 -o obj/startup_stm32f103x6.o src/startup_stm32f103x6.s
 5arm-none-eabi-gcc obj/system_stm32f1xx.o obj/main.o obj/startup_stm32f103x6.o -TSTM32F103X8_FLASH.ld -mthumb -mcpu=cortex-m3 --specs=nosys.specs  -o bin/blink.elf
 6arm-none-eabi-objdump -D bin/blink.elf > bin/blink.lst
 7arm-none-eabi-size bin/blink.elf
 8   text         data     bss     dec     hex filename
 9   1756         1092    1564    4412    113c bin/blink.elf
10arm-none-eabi-objcopy -R .stack -O binary bin/blink.elf bin/blink.bin
11openocd -f /usr/share/openocd/scripts/interface/stlink-v2.cfg -f /usr/share/openocd/scripts/target/stm32f1x.cfg -f openocd.cfg
12Open On-Chip Debugger 0.9.0 (2016-04-27-23:18)
13Licensed under GNU GPL v2
14For bug reports, read
15     http://openocd.org/doc/doxygen/bugs.html
16Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
17Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
18adapter speed: 1000 kHz
19adapter_nsrst_delay: 100
20none separate
21Info : Unable to match requested speed 1000 kHz, using 950 kHz
22Info : Unable to match requested speed 1000 kHz, using 950 kHz
23Info : clock speed 950 kHz
24Info : STLINK v2 JTAG v17 API v2 SWIM v4 VID 0x0483 PID 0x3748
25Info : using stlink api v2
26Info : Target voltage: 3.335870
27Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
28target state: halted
29target halted due to debug-request, current mode: Thread
30xPSR: 0x01000000 pc: 0x08000380 msp: 0x20004ffc
31auto erase enabled
32Info : device id = 0x20036410
33Info : flash size = 64kbytes
34target state: halted
35target halted due to breakpoint, current mode: Thread
36xPSR: 0x61000000 pc: 0x2000003a msp: 0x20004ffc
37wrote 3072 bytes from file bin/blink.bin in 0.249272s (12.035 KiB/s)
38shutdown command invoked
39kcuzner@kcuzner-laptop:~/Projects/ARM/stm32f103-blink$

After doing that I saw the following awesomeness:

IMG_20160521_212615.jpg

Wooo!!! The LED blinks! At this point, you have successfully flashed an ARM Cortex-M3 microcontroller with little more than a cheap programmer from eBay, a breakout board, and a few stray wires. Feel happy about yourself.

Conclusion

For me, this marks the end of one journey and the beginning of another. I can now feel free to experiment with ARM microcontrollers without having to worry about ruining a nice shiny development board. I can buy a obscenely powerful $1 STM32 microcontroller from eBay and put it into any project I want. If I were to try to do that with AVRs, I would be stuck with the ultra-low-end 8-pin ATTiny13A since that's about it for ~$1 AVR eBay offerings (don't worry...I've got plenty of ATMega328PB's...though they weren't $1). I sincerely hope that you found this tutorial useful and that it might serve as a springboard for doing your own dev board-free ARM development.

If you have any questions or comments (or want to let me know about any errors I may have made), let me know in the comments section here. I will try my best to help you out, although I can't always find the time to address every issue.

Teensy 3.1 bare metal: Writing a USB driver

One of the things that has intrigued me for the past couple years is making embedded USB devices. It's an industry standard bus that just about any piece of computing hardware can connect with yet is complex enough that doing it yourself is a bit of a chore.

Traditionally I have used the work of others, mainly the V-USB driver for AVR, to get my devices connected. Lately I have been messing around more with the ARM processor on a Teensy 3.1 which has an integrated USB module. The last microcontrollers I used that had these were the PIC18F4550s that I used in my dot matrix project. Even with those, I used microchip's library and drivers.

Over the thanksgiving break I started cobbling together some software with the intent of writing a driver for the USB module in the Teensy myself. I started originally with my bare metal stuff, but I ended up going with something closer to Karl Lunt's solution. I configured code::blocks to use the arm-none-eabi compiler that I had installed and created a code blocks project for my code and used that to build it (with a post-compile event translating the generated elf file into a hex file).

This is a work in progress and the git repository will be updated as things progress since it's not a dedicated demonstration of the USB driver.

The github repository here will be eventually turned in to a really really rudimentary 500-800ksps oscilloscope.

The code:  https://github.com/kcuzner/teensy-oscilloscope

The code for this post was taken from the following commit:

https://github.com/kcuzner/teensy-oscilloscope/tree/9a5a4c9108717cfec0174709a72edeab93fcf2b8

At the end of this post, I will have outlined all of the pieces needed to have a simple USB device setup that responds with a descriptor on endpoint 0.

USB Basics

I will actually not be talking about these here as I am most definitely no expert. However, I will point to the page that I found most helpful when writing this: http://www.usbmadesimple.co.uk/index.html

This site explained very clearly exactly what was going on with USB. Coupled with my previous knowledge, it was almost all I needed in terms of getting the protocol.

The Freescale K20 Family and their USB module

The one thing that I don't like about all of these great microcontrollers that come out with USB support is that all of them have their very own special USB module which doesn't work like anyone else. Sure, there are similarities, but there are no two exactly alike. Since I have a Teensy and the K20 family of microcontrollers seem to be relatively popular, I don't feel bad about writing such specific software.

There are two documents I found to be essential to writing this driver:

  1. The family manual. Getting a correct version for the MK20DX256VLH7 (the processor on the Teensy) can be a pain. PJRC comes to the rescue here: http://www.pjrc.com/teensy/K20P64M72SF1RM.pdf (note, the Teensies based on the MK20DX128VLH5 use a different manual)
  2. The Kinetis Peripheral Module Quick Reference: http://cache.freescale.com/files/32bit/doc/quick_ref_guide/KQRUG.pdf. This specifies the initialization sequence and other things that will be needed for the module.

There are a few essential parts to understand about the USB module:

  • It needs a specific memory layout. Since it doesn't have any dedicated user-accessible memory, it requires that the user specify where things should be. There are specific valid locations for its Buffer Descriptor Table (more on that later) and the endpoint buffers. The last one bit me for several days until I figured it out.
  • It has several different clock inputs and all of them must be enabled. Identifying the different signals is the most difficult part. After that, its not hard.
  • The module only handles the electrical aspect of things. It doesn't handle sending descriptors or anything like that. The only real things it handles are the signaling levels, responding to USB packets in a valid manner, and routing data into buffers by endpoint. Other than that, its all user software.
  • The module can act as both a host (USB On-the-go (OTG)) and a device. We will be exclusively focusing on using it as a device here.

In writing this, I must confess that I looked quite a lot at the Teensyduino code along with the V-USB driver code (even though V-USB is for AVR and is pure software). Without these "references", this would have been a very difficult project. Much of the structure found in the last to parts of this document reflects the Teensyduino USB driver since they did it quite efficiently and I didn't spend a lot of time coming up with a "better" way to do it, given the scope of this project. I will likely make more changes as I customize it for my end use-case.

Part 1: The clocks

The K20 family of microcontrollers utilizes a miraculous hardware module which they call the "Multipurpose Clock Generator" (hereafter called the MCG). This is a module which basically allows the microcontroller to take any clock input between a few kilohertz and several megahertz and transform it into a higher frequency clock source that the microcontroller can actually use. This is how the Teensy can have a rated speed of 96Mhz but only use a 16Mhz crystal. The configuration that this project uses is the Phase Locked Loop (PLL) from the high speed crystal source. The exact setup of this configuration is done by the sysinit code.

The PLL operates by using a divider-multiplier setup where we give it a divisor to divide the input clock frequency by and then a multiplier to multiply that result by to give us the final clock speed. After that, it heads into the System Integration Module (SIM) which distributes the clock. Since the Teensy uses a 16Mhz crystal and we need a 96Mhz system clock (the reason will become apparent shortly), we set our divisor to 4 and our multiplier to 24 (see common.h). If the other type of Teensy 3 is being used (the one with the MK20DX128VLH5), the divisor would be 8 and the multiplier 36 to give us 72Mhz.

Every module on a K20 microcontroller has a gate on its clock. This saves power since there are many modules on the microcontroller that are not being used in any given application. Distributing the clock to each of these is expensive in terms of power and would be wasted if that module wasn't used. The SIM handles this gating in the SIM_SCGC* registers. Before using any module, its clock gate must be enabled. If this is not done, the microcontroller will "crash" and stop executing when it tries to talk to the module registers (I think a handler for this can be specified, but I'm not sure). I had this happen once or twice while messing with this. So, the first step is to "turn on" the USB module by setting the appropriate bit in SIM_SCGC4 (per the family manual mentioned above, page 252):

1SIM_SCGC4 |= SIM_SCGC4_USBOTG_MASK;

Now, the USB module is a bit different than the other modules. In addition to the module clock it needs a reference clock for USB. The USB module requires that this reference clock be at 48Mhz. There are two sources for this clock: an internal source generated by the MCG/SIM or an external source from a pin. We will use the internal source:

1SIM_SOPT2 |= SIM_SOPT2_USBSRC_MASK | SIM_SOPT2_PLLFLLSEL_MASK;
2SIM_CLKDIV2 = SIM_CLKDIV2_USBDIV(1);

The first line here selects that the USB reference clock will come from an internal source. It also specifies that the internal source will be using the output from the PLL in the MCG (the other option is the FLL (frequency lock loop), which we are not using). The second line sets the divider needed to give us 48Mhz from the PLL clock. Once again there are two values: The divider and the multiplier. The multiplier can only be 1 or 2 and the divider can be anywhere from 1 to 16. Since we have a 96Mhz clock, we simply divide by 2 (the value passed is a 1 since 0 = "divide by 1", 1 = "divide by 2", etc). If we were using the 72Mhz clock, we would first multiply by 2 before dividing by 3.

With that, the clock to the USB module has been activated and the module can now be initialized.

Part 2: The startup sequence

The Peripheral Module Quick Reference guide mentioned earlier contains a flowchart which outlines the exact sequence needed to initialize the USB module to act as a device. I don't know if I can copy it here (yay copyright!), but it can be found on page 134, figure 15-6. There is another flowchart specifying the initialization sequence for using the module as a host.

Our startup sequence goes as follows:

 1//1: Select clock source
 2SIM_SOPT2 |= SIM_SOPT2_USBSRC_MASK | SIM_SOPT2_PLLFLLSEL_MASK; //we use MCGPLLCLK divided by USB fractional divider
 3SIM_CLKDIV2 = SIM_CLKDIV2_USBDIV(1); //(USBFRAC + 0)/(USBDIV + 1) = (1 + 0)/(1 + 1) = 1/2 for 96Mhz clock
 4
 5//2: Gate USB clock
 6SIM_SCGC4 |= SIM_SCGC4_USBOTG_MASK;
 7
 8//3: Software USB module reset
 9USB0_USBTRC0 |= USB_USBTRC0_USBRESET_MASK;
10while (USB0_USBTRC0 & USB_USBTRC0_USBRESET_MASK);
11
12//4: Set BDT base registers
13USB0_BDTPAGE1 = ((uint32_t)table) >> 8;  //bits 15-9
14USB0_BDTPAGE2 = ((uint32_t)table) >> 16; //bits 23-16
15USB0_BDTPAGE3 = ((uint32_t)table) >> 24; //bits 31-24
16
17//5: Clear all ISR flags and enable weak pull downs
18USB0_ISTAT = 0xFF;
19USB0_ERRSTAT = 0xFF;
20USB0_OTGISTAT = 0xFF;
21USB0_USBTRC0 |= 0x40; //a hint was given that this is an undocumented interrupt bit
22
23//6: Enable USB reset interrupt
24USB0_CTL = USB_CTL_USBENSOFEN_MASK;
25USB0_USBCTRL = 0;
26
27USB0_INTEN |= USB_INTEN_USBRSTEN_MASK;
28//NVIC_SET_PRIORITY(IRQ(INT_USB0), 112);
29enable_irq(IRQ(INT_USB0));
30
31//7: Enable pull-up resistor on D+ (Full speed, 12Mbit/s)
32USB0_CONTROL = USB_CONTROL_DPPULLUPNONOTG_MASK;

The first two steps were covered in the last section. The next one is relatively straightfoward: We ask the module to perform a "reset" on itself. This places the module to its initial state which allows us to configure it as needed. I don't know if the while loop is necessary since the manual says that the reset bit always reads low and it only says we must "wait two USB clock cycles". In any case, enough of a wait seems to be executed by the above code to allow it to reset properly.

The next section (4: Set BDT base registers) requires some explanation. Since the USB module doesn't have a dedicated memory block, we have to provide it. The BDT is the "Buffer Descriptor Table" and contains 16 * 4 entries that look like so:

1typedef struct {
2    uint32_t desc;
3    void* addr;
4} bdt_t;

"desc" is a descriptor for the buffer and "addr" is the address of the buffer. The exact bits of the "desc" are explained in the manual (p. 971, Table 41-4), but they basically specify ownership of the buffer (user program or USB module) and the USB token that generated the data in the buffer (if applicable).

Each entry in the BDT corresponds to one of 4 buffers in one of the 16 USB endpoints: The RX even, RX odd, TX even, and TX odd. The RX and TX are pretty self explanatory...the module needs somewhere to read the data its going to send and somewhere to write the data it just received. The even and odd are a configuration that I have seen before in the PIC 18F4550 USB module: Ping-pong buffers. While one buffer is being sent/received by the module, the other can be in use by user code reading/writing (ping). When the user code is done with its buffers, it swaps buffers, giving the usb module control over the ones it was just using (pong). This allows seamless communication between the host and the device and minimizes the need for copying data between buffers. I have declared the BDT in my code as follows:

1#define BDT_INDEX(endpoint, tx, odd) ((endpoint << 2) | (tx << 1) | odd)
2__attribute__ ((section(".usbdescriptortable"), used))
3static bdt_t table[(USB_N_ENDPOINTS + 1)*4]; //max endpoints is 15 + 1 control

One caveat of the BDT is that it must be aligned with a 512-byte boundary in memory. Our code above showed that only 3 bytes of the 4 byte address of "table" are passed to the module. This is because the last byte is basically the index along the table (the specification of this is found in section 41.4.3, page 970 of the manual). The #define directly above the declaration is a helper macro for referencing entries in the table for specific endpoints (this is used later in the interrupt). Now, accomplishing this boundary alignment requires some modification of the linker script. Before this, I had never had any need to modify a linker script. We basically need to create a special area of memory (in the above, it is called ".usbdescriptortable" and the attribute declaration tells the compiler to place that variable's reference inside of it) which is aligned to a 512-byte boundary in RAM. I declared mine like so:

1.usbdescriptortable (NOLOAD) : {
2     . = ALIGN(512);
3     *(.usbdescriptortable*)
4} > sram

The position of this in the file is mildly important, so looking at the full linker script would probably be good. This particular declaration I more or less lifted from the Teensyduino linker script, with some changes to make it fit into my linker script.

Steps 5-6 set up the interrupts. There is only one USB interrupt, but there are two registers of flags. We first reset all of the flags. Interestingly, to reset a flag we write back a '1' to the particular flag bit. This has the effect of being able to set a flag register to itself to reset all of the flags since a flag bit is '1' when it is triggered. After resetting the flags, we enable the interrupt in the NVIC (Nested Vector Interrupt Controller). I won't discuss the NVIC much, but it is a fairly complex piece of hardware. It has support for lots and lots of interrupts (over 100) and separate priorities for each one. I don't have reliable code for setting interrupt priorities yet, but eventually I'll get around to messing with that. The "enable_irq()" call is a function that is provided in arm_cm4.c and all that it does is enable the interrupt specified by the passed vector number. These numbers are specified in the datasheet, but we have a #define specified in the mk20d7 header file (warning! 12000 lines ahead) which gives us the number.

The very last step in initialization is to set the internal pullup on D+. According to the USB specification, a pullup on D- specifies a low speed device (1.2Mbit/s) and a pullup on D+ specifies a full speed device (12Mbit/s). We want to use the higher speed grade. The Kinetis USB module does not support high speed (480Mbit/s) mode.

Part 3: The interrupt handler state machine

The USB protocol can be interpreted in the context of a state machine with each call to the interrupt being a "tick" in the machine. The interrupt handler must process all of the flags to determine what happened and where to go from there.

 1#define ENDP0_SIZE 64
 2
 3/**
 4 * Endpoint 0 receive buffers (2x64 bytes)
 5 */
 6static uint8_t endp0_rx[2][ENDP0_SIZE];
 7
 8//flags for endpoint 0 transmit buffers
 9static uint8_t endp0_odd, endp0_data = 0;
10
11/**
12 * Handler functions for when a token completes
13 * TODO: Determine if this structure really will work for all kinds of handlers
14 *
15 * I hope this looks like a dynamic jump table to the compiler
16 */
17static void (*handlers[USB_N_ENDPOINTS + 2]) (uint8_t);
18
19void USBOTG_IRQHandler(void)
20{
21    uint8_t status;
22    uint8_t stat, endpoint;
23
24    status = USB0_ISTAT;
25
26    if (status & USB_ISTAT_USBRST_MASK)
27    {
28        //handle USB reset
29
30        //initialize endpoint 0 ping-pong buffers
31        USB0_CTL |= USB_CTL_ODDRST_MASK;
32        endp0_odd = 0;
33        table[BDT_INDEX(0, RX, EVEN)].desc = BDT_DESC(ENDP0_SIZE, 0);
34        table[BDT_INDEX(0, RX, EVEN)].addr = endp0_rx[0];
35        table[BDT_INDEX(0, RX, ODD)].desc = BDT_DESC(ENDP0_SIZE, 0);
36        table[BDT_INDEX(0, RX, ODD)].addr = endp0_rx[1];
37        table[BDT_INDEX(0, TX, EVEN)].desc = 0;
38        table[BDT_INDEX(0, TX, ODD)].desc = 0;
39
40        //initialize endpoint0 to 0x0d (41.5.23)
41        //transmit, recieve, and handshake
42        USB0_ENDPT0 = USB_ENDPT_EPRXEN_MASK | USB_ENDPT_EPTXEN_MASK | USB_ENDPT_EPHSHK_MASK;
43
44        //clear all interrupts...this is a reset
45        USB0_ERRSTAT = 0xff;
46        USB0_ISTAT = 0xff;
47
48        //after reset, we are address 0, per USB spec
49        USB0_ADDR = 0;
50
51        //all necessary interrupts are now active
52        USB0_ERREN = 0xFF;
53        USB0_INTEN = USB_INTEN_USBRSTEN_MASK | USB_INTEN_ERROREN_MASK |
54            USB_INTEN_SOFTOKEN_MASK | USB_INTEN_TOKDNEEN_MASK |
55            USB_INTEN_SLEEPEN_MASK | USB_INTEN_STALLEN_MASK;
56
57        return;
58    }
59    if (status & USB_ISTAT_ERROR_MASK)
60    {
61        //handle error
62        USB0_ERRSTAT = USB0_ERRSTAT;
63        USB0_ISTAT = USB_ISTAT_ERROR_MASK;
64    }
65    if (status & USB_ISTAT_SOFTOK_MASK)
66    {
67        //handle start of frame token
68        USB0_ISTAT = USB_ISTAT_SOFTOK_MASK;
69    }
70    if (status & USB_ISTAT_TOKDNE_MASK)
71    {
72        //handle completion of current token being processed
73        stat = USB0_STAT;
74        endpoint = stat >> 4;
75        handlers[endpoint](stat);
76
77        USB0_ISTAT = USB_ISTAT_TOKDNE_MASK;
78    }
79    if (status & USB_ISTAT_SLEEP_MASK)
80    {
81        //handle USB sleep
82        USB0_ISTAT = USB_ISTAT_SLEEP_MASK;
83    }
84    if (status & USB_ISTAT_STALL_MASK)
85    {
86        //handle usb stall
87        USB0_ISTAT = USB_ISTAT_STALL_MASK;
88    }
89}

The above code will be executed whenever the IRQ for the USB module fires. This function is set up in the crt0.S file, but with a weak reference, allowing us to override it easily by simply defining a function called USBOTG_IRQHandler. We then proceed to handle all of the USB interrupt flags. If we don't handle all of the flags, the interrupt will execute again, giving us the opportunity to fully process all of them.

Reading through the code is should be obvious that I have not done much with many of the flags, including USB sleep, errors, and stall. For the purposes of this super simple driver, we really only care about USB resets and USB token decoding.

The very first interrupt that we care about which will be called when we connect the USB device to a host is the Reset. The host performs this by bringing both data lines low for a certain period of time (read the USB basics stuff for more information). When we do this, we need to reset our USB state into its initial and ready state. We do a couple things in sequence:

  1. Initialize the buffers for endpoint 0. We set the RX buffers to point to some static variables we have defined which are simply uint8_t arrays of length "ENDP0_SIZE". The TX buffers are reset to null since nothing is going to be transmitted. One thing to note is that the ODDRST bit is flipped on in the USB0_CTL register. This is very important since it "syncronizes" the USB module with our code in terms of knowing whether the even or odd buffer should be used next for transmitting. When we do ODDRST, it sets the next buffer to be used to be the even buffer. We have a "user-space" flag (endp0_odd) which we reset at the same time so that we stay in sync with the buffer that the USB module is going to use.
  2. We enable endpoint 0. Specifically, we say that it can transmit, receive, and handshake. Enabled endpoints always handshake, but endpoints can either send, receive, or both. Endpoint 0 is specified as a reading and writing endpoint in the USB specification. All of the other endpoints are device-specific.
  3. We clear all of the interrupts. If this is a reset we obviously won't be doing much else.
  4. Set our USB address to 0. Each device on the USB bus gets an address between 0 and 127. Endpoint 0 is reserved for devices that haven't been assigned an address yet (i.e. have been reset), so that becomes our address. We will receive an address later via a command sent to endpoint 0.
  5. Activate all necessary interrupts. In the previous part where we discussed the initialization sequence we only enabled the reset interrupt. After being reset, we get to enable all of the interrupts that we will need to be able to process USB events.

After a reset the USB module will begin decoding tokens. While there are a couple different types of tokens, the USB module has a single interrupt for all of them. When a token is decoded the module gives us information about what endpoint the token was for and what BDT entry should be used. This information is contained in the USB0_STAT register.

The exact method for processing these tokens is up to the individual developer. My choice for the moment was to make a dynamic jump table of sorts which stores 16 function pointers which will be called in order to process the tokens. Initially, these pointers point to dummy functions that do nothing. The code for the endpoint 0 handler will be discussed in the next section.

Our code here uses USB0_STAT to determine which endpoint the token was decoded for, finds the appropriate function pointer, and calls it with the value of USB0_STAT.

Part 4: Token processing & descriptors

This is one part of the driver that isn't something that must be done a certain way, but however it is done, it must accomplish the task correctly. My super-simple driver processes this in two stages: Processing the token type and processing the token itself.

As mentioned in the previous section, I had a handler for each endpoint that would be called after a token was decoded. The handler for endpoint 0 is as follows:

 1#define PID_OUT   0x1
 2#define PID_IN    0x9
 3#define PID_SOF   0x5
 4#define PID_SETUP 0xd
 5
 6typedef struct {
 7    union {
 8        struct {
 9            uint8_t bmRequestType;
10            uint8_t bRequest;
11        };
12        uint16_t wRequestAndType;
13    };
14    uint16_t wValue;
15    uint16_t wIndex;
16    uint16_t wLength;
17} setup_t;
18
19/**
20 * Endpoint 0 handler
21 */
22static void usb_endp0_handler(uint8_t stat)
23{
24    static setup_t last_setup;
25
26    //determine which bdt we are looking at here
27    bdt_t* bdt = &table[BDT_INDEX(0, (stat & USB_STAT_TX_MASK) >> USB_STAT_TX_SHIFT, (stat & USB_STAT_ODD_MASK) >> USB_STAT_ODD_SHIFT)];
28
29    switch (BDT_PID(bdt->desc))
30    {
31    case PID_SETUP:
32        //extract the setup token
33        last_setup = *((setup_t*)(bdt->addr));
34
35        //we are now done with the buffer
36        bdt->desc = BDT_DESC(ENDP0_SIZE, 1);
37
38        //clear any pending IN stuff
39        table[BDT_INDEX(0, TX, EVEN)].desc = 0;
40        table[BDT_INDEX(0, TX, ODD)].desc = 0;
41        endp0_data = 1;
42
43        //run the setup
44        usb_endp0_handle_setup(&last_setup);
45
46        //unfreeze this endpoint
47        USB0_CTL = USB_CTL_USBENSOFEN_MASK;
48        break;
49    case PID_IN:
50        if (last_setup.wRequestAndType == 0x0500)
51        {
52            USB0_ADDR = last_setup.wValue;
53        }
54        break;
55    case PID_OUT:
56        //nothing to do here..just give the buffer back
57        bdt->desc = BDT_DESC(ENDP0_SIZE, 1);
58        break;
59    case PID_SOF:
60        break;
61    }
62
63    USB0_CTL = USB_CTL_USBENSOFEN_MASK;
64}

The very first step in handling a token is determining the buffer which contains the data for the token transmitted. This is done by the first statement which finds the appropriate address for the buffer in the table using the BDT_INDEX macro which simply implements the addressing form found in Figure 41-3 in the family manual.

After determining where the data received is located, we need to determine which token exactly was decoded. We only do things with four of the tokens. Right now, if a token comes through that we don't understand, we don't really do anything. My thought is that I should be initiating an endpoint stall, but I haven't seen anywhere that specifies what exactly I should do for an unrecognized token.

The main token that we care about with endpoint 0 is the SETUP token. The data attached to this token will be in the format described by setup_t, so the first step is that we dereference and cast the buffer into which the data was loaded into a setup_t. This token will be stored statically since we need to look at it again for tokens that follow, especially in the case of the IN token following the request to be assigned an address.

One part of processing a setup token that tripped me up for a while was what the next DATA state should be. The USB standard specifies that the data in a frame is either marked DATA0 or DATA1 and it alternates by frame. This information is stored in a flag that the USB module will read from the first 4 bytes of the BDT (the "desc" field). Immediately following a SETUP token, the next DATA transmitted must be a DATA1.

After this, the setup function is run (more on that next) and as a final step, the USB module is "unfrozen". Whenever a token is being processed, the USB module "freezes" so that processing can occur. While I haven't yet read enough documentation on the subject, it seems to me that this is to give the user program some time to actually handle a token before the USB module decodes another one. I'm not sure what happens if the user program takes to long, but I imagine some error flag will go off.

The guts of handling a SETUP request are as follows:

  1typedef struct {
  2    uint8_t bLength;
  3    uint8_t bDescriptorType;
  4    uint16_t bcdUSB;
  5    uint8_t bDeviceClass;
  6    uint8_t bDeviceSubClass;
  7    uint8_t bDeviceProtocol;
  8    uint8_t bMaxPacketSize0;
  9    uint16_t idVendor;
 10    uint16_t idProduct;
 11    uint16_t bcdDevice;
 12    uint8_t iManufacturer;
 13    uint8_t iProduct;
 14    uint8_t iSerialNumber;
 15    uint8_t bNumConfigurations;
 16} dev_descriptor_t;
 17
 18typedef struct {
 19    uint8_t bLength;
 20    uint8_t bDescriptorType;
 21    uint8_t bInterfaceNumber;
 22    uint8_t bAlternateSetting;
 23    uint8_t bNumEndpoints;
 24    uint8_t bInterfaceClass;
 25    uint8_t bInterfaceSubClass;
 26    uint8_t bInterfaceProtocol;
 27    uint8_t iInterface;
 28} int_descriptor_t;
 29
 30typedef struct {
 31    uint8_t bLength;
 32    uint8_t bDescriptorType;
 33    uint16_t wTotalLength;
 34    uint8_t bNumInterfaces;
 35    uint8_t bConfigurationValue;
 36    uint8_t iConfiguration;
 37    uint8_t bmAttributes;
 38    uint8_t bMaxPower;
 39    int_descriptor_t interfaces[];
 40} cfg_descriptor_t;
 41
 42typedef struct {
 43    uint16_t wValue;
 44    uint16_t wIndex;
 45    const void* addr;
 46    uint8_t length;
 47} descriptor_entry_t;
 48
 49/**
 50 * Device descriptor
 51 * NOTE: This cannot be const because without additional attributes, it will
 52 * not be placed in a part of memory that the usb subsystem can access. I
 53 * have a suspicion that this location is somewhere in flash, but not copied
 54 * to RAM.
 55 */
 56static dev_descriptor_t dev_descriptor = {
 57    .bLength = 18,
 58    .bDescriptorType = 1,
 59    .bcdUSB = 0x0200,
 60    .bDeviceClass = 0xff,
 61    .bDeviceSubClass = 0x0,
 62    .bDeviceProtocol = 0x0,
 63    .bMaxPacketSize0 = ENDP0_SIZE,
 64    .idVendor = 0x16c0, //VOTI VID/PID for use with libusb
 65    .idProduct = 0x05dc,
 66    .bcdDevice = 0x0001,
 67    .iManufacturer = 0,
 68    .iProduct = 0,
 69    .iSerialNumber = 0,
 70    .bNumConfigurations = 1
 71};
 72
 73/**
 74 * Configuration descriptor
 75 * NOTE: Same thing about const applies here
 76 */
 77static cfg_descriptor_t cfg_descriptor = {
 78    .bLength = 9,
 79    .bDescriptorType = 2,
 80    .wTotalLength = 18,
 81    .bNumInterfaces = 1,
 82    .bConfigurationValue = 1,
 83    .iConfiguration = 0,
 84    .bmAttributes = 0x80,
 85    .bMaxPower = 250,
 86    .interfaces = {
 87        {
 88            .bLength = 9,
 89            .bDescriptorType = 4,
 90            .bInterfaceNumber = 0,
 91            .bAlternateSetting = 0,
 92            .bNumEndpoints = 0,
 93            .bInterfaceClass = 0xff,
 94            .bInterfaceSubClass = 0x0,
 95            .bInterfaceProtocol = 0x0,
 96            .iInterface = 0
 97        }
 98    }
 99};
100
101static const descriptor_entry_t descriptors[] = {
102    { 0x0100, 0x0000, &dev_descriptor, sizeof(dev_descriptor) },
103    { 0x0200, 0x0000, &cfg_descriptor, 18 },
104    { 0x0000, 0x0000, NULL, 0 }
105};
106
107static void usb_endp0_transmit(const void* data, uint8_t length)
108{
109    table[BDT_INDEX(0, TX, endp0_odd)].addr = (void *)data;
110    table[BDT_INDEX(0, TX, endp0_odd)].desc = BDT_DESC(length, endp0_data);
111    //toggle the odd and data bits
112    endp0_odd ^= 1;
113    endp0_data ^= 1;
114}
115
116/**
117 * Endpoint 0 setup handler
118 */
119static void usb_endp0_handle_setup(setup_t* packet)
120{
121    const descriptor_entry_t* entry;
122    const uint8_t* data = NULL;
123    uint8_t data_length = 0;
124
125
126    switch(packet->wRequestAndType)
127    {
128    case 0x0500: //set address (wait for IN packet)
129        break;
130    case 0x0900: //set configuration
131        //we only have one configuration at this time
132        break;
133    case 0x0680: //get descriptor
134    case 0x0681:
135        for (entry = descriptors; 1; entry++)
136        {
137            if (entry->addr == NULL)
138                break;
139
140            if (packet->wValue == entry->wValue && packet->wIndex == entry->wIndex)
141            {
142                //this is the descriptor to send
143                data = entry->addr;
144                data_length = entry->length;
145                goto send;
146            }
147        }
148        goto stall;
149        break;
150    default:
151        goto stall;
152    }
153
154    //if we are sent here, we need to send some data
155    send:
156        if (data_length > packet->wLength)
157            data_length = packet->wLength;
158        usb_endp0_transmit(data, data_length);
159        return;
160
161    //if we make it here, we are not able to send data and have stalled
162    stall:
163        USB0_ENDPT0 = USB_ENDPT_EPSTALL_MASK | USB_ENDPT_EPRXEN_MASK | USB_ENDPT_EPTXEN_MASK | USB_ENDPT_EPHSHK_MASK;
164}

This is the part that took me the longest once I managed to get the module talking. Handling of SETUP tokens on endpoint 0 must be done in a rather exact fashion and the slightest mistake gives some very cryptic errors.

This is a very very very minimalistic setup token handler and is not by any means complete. It does only what is necessary to get the computer to see the device successfully read its descriptors. There is no functionality for actually doing things with the USB device. Most of the space is devoted to actually returning the various descriptors. In this example, the descriptor is for a device with a single configuration and a single interface which uses no additional endpoints. In a real device, this would almost certainly not be the case (unless one uses V-USB...this is how V-USB sets up their device if no other endpoints are compiled in).

The SETUP packet comes with a "request" and a "type". We process these as one word for simplicity. The above shows only the necessary commands to actually get this thing to connect to a Linux machine running the standard USB drivers that come with the kernel. I have not tested it on Windows and it may require some modification to work since it doesn't implement all of the necessary functionality. A description of the functionality follows:

  • Set address (0x0500): This is a very simple command. All it does is wait for the next IN token. Upon receipt of this token, the address is considered "committed" and the USB module is told of its new address (see the endpoint 0 handler function above (not the setup handler)).
  • Set configuration (0x0900): This command can be complex, but I have stripped it down for the purposes of this example. Normally, during this command the USB module would be set up with all the requisite BDT entries for the endpoints described by the selected configuration. Since we only have one possible configuration and it doesn't use any additional endpoints, we basically do nothing. Once I start added other endpoints to this, all of the setup for those endpoints will go in here. This is the equivalent of the RESET handler for non-zero endpoints in terms of the operations that occur. If the Set Interface command was implemented, it would have similar functionality. More about this command can be read in the referenced USB basics website.
  • Get descriptor (0x0680, 0x0681): In reality, this is two commands: Get descriptor and get interface. However, due to the structure we have chosen in storing the descriptors, these two commands can be merged. This is the most complex part of this particular driver and is influenced heavily by the way things are done with the Teensyduino driver since I thought they had a very efficient pattern. Basically, it uses the wIndex and wValue to find a pointer to some data to return, whether that be the device descriptor, the configuration descriptor, a string, or something else. In our case, we have only the device descriptor and the configuration descriptor. Adding a string would be trivial, however, and the exact wIndex and wValue combination for that is described in the USB basics. The wIndex for strings matches with any of the several i* (iManufacturer, iProduct, etc) which may be specified.
  • default: When an unrecognized command is received, we enter a stall. This is basically the USB way of saying "uhh...I don't know what to do here" and requires the host to un-stall the endpoint before it can continue. From what I gather, there isn't really much the user code has to do other than declare that a stall has occurred. The USB module seems to take care of the rest of that.

After handling a command and determining that it isn't a stall, the transmission is set up. At the moment, I only have transmission set up for a maximum of 64 bytes. In reality, this is limited by the wLength transmitted with the setup packet (note the if statement before the call to usb_endp0_transmit), but as far as I have seen this is generally the same as the length of the endpoint (I could be very wrong here...so watch out for that one). However, it would be fairly straightfoward to allow it to transmit more bytes: Upon receipt of an IN token, just check if we have reached the end of what we are supposed to transmit. If not, point the next TX buffer to the correct starting point and subtract the endpoint size from the remaining length until we have transmitted all of the bytes. Although the endpoint size is 64 bytes, it is easy to transmit much more than that; it just takes multiple IN requests. The data length is given by the descriptors, so the host can determine when to stop sending IN requests.

During transmission, both the even and data flags are toggled. This ensures that we are always using the correct TX buffer (even/odd) and the DATA flag transmitted is valid.

The descriptors are the one part that can't really be screwed up here. Screwing up the descriptors causes interesting errors when the host tries to communicate. I did not like how the "reference" usb drivers I looked at generally defined descriptors: They used a char array. This works very well for the case where there are a variable number of entries in the descriptor, but for my purposes I decided to use named structs so that I could match the values I had specified on my device to values I read from the host machine without resorting to counting bytes in the array. It's simply for easier reading and doesn't really give much more than that. It may even be more error prone because I am relying on the compiler packing the struct into memory in the correct order for transmission and in later versions I may end up using the char array method.

I won't delve into a long and drawn out description of what the USB descriptor has in it, but I will give a few points:

  • In Linux, the device descriptor is requested first and then the configuration descriptor after that. They are two separate commands, hence the two separate descriptor entries in my descriptor table.
  • The device descriptor must NOT be "const". For my compiler at least, this causes it to be placed into flash which, while a perfectly valid memory address that in general can be read, is inaccessible to the USB module. I spent a long time banging my head on this one saying "but it should work! why doesn't it work???" Moral of the story: Anything that is pointed to by a BDT entry (transmit buffers, receive buffers) must be located in main RAM, not in the flash. It must not be const.
  • A device must have at least one configuration. Linux, at least, didn't seem to like it very much when there were zero configurations and would put lots of errors into my log.
  • The configuration needs to have at least one interface. Specifying no interfaces caused the same problems as not specifying any configurations.
  • The configuration indices (bConfigurationValue) are 1-based and the interface indices (bInterfaceNumber) are zero based. I haven't fooled around with these enough to test the veracity of this claim fully, but it was the only configuration that I managed to get things working in.
  • The length values are very important. If these are not correct, the host will have some serious troubles reading the descriptors. I spend a while troubleshooting these. The main one to make sure of is the wTotalLength value in the configuration descriptor. Most of the others are pretty much always going to be the same.

Where to go from here

The driver I have implemented leaves much to be desired. This isn't meant to be a fully featured driver. Instead, its meant to be something of an introduction to getting the USB module to work on the bare metal without the support of some external dependency. A few things that would definitely need to be implemented are:

  • The full set of commands for the endpoint 0 SETUP token processing
  • A more expansive configuration that allows for having some bulk endpoints for sending data. The 64-byte limitation of packet size for endpoint 0 can cause some issues when attempting to actually utilize the full 12Mbit/s bandwidth. The USB protocol does actually add overhead and the less times that a token has to be invoked, the better.
  • Strings in the configuration. Right now, the configuration is essentially "blank" because it uses a shared VID/PID and doesn't specify a manufacturer, product, or serial number. It would be rather hard to identify this device using libusb on a system with multiple devices using that VID/PID combination.
  • Real error handling. Right now, the interrupt basically ignores the errors. In a real application, these would need to be handled.
  • A better structure. I am not a real fan of how I have structured this, but my idea was to make it "expandable" without needing to recompile usb.c every time a change was made. It doesn't achieve that yet, but in future iterations I hope to have a relatively portable usb driver module that I can port to other projects without modification, placing the other device-specific things into another, mimimalistic, file.

Conclusion

I can only hope that this discussion has been helpful. I spent a long time reading documentation, writing code, smashing my keyboard, and figuring things out and I would like to see that someone else could benefit from this. I hope as I learn more about using the modules on my Teensy that I will become more competent in understanding how many of the systems I rely on on a daily basis function.

The code I have included above isn't always complete, so I would definitely recommend actually reading the code in the repository referenced at the beginning of this article.

If there are any mistakes in the above, please let me know in the comments or shoot me an email.

Database Abstraction in Python

As I was recently working on trying out the Flask web framework for Python, I ended up wanting to access my MySQL database. Recently at work I have been using entity framework and I have gotten quite used to having a good database abstraction that allows programmatic creation of SQL. While such frameworks exist in Python, I thought it would interesting to try writing one. This is one great example of getting carried away with a seemingly simple task.

I aimed for these things:

  • Tables should be represented as objects which each instance of the object representing a row
  • These objects should be able to generate their own insert, select, and update queries
  • Querying the database should be accomplished by logical predicates, not by strings
  • Update queries should be optimized to only update those fields which have changed
  • The database objects should have support for "immutable" fields that are generated by the database

I also wanted to be able to do relations between tables with foreign keys, but I have decided to stop for now on that. I have a structure outlined, but it isn't necessary enough at this point since all I wanted was a database abstraction for my simple Flask project. I will probably implement it later.

This can be found as a gist here: https://gist.github.com/kcuzner/5246020

Example

Before going into the code, here is an example of what this abstraction can do as it stands. It directly uses the DbObject and DbQuery-inheriting objects which are shown further down in this post.

 1from db import *
 2import hashlib
 3
 4def salt_password(user, unsalted):
 5    if user is None:
 6        return unsalted
 7    m = hashlib.sha512()
 8    m.update(user.username)
 9    m.update(unsalted)
10    return m.hexdigest()
11
12class User(DbObject):
13    dbo_tablename = "users"
14    primary_key = IntColumn("id", allow_none=True, mutable=False)
15    username = StringColumn("username", "")
16    password = PasswordColumn("password", salt_password, "")
17    display_name = StringColumn("display_name", "")
18    def __init__(self, **kwargs):
19        DbObject.__init__(self, **kwargs)
20    @classmethod
21    def load(self, cur, username):
22        selection = self.select('u')
23        selection[0].where(selection[1].username == username)
24        result = selection[0].execute(cur)
25        if len(result) == 0:
26            return None
27        else:
28            return result[0]
29    def match_password(self, password):
30        salted = salt_password(self, password)
31        return salted == self.password
32
33#assume there is a function get_db defined which returns a PEP-249
34#database object
35def main():
36    db = get_db()
37    cur = db.cursor()
38    user = User.load(cur, "some username")
39    user.password = "a new password!"
40    user.save(cur)
41    db.commit()
42
43    new_user = User(username="someone", display_name="Their name")
44    new_user.password = "A password that will be hashed"
45    new_user.save(cur)
46    db.commmit()
47
48    print new_user.primary_key # this will now have a database assigned id

This example first loads a user using a DbSelectQuery. The user is then modified and the DbObject-level function save() is used to save it. Next, a new user is created and saved using the same function. After saving, the primary key will have been populated and will be printed.

Change Tracking Columns

I started out with columns. I needed columns that track changes and have a mapping to an SQL column name. I came up with the following:

  1class ColumnSet(object):
  2    """
  3    Object which is updated by ColumnInstances to inform changes
  4    """
  5    def __init__(self):
  6        self.__columns = {} # columns are sorted by name
  7        i_dict = type(self).__dict__
  8        for attr in i_dict:
  9            obj = i_dict[attr]
 10            if isinstance(obj, Column):
 11                # we get an instance of this column
 12                self.__columns[obj.name] = ColumnInstance(obj, self)
 13
 14    @property
 15    def mutated(self):
 16        """
 17        Returns the mutated columns for this tracker.
 18        """
 19        output = []
 20        for name in self.__columns:
 21            column = self.get_column(name)
 22            if column.mutated:
 23                output.append(column)
 24        return output
 25
 26    def get_column(self, name):
 27        return self.__columns[name]
 28
 29class ColumnInstance(object):
 30    """
 31    Per-instance column data. This is used in ColumnSet objects to hold data
 32    specific to that particular instance
 33    """
 34    def __init__(self, column, owner):
 35        """
 36        column: Column object this is created for
 37        initial: Initial value
 38        """
 39        self.__column = column
 40        self.__owner = owner
 41        self.update(column.default)
 42
 43    def update(self, value):
 44        """
 45        Updates the value for this instance, resetting the mutated flag
 46        """
 47        if value is None and not self.__column.allow_none:
 48            raise ValueError("'None' is invalid for column '" +
 49                             self.__column.name + "'")
 50        if self.__column.validate(value):
 51            self.__value = value
 52            self.__origvalue = value
 53        else:
 54            raise ValueError("'" + str(value) + "' is not valid for column '" +
 55                             self.__column.name + "'")
 56
 57    @property
 58    def column(self):
 59        return self.__column
 60
 61    @property
 62    def owner(self):
 63        return self.__owner
 64
 65    @property
 66    def mutated(self):
 67        return self.__value != self.__origvalue
 68
 69    @property
 70    def value(self):
 71        return self.__value
 72
 73    @value.setter
 74    def value(self, value):
 75        if value is None and not self.__column.allow_none:
 76            raise ValueError("'None' is invalid for column '" +
 77                             self.__column.name + "'")
 78        if not self.__column.mutable:
 79            raise AttributeError("Column '" + self.__column.name + "' is not" +
 80                                 " mutable")
 81        if self.__column.validate(value):
 82            self.__value = value
 83        else:
 84            raise ValueError("'" + value + "' is not valid for column '" +
 85                             self.__column.name + "'")
 86
 87class Column(object):
 88    """
 89    Column descriptor for a column
 90    """
 91    def __init__(self, name, default=None, allow_none=False, mutable=True):
 92        """
 93        Initializes a column
 94
 95        name: Name of the column this maps to
 96        default: Default value
 97        allow_none: Whether none (db null) values are allowed
 98        mutable: Whether this can be mutated by a setter
 99        """
100        self.__name = name
101        self.__allow_none = allow_none
102        self.__mutable = mutable
103        self.__default = default
104
105    def validate(self, value):
106        """
107        In a child class, this will validate values being set
108        """
109        raise NotImplementedError
110
111    @property
112    def name(self):
113        return self.__name
114
115    @property
116    def allow_none(self):
117        return self.__allow_none
118
119    @property
120    def mutable(self):
121        return self.__mutable
122
123    @property
124    def default(self):
125        return self.__default
126
127    def __get__(self, owner, ownertype=None):
128        """
129        Gets the value for this column for the passed owner
130        """
131        if owner is None:
132            return self
133        if not isinstance(owner, ColumnSet):
134            raise TypeError("Columns are only allowed on ColumnSets")
135        return owner.get_column(self.name).value
136
137    def __set__(self, owner, value):
138        """
139        Sets the value for this column for the passed owner
140        """
141        if not isinstance(owner, ColumnSet):
142            raise TypeError("Columns are only allowed on ColumnSets")
143        owner.get_column(self.name).value = value
144
145class StringColumn(Column):
146    def validate(self, value):
147        if value is None and self.allow_none:
148            print "nonevalue"
149            return True
150        if isinstance(value, basestring):
151            print "isstr"
152            return True
153        print "not string", value, type(value)
154        return False
155
156class IntColumn(Column):
157    def validate(self, value):
158        if value is None and self.allow_none:
159            return True
160        if isinstance(value, int) or isinstance(value, long):
161            return True
162        return False
163
164class PasswordColumn(Column):
165    def __init__(self, name, salt_function, default=None, allow_none=False,
166                 mutable=True):
167        """
168        Create a new password column which uses the specified salt function
169
170        salt_function: a function(self, value) which returns the salted string
171        """
172        Column.__init__(self, name, default, allow_none, mutable)
173        self.__salt_function = salt_function
174    def validate(self, value):
175        return True
176    def __set__(self, owner, value):
177        salted = self.__salt_function(owner, value)
178        super(PasswordColumn, self).__set__(owner, salted)

The Column class describes the column and is implemented as a descriptor. Each ColumnSet instance contains multiple columns and holds ColumnInstance objects which hold the individual column per-object properties, such as the value and whether it has been mutated or not. Each column type has a validation function to help screen invalid data from the columns. When a ColumnSet is initiated, it scans itself for columns and at that moment creates its ColumnInstances.

Generation of SQL using logical predicates

The next thing I had to create was the database querying structure. I decided that rather than actually using the ColumnInstance or Column objects, I would use a go-between object that can be assigned a "prefix". A common thing to do in SQL queries is to rename the tables in the query so that you can reference the same table multiple times or use different tables with the same column names. So, for example if I had a table called posts and I also had a table called users and they both shared a column called 'last_update', I could assign a prefix 'p' to the post columns and a prefix 'u' to the user columns so that the final column name would be 'p.last_update' and 'u.last_update' for posts and users respectively.

Another thing I wanted to do was avoid the usage of SQL in constructing my queries. This is similar to the way that LINQ works for C#: A predicate is specified and later translated into an SQL query or a series of operations in memory depending on what is going on. So, in Python one of my queries looks like so:

1class Table(ColumnSet):
2    some_column = StringColumn("column_1", "")
3    another = IntColumn("column_2", 0)
4a_variable = 5
5columns = Table.get_columns('x') # columns with a prefix 'x'
6query = DbQuery() # This base class just makes a where statement
7query.where((columns.some_column == "4") & (columns.another > a_variable)
8print query.sql

This would print out a tuple (" WHERE x.column_1 = %s AND x.column_2 > %s", ["4", 5]). So, how does this work? I used operator overloading to create DbQueryExpression objects. The code is like so:

  1class DbQueryExpression(object):
  2    """
  3    Query expression created from columns, literals, and operators
  4    """
  5    def __and__(self, other):
  6        return DbQueryConjunction(self, other)
  7    def __or__(self, other):
  8        return DbQueryDisjunction(self, other)
  9
 10    def __str__(self):
 11        raise NotImplementedError
 12    @property
 13    def arguments(self):
 14        raise NotImplementedError
 15
 16class DbQueryConjunction(DbQueryExpression):
 17    """
 18    Query expression joining together a left and right expression with an
 19    AND statement
 20    """
 21    def __init__(self, l, r):
 22        DbQueryExpression.__ini__(self)
 23        self.l = l
 24        self.r = r
 25    def __str__(self):
 26        return str(self.l) + " AND " + str(self.r)
 27    @property
 28    def arguments(self):
 29        return self.l.arguments + self.r.arguments
 30
 31class DbQueryDisjunction(DbQueryExpression):
 32    """
 33    Query expression joining together a left and right expression with an
 34    OR statement
 35    """
 36    def __init__(self, l, r):
 37        DbQueryExpression.__init__(self)
 38        self.l = l
 39        self.r = r
 40    def __str__(self):
 41        return str(self.r) + " OR " + str(self.r)
 42    @property
 43    def arguments(self):
 44        return self.l.arguments + self.r.arguments
 45
 46class DbQueryColumnComparison(DbQueryExpression):
 47    """
 48    Query expression comparing a combination of a column and/or a value
 49    """
 50    def __init__(self, l, op, r):
 51        DbQueryExpression.__init__(self)
 52        self.l = l
 53        self.op = op
 54        self.r = r
 55    def __str__(self):
 56        output = ""
 57        if isinstance(self.l, DbQueryColumn):
 58            prefix = self.l.prefix
 59            if prefix is not None:
 60                output += prefix + "."
 61            output += self.l.name
 62        elif self.l is None:
 63            output += "NULL"
 64        else:
 65            output += "%s"
 66        output += self.op
 67        if isinstance(self.r, DbQueryColumn):
 68            prefix = self.r.prefix
 69            if prefix is not None:
 70                output += prefix + "."
 71            output += self.r.name
 72        elif self.r is None:
 73            output += "NULL"
 74        else:
 75            output += "%s"
 76        return output
 77    @property
 78    def arguments(self):
 79        output = []
 80        if not isinstance(self.l, DbQueryColumn) and self.l is not None:
 81            output.append(self.l)
 82        if not isinstance(self.r, DbQueryColumn) and self.r is not None:
 83            output.append(self.r)
 84        return output
 85
 86class DbQueryColumnSet(object):
 87    """
 88    Represents a set of columns attached to a specific DbOject type. This
 89    object dynamically builds itself based on a passed type. The columns
 90    attached to this set may be used in DbQueries
 91    """
 92    def __init__(self, dbo_type, prefix):
 93        d = dbo_type.__dict__
 94        self.__columns = {}
 95        for attr in d:
 96            obj = d[attr]
 97            if isinstance(obj, Column):
 98                column = DbQueryColumn(dbo_type, prefix, obj.name)
 99                setattr(self, attr, column)
100                self.__columns[obj.name] = column
101    def __len__(self):
102        return len(self.__columns)
103    def __getitem__(self, key):
104        return self.__columns[key]
105    def __iter__(self):
106        return iter(self.__columns)
107
108class DbQueryColumn(object):
109    """
110    Represents a Column object used in a DbQuery
111    """
112    def __init__(self, dbo_type, prefix, column_name):
113        self.dbo_type = dbo_type
114        self.name = column_name
115        self.prefix = prefix
116
117    def __lt__(self, other):
118        return DbQueryColumnComparison(self, "<", other)
119    def __le__(self, other):
120        return DbQueryColumnComparison(self, "<=", other)
121    def __eq__(self, other):
122        op = "="
123        if other is None:
124           op = " IS "
125       return DbQueryColumnComparison(self, op, other)
126    def __ne__(self, other):
127        op = "!="
128        if other is None:
129            op = " IS NOT "
130        return DbQueryColumnComparison(self, op, other)
131    def __gt__(self, other):
132        return DbQueryColumnComparison(self, ">", other)
133    def __ge__(self, other):
134        return DbQueryColumnComparison(self, ">=", other)

The __str__ function and arguments property return recursively generated expressions using the column prefixes (in the case of __str__) and the arguments (in the case of arguments). As can be seen, this supports parameterization of queries. To be honest, this part was the most fun since I was surprised it was so easy to make predicate expressions using a minimum of classes. One thing that I didn't like, however, was the fact that the boolean and/or operators cannot be overloaded. For that reason I had to use the bitwise operators, so the expressions aren't entirely correct when being read.

This DbQueryExpression is fed into my DbQuery object which actually does the translation to SQL. In the example above, we saw that I just passed a logical argument into my where function. This actually was a DbQueryExpression since my overloaded operators create DbQueryExpression objects when they are compared. The DbColumnSet object is an dynamically generated object containing the go-between column objects which is created from a DbObject. We will discuss the DbObject a little further down

The DbQuery objects are implemented as follows:

  1class DbQueryError(Exception):
  2    """
  3    Raised when there is an error constructing a query
  4    """
  5    def __init__(self, msg):
  6        self.message = msg
  7    def __str__(self):
  8        return self.message
  9
 10class DbQuery(object):
 11    """
 12    Represents a base SQL Query to a database based upon some DbObjects
 13
 14    All of the methods implemented here are valid on select, update, and
 15    delete statements.
 16    """
 17    def __init__(self, execute_filter=None):
 18        """
 19        callback: Function to call when the DbQuery is executed
 20        """
 21        self.__where = []
 22        self.__limit = None
 23        self.__orderby = []
 24        self.__execute_filter = execute_filter
 25    def where(self, expression):
 26        """Specify an expression to append to the WHERE clause"""
 27        self.__where.append(expression)
 28    def limit(self, value=None):
 29        """Specify the limit to the query"""
 30        self.__limit = value
 31    @property
 32    def sql(self):
 33        query = ""
 34        args = []
 35        if len(self.__where) > 0:
 36            where = self.__where[0]
 37            for clause in self.__where[1:]:
 38                where = where & clause
 39            args = where.arguments
 40            query += " WHERE " + str(where)
 41        if self.__limit is not None:
 42            query += " LIMIT " + self.__limit
 43        return query,args
 44    def execute(self, cur):
 45        """
 46        Executes this query on the passed cursor and returns either the result
 47        of the filter function or the cursor if there is no filter function.
 48        """
 49        query = self.sql
 50        cur.execute(query[0], query[1])
 51        if self.__execute_filter:
 52            return self.__execute_filter(self, cur)
 53        else:
 54            return cur
 55
 56class DbSelectQuery(DbQuery):
 57    """
 58    Creates a select query to a database based upon DbObjects
 59    """
 60    def __init__(self, execute_filter=None):
 61        DbQuery.__init__(self, execute_filter)
 62        self.__select = []
 63        self.__froms = []
 64        self.__joins = []
 65        self.__orderby = []
 66    def select(self, *columns):
 67        """Specify one or more columns to select"""
 68        self.__select += columns
 69    def from_table(self, dbo_type, prefix):
 70        """Specify a table to select from"""
 71        self.__froms.append((dbo_type, prefix))
 72    def join(self, dbo_type, prefix, on):
 73        """Specify a table to join to"""
 74        self.__joins.append((dbo_type, prefix, on))
 75    def orderby(self, *columns):
 76        """Specify one or more columns to order by"""
 77        self.__orderby += columns
 78    @property
 79    def sql(self):
 80        query = "SELECT "
 81        args = []
 82        if len(self.__select) == 0:
 83            raise DbQueryError("No selection in DbSelectQuery")
 84        query += ','.join([col.prefix + "." +
 85                 col.name for col in self.__select])
 86        if len(self.__froms) == 0:
 87            raise DbQueryError("No FROM clause in DbSelectQuery")
 88        for table in self.__froms:
 89            query += " FROM " + table[0].dbo_tablename + " " + table[1]
 90        if len(self.__joins) > 0:
 91            for join in self.__joins:
 92                query += " JOIN " + join[0].dbo_tablename + " " + join[1] +
 93                         " ON " + str(join[2])
 94        query_parent = super(DbSelectQuery, self).sql
 95        query += query_parent[0]
 96        args += query_parent[1]
 97        if len(self.__orderby) > 0:
 98           query += " ORDER BY " +
 99                    ','.join([col.prefix + "." +
100                    col.name for col in self.__orderby])
101        return query,args
102
103class DbInsertQuery(DbQuery):
104    """
105    Creates an insert query to a database based upon DbObjects. This does not
106    include any where or limit expressions
107    """
108    def __init__(self, dbo_type, prefix, execute_filter=None):
109        DbQuery.__init__(self, execute_filter)
110        self.table = (dbo_type, prefix)
111        self.__values = []
112    def value(self, column, value):
113        self.__values.append((column, value))
114    @property
115    def sql(self):
116        if len(self.__values) == 0:
117            raise DbQueryError("No values in insert")
118        tablename = self.table[0].dbo_tablename
119        query = "INSERT INTO {table} (".format(table=tablename)
120        args = [val[1] for val in self.__values
121                if val[0].prefix == self.table[1]]
122        query += ",".join([val[0].name for val in self.__values
123                          if val[0].prefix == self.table[1]])
124        query += ") VALUES ("
125        query += ",".join(["%s" for x in args])
126        query += ")"
127        return query,args
128
129class DbUpdateQuery(DbQuery):
130    """
131    Creates an update query to a database based upon DbObjects
132    """
133    def __init__(self, dbo_type, prefix, execute_filter=None):
134        """
135        Initialize the update query
136
137        dbo_type: table type to be updating
138        prefix: Prefix the columns are known under
139        """
140        DbQuery.__init__(self, execute_filter)
141        self.table = (dbo_type, prefix)
142        self.__updates = []
143    def update(self, left, right):
144        self.__updates.append((left, right))
145    @property
146    def sql(self):
147        if len(self.__updates) == 0:
148            raise DbQueryError("No update in DbUpdateQuery")
149        query = "UPDATE " + self.table[0].dbo_tablename + " " + self.table[1]
150        args = []
151        query += " SET "
152        for update in self.__updates:
153            if isinstance(update[0], DbQueryColumn):
154                query += update[0].prefix + "." + update[0].name
155            else:
156                query += "%s"
157                args.append(update[0])
158            query += "="
159            if isinstance(update[1], DbQueryColumn):
160                query += update[1].prefix + "." + update[1].name
161            else:
162                query += "%s"
163                args.append(update[1])
164        query_parent = super(DbUpdateQuery, self).sql
165        query += query_parent[0]
166        args += query_parent[1]
167        return query, args
168
169class DbDeleteQuery(DbQuery):
170    """
171    Creates a delete query for a database based on a DbObject
172    """
173    def __init__(self, dbo_type, prefix, execute_filter=None):
174        DbQuery.__init__(self, execute_filter)
175        self.table = (dbo_type, prefix)
176    @property
177    def sql(self):
178        query = "DELETE FROM " + self.table[0].dbo_tablename + " " +
179                self.table[1]
180        args = []
181        query_parent = super(DbDeleteQuery, self).sql
182        query += query_parent[0]
183        args += query_parent[1]
184        return query, args

Each of the SELECT, INSERT, UPDATE, and DELETE query types inherits from a base DbQuery which does execution and such. I decided to make the DbQuery object take a PEP 249-style cursor object and execute the query itself. My hope is that this will make this a little more portable since, to my knowledge, I didn't make the queries have any MySQL-specific constructions.

The different query types each implement a variety of statements corresponding to different parts of an SQL query: where(), limit(), orderby(), select(), from_table(), etc. These each take in either a DbQueryColumn (such as is the case with where(), orderby(), select(), etc) or a string to be appended to the query, such as is the case with limit(). I could easily have made limit take in two integers as well, but I was kind of rushing through because I wanted to see if this would even work. The query is built by creating the query object for the basic query type that is desired and then calling its member functions to add things on to the query.

Executing the queries can cause a callback "filter" function to be called which takes in the query and the cursor as arguments. I use this function to create new objects from the data or to update an object. It could probably be used for more clever things as well, but those two cases were my original intent in creating it. If no filter is specified, then the cursor is returned.

Table and row objects

At the highest level of this hierarchy is the DbObject. The DbObject definition actually represents a table in the database with a name and a single primary key column. Each instance represents a row. DbObjects also implement the methods for selecting records of their type and also updating themselves when they are changed. They inherit change tracking from the ColumnSet and use DbQueries to accomplish their querying goals. The code is as follows:

  1class DbObject(ColumnSet):
  2    """
  3    A DbObject is a set of columns linked to a table in the database. This is
  4    synonomous to a row. The following class attributes must be set:
  5
  6    dbo_tablename : string table name
  7    primary_key : Column for the primary key
  8    """
  9    def __init__(self, **cols):
 10        ColumnSet.__init__(self)
 11        for name in cols:
 12            c = self.get_column(name)
 13            c.update(cols[name])
 14
 15    @classmethod
 16    def get_query_columns(self, prefix):
 17        return DbQueryColumnSet(self, prefix)
 18
 19    @classmethod
 20    def select(self, prefix):
 21        """
 22        Returns a DbSelectQuery set up for this DbObject
 23        """
 24        columns = self.get_query_columns(prefix)
 25        def execute(query, cur):
 26            output = []
 27            block = cur.fetchmany()
 28            while len(block) > 0:
 29                for row in block:
 30                    values = {}
 31                    i = 0
 32                    for name in columns:
 33                        values[name] = row[i]
 34                        i += 1
 35                    output.append(self(**values))
 36                block = cur.fetchmany()
 37            return output
 38        query = DbSelectQuery(execute)
 39        query.select(*[columns[name] for name in columns])
 40        query.from_table(self, prefix)
 41        return query, columns
 42
 43    def get_primary_key_name(self):
 44        return type(self).__dict__['primary_key'].name
 45
 46    def save(self, cur):
 47        """
 48        Saves any changes to this object to the database
 49        """
 50        if self.primary_key is None:
 51            # we need to be saved
 52            columns = self.get_query_columns('x')
 53            def execute(query, cur):
 54                self.get_column(self.get_primary_key_name()
 55                                ).update(cur.lastrowid)
 56                selection = []
 57                for name in columns:
 58                    if name == self.get_primary_key_name():
 59                        continue #we have no need to update the primary key
 60                    column_instance = self.get_column(name)
 61                    if not column_instance.column.mutable:
 62                        selection.append(columns[name])
 63                if len(selection) != 0:
 64                    # we get to select to get additional computed values
 65                    def execute2(query, cur):
 66                        row = cur.fetchone()
 67                        index = 0
 68                        for s in selection:
 69                            self.get_column(s.name).update(row[index])
 70                            index += 1
 71                        return True
 72                    query = DbSelectQuery(execute2)
 73                    query.select(*selection)
 74                    query.from_table(type(self), 'x')
 75                    query.where(columns[self.get_primary_key_name()] ==
 76                                self.get_column(self.get_primary_key_name()
 77                                                ).value)
 78                    return query.execute(cur)
 79                return True
 80            query = DbInsertQuery(type(self), 'x', execute)
 81            for name in columns:
 82                column_instance = self.get_column(name)
 83                if not column_instance.column.mutable:
 84                    continue
 85                query.value(columns[name], column_instance.value)
 86            print query.sql
 87            return query.execute(cur)
 88        else:
 89            # we have been modified
 90            modified = self.mutated
 91            if len(modified) == 0:
 92                return True
 93            columns = self.get_query_columns('x')
 94            def execute(query, cur):
 95                for mod in modified:
 96                    mod.update(mod.value)
 97                return True
 98            query = DbUpdateQuery(type(self), 'x', execute)
 99            for mod in modified:
100                query.update(columns[mod.column.name], mod.value)
101            query.where(columns[self.get_primary_key_name()] == self.primary_key)
102            return query.execute(cur)

DbObjects require that the inheriting classes define two properties: dbo_tablename and primary_key. dbo_tablename is just a string giving the name of the table in the database and primary_key is a Column that will be used as the primary key.

To select records from the database, the select() function can be called from the class. This sets up a DbSelectQuery which will return an array of the DbObject that it is called for when the query is executed.

One fallacy of this structure is that at the moment it assumes that the primary key won't be None if it has been set. In other words, the way I did it right now does not allow for null primary keys. The reason it does this is because it says that if the primary key hasn't been set, it needs to generate a DbInsertQuery for the object when save() is called instead of a DbUpdateQuery. Both insert and update queries do not include every field. Immutable fields are always excluded and then later selected or inferred from the cursor object.