I have an HDMI 800x480 touch screen for the raspberry pi labelled "5inch HDMI LCD V2", which specifies that it has an XPT2046 Touch Controller. I was having difficulties getting the touch controller to work together with Ubuntu 'classic'.
Most tutorials to get the device to work simply specify that all you have to do is add the following to `/boot/config.txt`:
dtparam=i2c_arm=on
dtparam=spi=on
dtoverlay=ads7846,penirq=25,speed=10000,penirq_pull=2,xohms=150
Unfortunately there are a few differences with how the ubuntu image is set up in comparison to Raspbian. The first is that the config file is located in `/boot/firmware/config.txt`. The second is that rather than using the raspbian boot image for loading the linux kernel, the Ubuntu image uses `u-boot.bin`. By default this setup doesn't load any of the device tree overlay files, and so it just ignores the `dtoverlay` configuration parameter. I'm sure it is possible to get this to work, but for me the workaround was to revert to the raspberry pi bootloader following https://wiki.ubuntu.com/ARM/RaspberryPi#Change_the_bootloader.
In practice this means changing the first few lines of the config, changing the kernel parameter and adding the initramfs line, while commenting out device_tree:
kernel=vmlinuz
initramfs initrd.img followkernel
# device_tree_address=0x02000000
In addition to this, it is also necessary to copy the overlays to the boot folder, as well as the chipset device tree file (note this file and the kernel path may be different, just choose the latest kernel path and the correct bcm file):
cp -r /lib/firmware/4.15.0-1034-raspi2/device-tree/overlays/ /boot/firmware/
cp /lib/firmware/4.15.0-1034-raspi2/device-tree/bcm2709-rpi-2-b.dtb /boot/firmware/
After that the device should show up in `/dev/input/event0`.
You will probably still need to create the file `/usr/share/X11/xorg.conf.d/99-ads7846.conf`:
Section "InputClass"
Identifier "calibration"
MatchProduct "ADS7846 Touchscreen"
Option "Calibration" "3853 170 288 3796"
Option "SwapAxes" "1"
EndSection
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 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
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!
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
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 Registersstruct 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 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 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.