>Home

Simulating a PID control

Published 10/3/17

 

One thing that I have always wanted to do but never found the time for is simulating a PID controller with a heat source. In the following I describe how I do this in a small Excel spreadsheet.

The first nuisance related to this problem is that in order to simulate a PID controller, we need an enviroment to simulate. 

In our case we assume we have a medium which is divided into 5 compartments labeled Tn, where n is a number between 1 and 5. T1 is heated by a heat source, and the heat propagates through each of the compartments till it reaches compartment T5, which contains our sensor. Each heat compartment dissipates some heat along the way. This setup provides us with a non-linear, time delayed and exponential setup. A figure is shown below.

 

We first define a constant for the room temperature, which we call Tenv and we set it to 20 degrees Celcius. Then we define T loss, which is the amount of degrees per minute per degrees Celcius which are lost.

This allows us to define the loss using the following equation:

This allows us to define the equation of each compartment in the next timestep. The new temperature for compartments 2 to 5 are simply the average of compartment n and compartment n-1 (this could be weighted however) minus the losses due to heat dissipation. For the first compartment, we set it to the old temperature minus heat losses and add the heat depending on the duty cycle d and assuming that the heating element becomes less effective as the heat rises.

 

Finally we can define our PID duty equation, which has a target temperature of 70 degrees celcius, and makes use of the proportional integral and differential constants (Kp, Kd and Ki).

Given these equations, it is possible to pick some values, which allow for a fairly optimal graph. In this case the following values were chosen:

The effects of the different parts of a PID can be seen in the three figures below, the first being proportiona only, the second being proportional + differential and the last being all three:

 

The excel spreadsheet is available for download from here.

Debugging the STM32F103 with an ST LINK/V2 and OpenOCD

Published 10/3/17

Debugging is a very useful tool when working with Microcontrollers. I decided to order an ST-Link/V2, which is available for a few Euros (I paid roughly 6 EUR for mine). This device supports the SWD protocol for STM32 chips, which allows the chip to be programmed and debugged from 4 pins.

The first step is to connect the 4 pins. In the following picture, red is the target device voltage, grey is GND, green is IO and blue is CLK. Note that the device still needs external power, which I supply via the micro USB cable (not shown).

 

OpenOCD is an open source on chip debugger which can be downloaded from http://openocd.org/. It takes a client server approach, and the first step is to start the daemon which connects to the target device. This can be done using the following command, where -s tells openocd where to search for scripts and we tell it to use the stlink-v2 config as well as the stm32f1x config.

.\openocd.exe -s ..\scripts\ -f interface\stlink-v2.cfg -f target\stm32f1x.cfg

Once the OpenOCD daemon is started, we need to use telnet to connect to the device. For this we can connect to the local machine on port 4444, and can directly start issuing commands to the device.

telnet localhost 4444

With OpenOCD connected we can start using various commands. The first thing we can do is reset the device and tell it to break on the first command. This can be done by calling "reset halt" as follows:

> reset halt
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x00000152 msp: 0x20005000

As can be seen, our program counter (pc) is at 0x152, so if we want to see the assembler at that point, we can ask OpenOCD to disassemble it for us, using the arm disassemble command, giving it the address as well as the number of instructions to disassemble. 

> arm disassemble 0x152 20
0x00000152  0xe92d4ff0  PUSH.W  {r4, r5, r6, r7, r8, r9, r10, r11, r14}
0x00000156  0xb083      SUB     SP, #0xc
0x00000158  0xf2400004  MOVW    r0, #4  ; 0x004
0x0000015c  0xf2400108  MOVW    r1, #8  ; 0x008
0x00000160  0xf2c20000  MOVT    r0, #8192       ; 0x2000
0x00000164  0xf2c20100  MOVT    r1, #8192       ; 0x2000
0x00000168  0x1a09      SUBS    r1, r1, r0
0x0000016a  0x2901      CMP     r1, #0x01
0x0000016c  0xbfa8      IT      GE
0x0000016e  0xf000fb1c  BL      0x000007aa
0x00000172  0xf2400000  MOVW    r0, #0  ; 000
0x00000176  0xf2400104  MOVW    r1, #4  ; 0x004
0x0000017a  0xf2c20000  MOVT    r0, #8192       ; 0x2000
0x0000017e  0xf2c20100  MOVT    r1, #8192       ; 0x2000
0x00000182  0x1a0a      SUBS    r2, r1, r0
0x00000184  0x2a01      CMP     r2, #0x01
0x00000186  0xdb05      BLT     0x00000194
0x00000188  0xf24071f8  MOVW    r1, #2040       ; 0x7f8
0x0000018c  0xf2c00100  MOVT    r1, #0  ; 0000
0x00000190  0xf000fb00  BL      0x00000794

