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:
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:
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_CCMR2 = PWM_MODE_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
}
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 / {
proxy_pass http://127.0.0.1:8000;
}
}
Also, my last blog was Xanga. I'm hoping there is more functionality in one index.html than all of xanga circa 2006.
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
./autogen.sh
./configure
make -j$(nproc)
sudo make install
#stm8flash:
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 |.....|
00000005
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 |................|
*
00000040
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
STM8S103F3
___________
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
index++;
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_CCMR2 = PWM_MODE_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
timer1_setup();
rim(); //enable interrupts
for(;;)
{
wfi(); //wait for interrupts
}
}