RK3308 synth research phase

mitch5th September 2021 at 3:11pm
linux live rk3308

In 2019 or early 2020, I bought some interesting dev boards:

These have been collecting dust until recently, when I realized it actually has the perfect hardware for multichannel audio, and the Cortex A35 cores seem to run circles around the previous Allwinner H3 Cortex-A7. This is based purely on different benchmark tests, and thermal dissipation while idle and under load.

Now, I have a thermal camera, about 3 years experience actually maintaining a linux product at a real job, and I'm much more comfortable with everything I used to do regarding the synth. I'm confident that I can make the RK3308 work for me, with much faster turnaround time. The best part is that I found an RK3308 SoM, which is actually ideal. I plan to use the same peripheral setup as before:

  • STM32 (or GD32) for OSC messaging from user inputs
  • Wifi over USB (if the SoM doesn't have wifi built in, I have not received them yet)
  • ILI9341 (parallel using the LCD bus of the RK3308 this time, instead of SPI)
  • RF transmit/receive controlled over I2C (QN8066, not KT0803L)
  • 14500 batteries (Not the huge, flat, kindle 3 battery)
  • BD9G401EFJ-ME2 Buck converter (in lieu of the QFN adafruit buck-boost I was using)
  • Everest I2S codecs instead of shady C-Media USB audio codecs
    • ES7134LV I2S DAC @ 192KHz, $0.20 @ Qty 10
    • ES7240S, I2S ADC @ 192KHz, $0.35 Qty 10

Of course, most of these items are barely in stock, or extremely expensive. I seem to have bought in bulk, so I have enough here for a prototype or two, and at least one finished working device. It looks like I ordered the I2S codecs on Apr 14, 2019, and currently they're not in stock anywhere. You can find shady dealers on eBay.

So far, I have taken a look at Armbian, which has upped their game considerably in the 3 years I havent been looking, and the Radxa vendor board support packages. I'm interested in getting it going with buildroot first, because that worked extremely well last time, and eventually graduating to Yocto for actual package management and parallel builds, while creating a branch of Armbian so I can switch_root to a Debian filesystem for dev on-device or to allow the users to have the option.

I'm worried about my browser crashing due to too many open tabs, so heres the hitlist:

I'm worried I go way too deep into research instead of actually doing, so this post should mark the END of the research phase! Time to apply all that information to boot the MVP:

  • A simple /dev/fb0 that I can draw on using whatever
  • Audio in/out using I2S0
  • Fast booting kernel
    • Realtime, eventually
  • Puredata, xauth, python, ssh, taskset, bash

Intro to Direct Digital Synthesis WIP

mitch5th September 2021 at 3:11pm
live stm8

I bought some 25 cent 8 bit microcontrollers from lcsc.com , and while the price has jumped up to $1.12 each if you buy 30, and I figured this would be a perfect application for a dead-simple VCO. 8kb ROM, 1kb RAM, 16MHz, a 16 bit advanced timer, it looks pretty stacked for a quarter.

I remember a pretty clear article regarding DDS from hackaday, which I largely based my work on. I have this strange notion that, given a simple header file, I should be able to do what I want to do with this chip, and nothing extra. I'm not sure that this is the correct way to go about this if you actually want to build something in a week.

The first struggle was that I thought, somehow, that if I could generate 3 different voices with the 'advanced' timer 1, the rest would be simple. The bet was to put a lot of work into getting that figured out, and then putting the fluff on at the end of that. This, I believe, may have been a bad assumption. Lets begin:

I need to set up a bunch of registers for the timer. Some are obvious, like the prescaler and auto-reload register. What murdered me were the configuration register, assigning the correct interrupt, and being unfamiliar with the stm8 series. I eventually, after using a brute-force method when I knew I was close, came up with this sequence. Please note that |= is an OR operation (set bit), and &= ~(bit) is a NOT operation (clear bit).

#define PWM_MODE_1 (6 << 4) //channel active as long as TIM1_CNT < TIM1_CCR1
void timer1_setup(void)
    TIM1_CR1 &= ~(1<<0); // Close TIM1 just to be safe
	TIM1_PSCRH = 0; //no prescaler, give me 16MHz
	TIM1_PSCRL = 0;
	TIM1_ARRH  = 0; //auto-reload register, triggers an interrupt when it meets this
	TIM1_ARRL  = 255; //goes from 0 to 255, 256 bits inclusive
	TIM1_CCR1H = 0; //PC6 in AFR0 mode
	TIM1_CCR1L = 0; //this value will be the actual PWM value we  are using
	TIM1_CCMR1 = PWM_MODE_1; // PWM mode 1
	TIM1_CCER1 |= 0x11; //0b00010001, capture-compare 2 enabled, active highs, capture-compare 1 enabled, active high
	TIM1_CCR2H = 0;  //PC7 in AFR0 mode
	TIM1_CCR2L = 255;// our analog value for our second VCO  (TIM1_CCR2L /(TIM1_ARR +1))
	TIM1_IER |= (1<<1);            //capture/compare 1 interrupt enable
	TIM1_IER |= TIM_IER_UIE; // bit 0 == 1 update interrupt enabled

	bitClear(TIM1_CR1, 6); //edge-aligned mode
	bitClear(TIM1_CR1, 5); //edge-aligned mode
	bitClear(TIM1_CR1, 4);//direction 0: up, 1: down
	bitClear(TIM1_CR1, 3);//one-pulse mode: 0: counter is not stopped at update

	TIM1_CR1 |= TIM_CR1_UDIS; // only generate interrupt on overflow
	bitSet(TIM1_CR2, 6); //this and the next two lines set up the mode selection
	bitSet(TIM1_CR2, 5); //111 sets OC4REF as trigger output, dont know what i'm doing
	bitSet(TIM1_CR2, 4);
	//bitSet(TIM1_EGR, 5);
	bitSet(TIM1_EGR, 0);
	//TIM1_CR1 |= 0b100; //no interrupts at all
	//TIM1_CR1 |= 0b110; //THIS ONE IS REAL IMPORTANT, sets UDIS and URS so only when the counter overflows
	TIM1_BKR |= (1<<7); //Output compare pins are enabled if their bits are set
	TIM1_CR1 |= 1; // Enable TIM1 

Why Tiddlywiki

mitch5th September 2021 at 3:01pm

Its self-contained, all one html file. Until I end up writing/documenting about a thousand posts, this should be absolutely fine.

Its a single application, meant to be used from a browser. There is no reason to jump into a terminal to use this. It also automatically saves, and I really like the general idea. I only had to run a few commands to make this happen:

sudo npm install -g tiddlywiki
mkdir /some/directory && cd /some/directory
tiddlywiki mynewwiki --init server
tiddlywiki mynewwiki --listen

Thats it! thats the whole "install a server and pull in all the dependencies" trick. Of course, I'm reverse proxying with nginx, but that is also trivial. Here is a minimal version of that

server {
         listen 80;
         server_name blog.smallrat.net;
        location / {

Also, my last blog was Xanga. I'm hoping there is more functionality in one index.html than all of xanga circa 2006.

Getting started with an stm8s003

mitch5th September 2021 at 2:26pm
live stm8

Get sdcc (the compiler) and stm8flash (the uploader):

#libusb, if you already have it, skip to stm8flash
cd /tmp
git clone https://github.com/libusb/libusb.git && cd libusb
make -j$(nproc)
sudo make install

cd /tmp
git clone https://github.com/vdudouyt/stm8flash.git && cd stm8flash
make -j$(nproc)
sudo cp stm8flash /usr/bin/

#sdcc (but built statically , and only for stm8)
#installs to /opt/sdcc, change it in the configure command
cd /tmp
git clone https://github.com/swegener/sdcc.git && cd sdcc

export CFLAGS=-static; export LDFLAGS=-static #static flags
#ok, this just disables everything we do not need for stm8 development
./configure --disable-mcs51-port --disable-z80-port --disable-z180-port --disable-r2k-port --disable-r2ka-port --disable-r3ka-port --disable-gbz80-port --disable-tlcs90-port --disable-ez80_z80-port --disable-z80n-port --disable-ds390-port --disable-ds400-port --disable-pic14-port --disable-pic16-port --disable-hc08-port --disable-s08-port  --disable-pdk13-port --disable-pdk14-port --disable-pdk15-port --prefix=/opt/sdcc

make -j$(nproc)
sudo mkdir -p /opt/sdcc
sudo make install

#now you want to add that to your path, with bash you can do this:
echo "export PATH=\$PATH:/opt/sdcc/bin" >> ~/.bashrc

On first attempt of programming, you gotta unlock it. Do this: stm8flash -c stlinkv2 -p stm8s003f3 -u

You should see this:

Determine OPT area
Due to its file extension (or lack thereof), "Workaround" is considered as RAW BINARY format!
Unlocked device. Option bytes reset to default state.
Bytes written: 11

Now its unlocked and you can actually write your file to the device: stm8flash -c stlinkv2 -p stm8s003f3 -s flash -w sound.ihx

Determine FLASH area
Due to its file extension (or lack thereof), "sound.ihx" is considered as INTEL HEX format!
2323 bytes at 0x8000... OK
Bytes written: 2323

Compilation is done with sdcc: sdcc -mstm8 sound.c This should give you a few intermediate files, and sound.ihx. This is the binary you use to upload.

Notice that the ihx file isn't 2323 bytes. size sound.bin will also show you how much ROM and RAM you are using. In this one, I'm not using any variables, so bss is zero. Its all in ROM.

   text	   data	    bss	    dec	    hex	filename
      0	   2323	      0	   2323	    913	sound.ihx

Turns out there are alternate functions for pins, and you gotta hunt to find em. I found this https://www.st.com/resource/en/datasheet/stm8s003k3.pdf, which details the structure of the option bytes. Its not the most straightforward thing, but heres my solution. In my case, I wanted PC6 and PC7 to be attached to timer 1, channels 1 and 2 respectively. Those two output settings are alternate functions. Page 28 states this. Page 42 has a map of the option bytes. They start at 0x4800, and only go to 0x480A. Some of them need to be set in complimentary pairs, such as OPT2, the one we want to change. So if I want to change AFR0, the 0th bit of OPT2, I need to set it normally at address 0x4803, and the inverse of it at 0x4804. To accomplish this I will use echo and xxd. The command looks like this: echo "0000FF01FE" | xxd -r -p > opt_bytes.bin The preceeding patterns are for OPT0, or read-out protection, which I don't care about, and the user boot code, which I also don't care about. That gives us 00 for OPT0, and 00FF for OPT1 and NOPT1. Finally, we can insert our alternate OPT2 memory here: 01, and NOPT2, FE. To make sure I've got a file which is exactly those bytes, I check with hexdump -C opt_bytes.bin

00000000  00 00 ff 01 fe                                    |.....|

And thats the pattern we're looking for. We write this to the device with stm8flash -c stlinkv2 -p stm8s003f3 -s opt -w opt_bytes.bin

Determine OPT area
Due to its file extension (or lack thereof), "opt_bytes.bin" is considered as RAW BINARY format!
5 bytes at 0x4800... OK
Bytes written: 5

Finally, I wanted to make sure this was actually on the device. Time to use the read command for stm8flash: stm8flash -c stlinkv2 -p stm8s003f3 -s opt -r stm8s103_opt && hexdump -C stm8s103_opt

00000000  00 00 ff 01 fe 00 ff 00  ff 00 ff 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

This is what I've got at the end of the day:

#include "stm8.h"
#include <stdint.h>
/*page 219 of https://www.st.com/resource/en/reference_manual/cd00190271-stm8s-series-and-stm8af-series-8bit-microcontrollers-stmicroelectronics.pdf
TIM1 register map

PD4 ----|          |-- PD3
PD5 TX -|          |-- PD2
PD6 RX -|          |-- PD1 SWIM
RESET --|          |-- PC7 MISO
PA1  ---|          |-- PC6 MOSI
PA2  ---|          |-- PC5 SCK
GND  ---|          |-- PC4
5V  ----|          |-- PC3
3.3V ---|          |-- PB4 SCL 
D2  ----|__________|-- PB5 SDA

   wait wait, official TIMER output channel pins
 *  PD4 is TIM2_CH1, UART1_CK
 *  PD5 is UART1_TX, AIN5
 *  PD6 is UART1_RX, AIN6
 *  PA3 is TIM2_CH3, alternately SPI_NSS with AFR1 set
 *  PB5 is I2C_SDA,  alternately TIM1_BKIN with AFR1 set
 *  PB4 is I2C_SCL, ADC external trigger with AFR4 set
 *  PC3 is TIM1_CH3, TLI with AFR3 set, TIM1_CH1N with AFR7 set
 *  PC4 is TIM1_CH4, CLK_CCO, AIN2, TIM1_CH2N with AFR7 set
 *  PC5 is SPI_SCK,  alternately TIM2_CH1 with AFR0 set
 *  PC6 is SPI_MOSI, alternately TIM1_CH1 with AFR0 set
 *  PC7 is SPI_MISO, alternately TIM1_CH2 with AFR0 set
 *  PD1 is SWIM
 *  PD2 is AIN3, alternately TIM2_CH3 with AFR1 set
 *  PD3 is TIM2_CH2, AIN4, ADC_ETR

volatile uint8_t index = 0;
ISR(timer1_isr, 0x0E) //neither TIM1_OVR_UIF_vector or TIM1_CAPCOM_CC1IF_vector worked
	TIM1_CCR2L = 127 - index; //ramp down
	TIM1_CCR1L = index;		  //ramp up
	if (index >= 128) { index = 0; }
	//index = index % 256;
	TIM1_SR1 = 0; 			  //erase all interrupts

#define PWM_MODE_1 (6 << 4) //110: PWM mode 1 - In up-counting, channel 1 is active as long as TIM1_CNT < TIM1_CCR1, otherwise, the channel is inactive
void timer1_setup(void) //PWM_OC1_25_OC2_50_OC3_75
//from https://community.st.com/s/question/0D50X00009XkdtmSAB/configuring-channel-1-of-timer1-for-pwm-signal-stm8s-controller
	TIM1_CR1 &= ~(1<<0); // Close TIM1
	TIM1_PSCRH = 0; 
	TIM1_PSCRL = 0; // undivided
	TIM1_ARRH  = 0; //0xFF at reset
	TIM1_ARRL  = 255 ; //goes from 0 to 127, 0xFF at reset
	TIM1_CCR1H = 0; //PC6 in AFR0 mode
	TIM1_CCR1L = 64; // 25% duty cycle (25 / (99 + 1)) 
	TIM1_CCMR1 = PWM_MODE_1; // PWM mode 1  //0x60
	TIM1_CCER1 |= 0x11; //0b00010001, CC2 enabled, active highs, CC1 enabled, active high, 6:7 and 2:3 are reserved
	TIM1_CCR2H = 0;  //PC7 in AFR0 mode
	TIM1_CCR2L = 120; // duty cycle is  (TIM1_CCR2L /(TIM1_ARR +1))
	TIM1_IER |= (1<<1); //capture/compare 1 interrupt enable
	TIM1_IER |= TIM_IER_UIE; // bit 0 == 1 update interrupt enabled
	TIM1_CR1 |= 1; // Enable TIM1 
	TIM1_CR1 |= 0b110; //THIS ONE IS REAL IMPORTANT, sets UDIS and URS so only when the counter overflows
	TIM1_BKR |= (1<<7); //Output compare pins are enabled if their bits are set

void main()
	CLK_CKDIVR = 0x00;	//16MHz
	sim(); 				//disable interrupts
	rim();				//enable interrupts
		wfi(); 			//wait for interrupts