We can also use the debugger to dump raw memory, using the mdb command, we tell it where and how much memory to dump:

> mdb 0x0 64
0x00000000: 00 50 00 20 53 01 00 00 51 01 00 00 51 01 00 00 51 01 00 00 51 01 00 00 51 01 00 00 00 00 00 00
0x00000020: 51 01 00 00 51 01 00 00 00 00 00 00 51 01 00 00 51 01 00 00 51 01 00 00 51 01 00 00 51 01 00 00

Setting breakpoints can be done using the bp commands, and they can be removed using the rbp command, as in the following examples:

> bp 0x7aa 2
breakpoint set at 0x000007aa
> rbp 0x7aa

Otherwise there is the resume command, to continue execution in halted state, as well as the step command, to only execute a single opcode.

Another nice command is the reg command, which dumps all registers:

> reg
===== arm v7m registers
(0) r0 (/32): 0x20000004
(1) r1 (/32): 0x00000004
(2) r2 (/32): 0xD4F014C0
(3) r3 (/32): 0x04A82F37
(4) r4 (/32): 0x20000004
(5) r5 (/32): 0xFFCFFEDD
(6) r6 (/32): 0x0C06C5A6
(7) r7 (/32): 0x390A2280
(8) r8 (/32): 0x9A9F8FD8
(9) r9 (/32): 0xFFDF5B9D
(10) r10 (/32): 0xD4368C54
(11) r11 (/32): 0x680C1352
(12) r12 (/32): 0x7DBEF177
(13) sp (/32): 0x20005000
(14) lr (/32): 0xFFFFFFFF
(15) pc (/32): 0x00000152
(16) xPSR (/32): 0x01000000
(17) msp (/32): 0x20005000
(18) psp (/32): 0xC4E16220
(19) primask (/1): 0x00
(20) basepri (/8): 0x00
(21) faultmask (/1): 0x00
(22) control (/2): 0x00
===== Cortex-M DWT register
(23) dwt_ctrl (/32)
(24) dwt_cyccnt (/32)
(25) dwt_0_comp (/32)
(26) dwt_0_mask (/4)
(27) dwt_0_function (/32)
(28) dwt_1_comp (/32)
(29) dwt_1_mask (/4)
(30) dwt_1_function (/32)
(31) dwt_2_comp (/32)
(32) dwt_2_mask (/4)
(33) dwt_2_function (/32)
(34) dwt_3_comp (/32)
(35) dwt_3_mask (/4)
(36) dwt_3_function (/32)

The problem I was facing in http://localhost:8089/Home/6-rust-stm-handling-static-variables with my rust code crashing, seems to be that the code produces the following code for the memory initialization routine:

> arm disassemble 0x7aa 20
0x000007aa  0xb510      PUSH    {r4, r14}
0x000007ac  0x4604      MOV     r4, r0
0x000007ae  0x2901      CMP     r1, #0x01
0x000007b0  0xbfa4      ITE     GE
0x000007b2  0x4620      MOV     r0, r4
0x000007b4  0xf7fffff9  BL      0x000007aa

This code essentially ends up in an infinite loop, because the r1 register is never changed. Each time it loops it jumps to 0x7aa, which contains a push command causing the stack pointer to decrease. This continues untill the microcontroller overwrites memory which it is not allowed to write, causing it to land in a fault interrupt. I'm still not entirely sure why the compiler produces this code however.

This can be confirmed by running and then halting the microcontroller:

> resume
> halt
target halted due to debug-request, current mode: Handler HardFault
xPSR: 0x21000003 pc: 0x00000150 msp: 0x1fffffe0

 

Rust STM32 Handling Static Variables

Published 9/11/17

I've just pushed an example showing how to use code that contains statically initialized variables.

The general problem with static variables can be described as follows. Lets say we define a static variable INT_DATA and assign it some value as follows:

pub static mut INT_DATA : u32 = 5;

When we upload our Flash to the microcontroller now however, we are only overwriting the Flash partition. This means that the value 5 for our static variable needs to be stored in Flash. This would work fine, until somewhere in our application we have code which alters this variable such as:

unsafe { INT_DATA = 9 };

This is problematic because the variable is in Flash, where memory cannot be modified!

The solution to this problem is to store all variables with values that need to be initialized in our Flash, and to then copy them to RAM when the application is loaded. By default all variables with non zero values are stored in the .data section, while all zeroed static variables are stored in the .bss section, which must be set to zero during startup.

We start by specifying this in more detail in the linker script. A nice but cryptic resource on what is required is the following page: http://www.math.utah.edu/docs/info/ld_3.html#SEC18

We use the following updated linker script. We now define a section .etext which we use to get the location after everything else in the Flash memory (I did this because the example using SIZEOF didn't seem to quite work with our setup, but it might be possible). Using it we define the section .data, specifying AT to be the location of .etext, which makes the physical memory address of our data in the ROM be the location of .etext. Finally we also place .bss in RAM, without giving it an address in physical memory.

Within the linker script we also export all the addresses as symbol, so that we can use them within our code. The code _etext = . ; specifies that the current location should be stored under the _etext symbol, which we can use later in code. In this case _etext is the physical memory location of the variables requiring initialization, while _data and _edata mark the virtual addresses. The variables _bstart and _bend are the virtual addresses of the memory that needs to be cleared on startup.

MEMORY
{
  FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64K
  RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
  /* Set stack top to end of RAM */
  __StackTop = ORIGIN(RAM) + LENGTH(RAM);

  /* first entry should be the stack pointer and then the interrupt table */
  .ivtable :
  {
    LONG(__StackTop);
    KEEP(*(.ivtable));
  } > FLASH

  /* after that the program data is copied */
  .text :
  {
    *(.text*)
 *(.rodata*)
  } > FLASH
 
  .etext :
  {
 _etext = . ;
  } > FLASH

  /* The data has a virtual address in RAM, but is copied just after the program data */
  .data : AT ( ADDR(.etext) )
  {
    _data = . ;
    *(.data*)
 _edata = . ;
  } > RAM

  /* The data that receives a virtual address in RAM, but needs to be initialized to zero */
  .bss : {
    _bstart = . ;
    *(.bss*) *(COMMON)
 _bend = . ;
  } > RAM
}

Now we need a way to be able to get these addresses within our code. This can be done be defining external variables as follows. To get the address of _etext we can then use the code addr_of!(_etext) using the macro we define below.

extern {
    pub static _etext : u8;
    pub static _data : u8;
    pub static _edata : u8;
    pub static _bstart : u8;
    pub static _bend : u8;
}

#[macro_export]
macro_rules! addr_of {
    ($id:expr) => (
        (&$id as *const u8) as *mut u8
    )
}

We now need to define both memclr and memcpy functions, which we will need to initialize the memory. This is fairly straightforward and we use the following:

#[no_mangle]
pub unsafe extern fn __aeabi_memcpy(dest: *mut u8, src: *mut u8, n: isize) -> *mut u8 {
    let mut i =  0;
    while i < n {
        *dest.offset(i) = *src.offset(i);
        i += 1;
    }
    return dest;
}

#[no_mangle]
pub unsafe extern fn __aeabi_memclr(dest: *mut u8, n: isize) -> *mut u8 {
    let mut i =  0;
    while i < n {
        *dest.offset(i) = 0u8;
        i += 1;
    }
    return dest;
}

Finally we can define an init_mem function, which we need to call from our main function. This simply clears the .bss section using the memclr function, and it then uses memcpy to copy our data from flash to RAM:

pub unsafe fn init_mem() {
    // zero .bss section
    __aeabi_memclr(addr_of!(_bstart), addr_of!(_bend) as isize - addr_of!(_bstart) as isize);
    // copy .data section
    __aeabi_memcpy(addr_of!(_data), addr_of!(_etext), addr_of!(_edata) as isize - addr_of!(_data) as isize);
}

This allows the following program to produce the correct output:


pub static mut INT_DATA: u32 = 5; // static value
pub static mut INT_BSS: u32 = 0; // default value

pub fn main() {
    unsafe { stm32::mem::init_mem() };

    // setup system
    init_clock();
    enable_led();
    enable_uart();

    println("STM32 Memory Test App");
    println("  - by Rudi Horn");

    unsafe {
        print("int_data: "); print_int(INT_DATA); println("");
        print("int_bss: "); print_int(INT_BSS); println("");

        INT_DATA = 9; INT_BSS = 99;

        print("int_data: "); print_int(INT_DATA); println("");
        print("int_bss: "); print_int(INT_BSS); println("");
    }

    loop {
    }
}

Output:

STM32 Memory Test App
  - by Rudi Horn
int_data: 5
int_bss: 0
int_data: 9
int_bss: 99

For the full source code see https://github.com/rudihorn/rust-stm32f103-examples/tree/master/stm32-mem.

Small note: This example does not compile with --release for some reason. Unfortunately I don't have a debugger (yet) and so it is a bit difficult to find out what the exact cause is, but it seems to be the memclr function and may be related to the actual function call to memclr. If anyone has any ideas let me know!

 

Rust on STM32 now on Github

Published 9/6/17

I have decided to publish my stm32 library on github. https://github.com/rudihorn/rust-stm32f103

Along with it there is another project with an example: https://github.com/rudihorn/rust-stm32f103-examples

Compiling Rust for Cortex M3

Published 9/4/17

 

I was trying to get Rust to work on ARM based on the instructions found at http://www.acrawford.com/2017/03/09/rust-on-the-cortex-m3.html. Another good resource to look at is http://wiki.osdev.org/Raspberry_Pi_Bare_Bones_Rust. The goal is to be able to use rust on the mini STM32F103C8T6 development boards, which are cheap but make use of a powerful microcontroller. Here is another short summary of what is required.

Setup:

The first step is to install rust and set up nightly build (the nightly will apparently probably still be necessary till the end of 2017). Furthermore xargo is needed, which can be installed from the rust cargo repository.

curl https://sh.rustup.rs -sSf | sh
rustup update nightly
rustup default nightly
rustup component add rust-src
cargo intsall xargo

Install the gcc ARM linker:

apt-get install gcc-arm-none-eab

 

Now create a new project:

cargo init --bin myproject

 

The Cargo.toml file:

[package]
name = "arm-test"
version = "0.1.0"

[dependencies]

[profile.dev]
lto = true

[profile.release]
lto = true

Make the directory ".cargo" and then create the file .cargo/config:

[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi]
rustflags = [
    "-C", "link-arg=-nostartfiles",
    "-C", "link-arg=-Tlayout.ld",
]

We start by editing the file src/main.rs.

For our example we will need to refer to some registers. To do this we define two structs, PortRegisters and RCCRegisters, which refer to a set of consecutive registers that start at a location. We can make use of type aliases to tell the compiler that our registers have the type u32. Having a struct is useful, because if you look at the datasheet the same format of registers repeats itself. We can then just define two pointers for PORT_C and RCC which tell the compiler there is a struct of registers at the given address.

type Register = u32;

// Port Registers

struct PortRegisters {
 // 0x00: Control register lower
 crl : Register,

 // 0x04: Control register higher
 crh: Register,

 // 0x08: Input data register
 idr: Register,

 // 0x0c: Output data register
 odr: Register,

 // 0x10: Bit set / reset register
 bsrr: Register,
}

// Reset and Clock Control
struct RCCRegisters {
 // 0x00: Clock control register
 cr: Register,

 // 0x04: Clock configuration register
 cfgr: Register,

 // 0x08: Clock interrupt register
 cir: Register,

 // 0x0C: peripheral reset register
 apb2_rstr: Register,

 // 0x10: peripheral reset register
 apb1_rstr: Register,

 // 0x14: peripheral clock enable register
 ahb_enr: Register,

 // 0x18: peripheral clock enable register
 apb2_enr: Register,

}

const RCC: *mut RCCRegisters =    0x40021000 as *mut RCCRegisters;
const PORT_C: *mut PortRegisters = 0x40011000 as *mut PortRegisters;

Next we define the main function, which does three things. First it enables the clock for the PORT C peripheral. This is done by writing to the APB2 ENR register of RCC. After that it sets the pin C13 to an output, and sets the output value for the entire PORTC to 0 (keep in mind the other pins are floating). We mark this function as naked, because there is no stack yet.


#[naked]
fn main() {
 unsafe {
  // enable clock for PORTC
  (*RCC).apb2_enr = (1 << 4);
  // set port C13 to output
  (*PORT_C).crh= 0x44344444;
  // set output data registor to 0
  (*PORT_C).odr = 0;
 }

 loop {}
}

We can then define the interrupts function. We want all other interrupts to simply stop the microcontroller in a loop.

#[naked]
fn interrupts() {
 loop {}
}

If we look at the default / sample application which was downloaded off the newly bought STM device, we can see that the binary of the file starts with the value 0x20000400, which is the default stack pointer location. After that it contains the address 0x80000145, which defines the memory address referring to the first instruction in flash. The further 32 bit words contain the entry points for interrupts. Note that the flash is mapped into memory twice, once at 0x08000000 and again at 0x00000000. This is why our code works, even though as you may see the memory addresses for the entry point differ between our sample and the app we generate. This file is normally located at 0x0800000 in memory.

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000  00 04 00 20 45 01 00 08 E7 02 00 08 DF 02 00 08  ... E...ç...ß...
00000010  E3 02 00 08 8D 01 00 08 5D 04 00 08 00 00 00 00  ã.......].......
00000020  00 00 00 00 00 00 00 00 00 00 00 00 0D 03 00 08  ................
00000030  91 01 00 08 00 00 00 00 E9 02 00 08 F9 03 00 08  ‘.......é...ù...
00000040  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000050  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000060  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000070  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000080  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000090  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
000000A0  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
000000B0  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
000000C0  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
000000D0  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
000000E0  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
000000F0  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000100  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000110  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000120  5F 01 00 08 5F 01 00 08 5F 01 00 08 5F 01 00 08  _..._..._..._...
00000130  DF F8 0C D0 00 F0 18 F8 00 48 00 47 81 04 00 08  ßø.Ð.ð.ø.H.G....
00000140  00 04 00 20 06 48 80 47 06 48 00 47 FE E7 FE E7  ... .H€G.H.Gþçþç
00000150  FE E7 FE E7 FE E7 FE E7 FE E7 FE E7 FE E7 FE E7  þçþçþçþçþçþçþçþç
00000160  FD 03 00 08 31 01 00 08 06 4C 07 4D 06 E0 E0 68  ý...1....L.M.ààh
00000170  40 F0 01 03 94 E8 07 00 98 47 10 34 AC 42 F6 D3  @ð..”è..˜G.4¬BöÓ
00000180  FF F7 DA FF BC 04 00 08 CC 04 00 08 00 BF FE E7  ÿ÷Úÿ¼...Ì....¿þç

We can define this in rust using a structure which contains our interrupt vector table. We then define a static instance of this struct, and define the entrypoint to be the function main, while all of the other interrupts are set to a dummy interrupt which just loops. Note that the #[link_section] directive defines the section which we refer to later in the linker script. Also note the #[used] directive, this is very important for compiling for release, as otherwise everything is optimized away!

type IVFunction = fn ();

struct IVTable {
 main: IVFunction,
 nmi_handler: IVFunction,
 hard_fault: IVFunction,
 bus_fault: IVFunction,
 usage_fault: IVFunction,
}

#[link_section = ".ivtable"]
#[used]

static IVTABLE: IVTable = IVTable {
 main: main,
 nmi_handler: interrupts,
 hard_fault: interrupts,
 bus_fault: interrupts,
 usage_fault: interrupts
};

At the beginnig of the rust file we need to remove the std library, declare that there isn't a main function and the features for assembler, language items, start and naked functions. Then we need to declare two dummy functions for panics and "eh_personality".

#![no_std]
#![no_main]
#![feature(asm, lang_items, start, naked_functions)]

#[lang = "panic_fmt"]
pub extern fn rust_begin_unwind(_: ::core::fmt::Arguments, _: &'static str, _: u32) -> ! {
 loop {}
}

#[lang = "eh_personality"]
pub extern fn rust_eh_unwind_resume() {
}

The compiler requires a linker script to know how to build the binary. We first declare the memory layout of our device and then we declare a a text section, which should first contain a long word with the stack address, which we calculate as the top of the RAM address and then it should contain the ivtable. We then skip forward to address 0xD8, and include the rest of the ".text" section there (our main program execution code, including the main method).

MEMORY
{
  FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64K
  RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
  /* Set stack top to end of RAM */
  __StackTop = ORIGIN(RAM) + LENGTH(RAM);

  .text :
  {
    LONG(__StackTop);
    KEEP(*(.ivtable));

        . = 0xD8;

    *(.text*)
  } > FLASH
}

We can then build our application using the following commands:

xargo build
arm-none-eabi-objcopy -O binary target/thumbv7m-none-eabi/debug/<myproj> <myproj>.bin

If you wan't you can dissasemble it using the following command, which I recommend doing if you run into any bugs. The assembly output can be seen at http://files.rudi-horn.de/website/2017/09/arm-test-dump.txt.

arm-none-eabi-objdump -d target/thumbv7m-none-eabi/debug/<myproj>

 

I then flashed the .bin file to the microcontroller using the flash tool available at http://www.st.com/en/development-tools/flasher-stm32.html.

For the full source code: http://files.rudi-horn.de/website/2017/09/arm-test.tar.gz

Bottle Cleaner Prototype

Published 7/15/17

Bottle cleaning can be a tedious and time consuming task to perform. As such I have been trying to experiment with creating something to clean bottles, similar to projects using the FastRack bottle drying tray (e.g. https://www.youtube.com/watch?v=_iJcG6HbSsE).

The prototype consists out of 20mm outer diameter PVC conduit pipe with 5mm outer diameter copper pipe glued in 80mm intervals 3.5" intervals (estimated to be the distance of FastRack, without actually having a FastRack to measure from). The copper pipe is slightly squeezed together at the ends in order to increase pressure within the system.

The centrifugal submersible pump used can be bought off Aliexpress and is roughly sufficient for 4 bottles at a time (but cheap enough if more are needed): https://www.aliexpress.com/item/New-10L-min-12V-DC-Mini-Brushless-Motor-Submersible-Water-Pump-3-3M-Amphibious/32731443626.html

 

The bottle holder is a quick 3D printed mockup (took roughly 7 hours to print, so not really suitable for the entire device but good for testing):

Update: I ended up ordering a Fastrack, and the 3x4 bottle tray seems to have a hole distance of 80mm (surprisingly metric ☺).

Welcome to my new Site!

Published 7/14/17

Welcome to my new webpage!

My homepage is finally dynamically editable without having to change the project itself, meaning I am hoping to contribute much more actively in the future. This is also because the website supports blog style entries, meaning less significant projects can just be posted as blog entries.

For more information about me check out my about page or any of the other projects posted on this website.