Rosetta Stone - Translate from FV-1 to FXCore

DisasterArea
Posts: 26
Joined: Sat Jul 25, 2020 7:07 pm

Rosetta Stone - Translate from FV-1 to FXCore

Post by DisasterArea »

If you're experienced with the FV-1, you may be finding getting the FXCore working to be a bit of a challenge! The architecture is similar in a lot of ways but just different enough to create a learning curve.

I'm creating this post both as a way to help others get started with FXCore and as a way for me to get more familiar with the chip and its instruction set. I'll be posting my findings in a series of topics on this post, if you have questions or suggestions please let me know and I can update as needed.

GETTING STARTED

Part 1: Registers

A register is a memory location that stores a single value for use in the program. You can put pretty much anything in a register - an audio sample, a control signal value, whatever.

The FV-1 has 32 registers, REG0-REG31. It supports indirect addressing only, so you'll need to load the register into the accumulator first.

Let's say we want to read in a register, scale it by the value of a pot, save it back to the original register. Here's how you do that with FV-1:

Code: Select all

RDAX REG0, 1.000	; load the contents of register 0 into the accumulator and multiply by 1.00
MULX POT0		; multiply value in accumulator by pot 0
WRAX REG0, 0.000	; write scaled value back to register 0, multiply accumulator by 0.00 / clear it
Not too tough. If you understand how assembly language works this won't feel too strange.

On FXCore we have only 16 registers, r0-r15 (note the difference in names!) and you can actually do things directly to register values without loading them in the accumulator first. BUT we can't use the pot values directly, so we have a few extra steps. Here's how you'd do the same thing as above but on FXCore:

Code: Select all

cpy_cs	acc32, pot0_smth	; copy the smoothed value of pot 0 into the 32-bit accumulator.  pot0 is a "special" register so use cpy_cs
multrr	r0, acc32		; multiply register 0 by the value in the accumulator - the new value is placed in acc32 automatically.  use multrr to do register * register
cpy_cc	r0, acc32		; copy accumulator back into r0.  acc32 and r0 are both "core" registers so use cpy_cc
It's different but not too different. Note that the "copy" commands have a suffix that must be correct for the registers you are using. The format is copy - destination - source. In this example we use "cpy_cs" to copy from a "special" register into a "core" register. We also use "cpy_cc" to copy from a core register to another core register. Special Function Registers are things that access the chip hardware directly, like the audio inputs and outputs, the pots, switches, etc. Core registers are r0-r15, the 32-bit accumulator and so on. Most things you want to do in the chip happen in core registers so it's important to understand how to get data in and out of them.

You could also use a second register as the initial holding place for the pot value like this:

Code: Select all

cpy_cs	r1, pot0_smth		; copy the smoothed value of pot 0 into core register r1.  pot0 is a "special" register so use cpy_cs
multrr	r0, r1			; multiply register 0 by register 1 - the new value is placed in acc32 automatically.  use multrr to do register * register
cpy_cc	r0, acc32		; copy accumulator back into r0.  acc32 and r0 are both "core" registers so use cpy_cc
Why would you want to do this instead of using the accumulator? If you already have the two values you want to mess with in registers already, use a direct register operation. If you're just now loading up one of the values, use acc32 to save a register.

On FV-1, if you want to start a program with a value in a register you have to initialize it yourself like this

Code: Select all

SKP	run, 3			; run the following section once, then on each cycle we skip these 4 lines
SOF	0.00, 0.585		; load accumulator with 0 + 0.585
WRAX	reg0, 0.00		; store to register 1 and clear accumulator
WRAX	reg1, 0.00		; store 0 to register 1
; rest of program follows
This works great, but it does eat up valuable program space. That's 4 lines out of 128, 1/32 of your total! Brutal.

FXCore has a really neat feature that initializes core registers (CREGs) without eating your program space. They're processed in the assembler and then saved in a special section of the FXCore's internal memory.

Code: Select all

.creg	r0 0.585		; set register 0 to 0.585, note that we don't put a comma between these two terms
.creg	r1 0.00			; set register 1 to 0, we don't really need to do this, see below
Important note: You can assume all of the CREGs are 0 unless you explicitly initialize them. On FV-1 they're all random values unless you set them. Usually not a big deal since they get written during the program run but sometimes they need to be set somewhere, like for some filtering or counters.

You might see the same registers used over and over in the example programs, usually called something like "temp" or "temp2." Why would you do this? Well, the FXCore has half the registers of the FV-1. There are some tricks to get more registers on FXCore (see MREGs and Lookup Tables, below) but let's assume for the moment we just have the 16. My rule of thumb is "if the value has to survive to the next program cycle it needs its own register." Otherwise you're better off learning how to reuse registers to keep as many free as you can. Those extra registers might be the key to getting the effect you want, after all!

Here's an example that uses a pot to crossfade or mix between two signals. With the pot at counter-clockwise you get one signal, clockwise you get the other, and in the middle you get some mix of both.

Code: Select all

; register names - you don't have to do this but it helps!
.rn	fade r0			; assign name "fade" to r0
.rn	wet r1			; assign name "wet" to r1
.rn	temp r2			; assign name "temp" to r2

; grab the mix pot value and treat the wet signal - this assumes the output of the effect is in the "wet" register
cpy_cs	fade, pot0_smth		; put value of pot 0 in "fade."  note we don't have to put anything into the accumulator
multrr	wet, fade		; multiply wet signal by fade, so it gets louder as the pot goes clockwise.  result in acc32
cpy_cc	wet, acc32		; store result from acc32 back into wet, lets us re-use a register!

; now get the clean signal, scale and mix it with the wet from above
cpy_cs	temp, in0		; get clean signal and put it in the "temp" register so we can do stuff with it
neg	fade			; change the sign of "fade" so that it ranges -0.999 to 0, result is in acc32
addsi	acc32, 0.999 		; add 0.999 to acc32, so now the pot ranges from 1 to 0 for the dry signal
multrr	acc32, temp		; scale the dry signal in temp by the reversed pot
adds	acc32, wet		; add scaled wet back in, result is in acc32
cpy_sc	out0, acc32		; output acc32 to output 0.
This uses 3 registers: fade, wet, and temp. There's not too bad, but technically none of these need to survive to the next sample since we'll be putting new values in them on the next run-through. But you can't really get rid of any registers here, three are required.

We use "fade," "wet," and "temp" twice each in this section so while they don't have to survive the next sample they do need to survive for a few lines in the program. You can re-use them somewhere else in the program, but we need three right here. So you do need these three, but you could easily re-use that "temp" register several times in the program, as long as it doesn't need to stick around for the next sample.

Next time we'll cover manipulating numbers for fun and profit. Want to see a specific topic covered? Let me know.

(edited to correct tab spacing in code examples and replace "common" with "core" per Frank's suggestion)
Last edited by DisasterArea on Sun Jul 26, 2020 10:49 am, edited 3 times in total.
Frank
Posts: 159
Joined: Sun May 03, 2015 2:43 pm

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by Frank »

Great read, only minor thing, they are "core" registers not "common". So named as they are tightly tied to the processing core.
DisasterArea
Posts: 26
Joined: Sat Jul 25, 2020 7:07 pm

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by DisasterArea »

GETTING STARTED

Part 2: Manipulating Numbers for Fun and Profit, or "Where did my SOF go?"

The most used instructions on FV-1 are:

RDAX - read a value from a register and put in the accumulator
WRAX - write the accumulator value into a register
SOF - scale and offset the accumulator value

We have cpy_cs, cpy_sc, cpy_cc to stand in for RDAX and WRAX, but where the heck is SOF?!?!. It's GONE.

But it isn't, really.

FXCore still has the ability to scale and offset, but it's been broken up into several smaller instructions. The reason for this is a bit technical but it boils down to the fact that FXCore lets you eliminate the little pieces of FV-1 instructions that you didn't need. On FV-1, every RDAX, WRAX, and many other instructions includes a multiply at the end. Multiply is a fairly slow operation, so FXCore doesn't make you waste clock cycles unless you need it.

So how do we work this? Let's look at a typical FV-1 use case, read a register, scale it, put it in another register.

Code: Select all

RDAX	reg0, 1.0		; read register 0 into acc, scale by 1.0
SOF	0.80, 0.20		; scale by 0.8, add 0.2.  the value now ranges from 0.2 to 1.0
WRAX	reg1, 0.0		; put scaled value in register 0, clear accumulator 
We do this kind of thing all the time for dialing in potentiometer ranges or tweaking gains between stages, etc.

On FXCore we use the MULTRI (multiply core register by a constant) and ADDSI (add constant to core register) instructions. Important note that the range of these constants is an S.15 number, from -1.00 to +0.999.

Code: Select all

; value is r0 to begin
multri	r0, 0.80		; multiply register by a constant, result is in acc32
addsi	acc32, 0.20		; add constant to a register, result is in acc32
cpy_cc	r1, acc32		; store core register acc32 into another core reg r1
Not too bad, right? MULTRI and ADDSI can stand in for SOF. If you don't need to scale and offset then you can just not use one or the other.

If you're used to adding registers in FV-1 like this:

Code: Select all

RDAX	reg0, 1.0		; load reg0 into acc
RDAX	reg1, 1.0		; add in reg1 to acc
WRAX	reg2, 0.0		; save in reg2 and clear
Then you can do something similar in FXCore with the "ADDS" instruction. ADDS will add two core registers together and place the result in ACC32.

Code: Select all

; values are in r0 and r1 to begin
adds	r0, r1			; add r0 to r1, result is in acc32
cpy_cc	r2, acc32		; copy value from acc32 to core reg r2
FV-1 had some peculiarities with regards to SOF and number systems - you could use an SOF to multiply by a maximum of positive 1.9999398 or negative -2.000, and add a max of +0.999 or -1.0. You'll see a lot of this kind of thing in FV-1 programs:

Code: Select all

RDAX	reg0, 1.0		; read in reg0 to acc32
SOF	-2.0, 0.0		; double the value to add gain
SOF	-2.0, 0.0		; double the value again, more gain
SOF	-2.0, 0.0		; double the value again, even more gain
SOF	-1.0, 0.0		; just invert the phase so it's the same as the input
WRAX	reg2, 0.0		; store in reg2 and clear acc
If you remember our description of ADDSI and MULTRI from above, you'll recall that the max coefficients for these instructions are -1.0 to +0.999. So you can't use them to add gain in the same way as SOF. But there is a super cool way around this using the SLS instruction.

SLS means "shift left with saturation." This takes the value in a core register and shifts its binary value left by one place, which doubles it. The "saturation" part of the instruction means that the FXCore will never let that number roll over, so that even if you go nuts shifting places the register just gets bigger and bigger without resetting. It's not really mentioned in the FV-1 documentation but SOF does saturate in the same way, so these two methods are pretty much the same.

The coefficient for SLS tells FXCore how many times to double the value. 1 means double, 2 means x4 (double double,) and 3 means x8 (double double double.). It's almost trivial to add a ton of gain this way.

Code: Select all

; value is in r0 to begin
sls	r0, 3			; multiply by 8x, result is in acc32
cpy_cc	r2, acc32		; store value from acc32 into r2
Note that there is also a "SR" instruction, which divides by twos instead of multiplying. It's useful if you want to scale something down in a precise manner, or to constrain a value to a very narrow range of bits. We'll cover that more in MREGs and Lookup Tables later.

Speaking of saturation, ADDSI will saturate just like SOF will. If you keep adding to a value, eventually FXCore will run out of numbers and stop. If you were to use an unsaturated addition like ADDI (note there's no "S") then FXCore will roll the number over from most positive to most negative and then keep counting. ADDI is useful for counters when you want the number to roll over.

Code: Select all

; 0.5 in r0 to begin
addsi	r0, 0.25		; add 0.25 + 0.5 = 0.75 in acc32
addsi	acc32, 0.25		; add 0.25 + 0.7 = 0.95 in acc32
addsi	acc32, 0.25		; add 0.25 + 0.95 = 0.999 saturated!
cpy_cc	r1, acc32		; store value in core register r1
If you want to multiply two registers together in FV-1, for example to scale a signal by a pot, it's pretty easy:

Code: Select all

RDAX	pot0, 1.0		; read in pot0 into accumulator
SOF	0.9, 0.1		; scale pot to 0.1 - 1.0, so it never goes all the way to zero
MULX	reg0			; multiply scaled pot by register 0, thereby scaling reg0
WRAX	reg1, 0.0		; store to register 1 and clear
This is sometimes a minor thing, but the FV-1 doesn't do a true 32 x 32 multiply. It truncates the operand in MULX to 16 bits and pads it to the left before it does the multiply.

Code: Select all

RDAX	pot0, 1.0		; read in pot 0
MULX	reg0			; multiply by reg 0
WRAX	reg1, 0.0		; store and clear

RDAX	reg0, 1.0		; read in reg 0
MULX	pot0			; multiply by pot 0
WRAX	reg2, 0.0		; store and clear

;reg1 and reg2 will probably be different in the very low bits!
You can do this on FXCore, but it's a bit different. We don't have a MULX instruction (multiply register by accumulator, instead we have MULTRR, multiply two core registers together. Unlike FV-1, FXCore does a true 32 x 32 multiply so it doesn't matter which order you choose. You also can't multiply by an SFR like the pots or the inputs, so you'll have to remember to load them into a core reg before multiplying.

Code: Select all

; value is in r0 to start
cpy_cs	acc32, pot0_smth	; copy smoothed pot0 into acc32
multri	acc32, 0.9		; scale by 0.9
addsi	acc32, 0.1		; add 0.1, these two instructions are like SOF 0.9, 0.1
multrr	acc32, r0		; multiply acc32 by r0 or vice versa, multiplication is commutative
cpy_cc	r1, acc32		; store scaled value into core register r1
What if you want to subtract from a register? With SOF you'd just make the coefficient negative and that would do it. You can still do that on FXCore, but in a couple of ways. First, you can use ADDSI to add a negative number. If the coefficient is bigger than your number, the result will be negative. If you keep going, eventually ADDSI will saturate to -1.0 as you subtract

Code: Select all

; value of 0.5 is in r0 to begin
addsi	r0, -0.25		; 0.5 - 0.25 = 0.25 in acc32
addsi	acc32, -0.5		; 0.25 - 0.5 = -0.25 in acc32
cpy_cc	r1, acc32		; negative 0.25 now stored in acc32
There are also a couple of special instructions for subtraction, SUB (unsigned subtract two core regs) and SUBS (signed subtract two core regs) but they don't work with constants in the same way that ADDSI and ADDS do. They're usually used to compare two values in order to branch a program.

Next up we'll look at MREGs and Lookup Tables, which are super fun and useful.
Last edited by DisasterArea on Mon Jul 27, 2020 12:41 pm, edited 1 time in total.
PhilHaw
Posts: 65
Joined: Thu Mar 07, 2019 5:37 pm

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by PhilHaw »

Excellent work DisasterArea!

This is going to be a very useful resource for anyone converting from FV-1 to FXCore.

Keep up the good work :)

Phil.
Philip Hawthorne

Blue Nebula Development Team
stanleyfx
Posts: 42
Joined: Fri Jan 27, 2017 2:19 pm

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by stanleyfx »

Thank you for the easy to understand FV-1 to FX-Core instructions. Great write up and I look forward to more. 8-)
Mick (Blue Nebula Design team)
scmitche
Posts: 12
Joined: Mon Mar 16, 2020 5:28 am

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by scmitche »

Excellent work. You are correct it's quite a task moving from FV-1 with 31 registers to FX-core with 15/16 core registers where I'm going to have to learn how to manipulate efficiently the MRegisters before I can think about extending our suite of vintage echo emulations.


Steve Mitchell
Blue Nebula Design Team

___________________________________________________________________
FX-Core Dev Board V1.3
Assembler 1.0.11
Win 10 32-bit Intel Duo-Core laptop
Win 10 64-bit Intel i7 processor
DisasterArea
Posts: 26
Joined: Sat Jul 25, 2020 7:07 pm

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by DisasterArea »

Part 3 - MREGs and Lookup Tables

As we noted in Part 1, FXCore has half the core registers compared to FV-1. This isn't a huge limitation if you manage things carefully but at some point in a long program you might find yourself needing just a few more places to put things!

FXCore has a trick up its sleeve here: Memory Registers (MREGs.). There are 128 memory locations that hold a single 32-bit value each, just like the core registers (CREGs.) You can put all the same kinds of data in MREGs, audio samples, control values, whatever you want.

The only real difference is that you must copy data into a CREG from an MREG before you can use it for anything in your program. You can name MREGs, in exactly the same way as you'd name CREGs.

As a refresher, here's how you'd use a core register (CREG) with FXCore. This example reads a value from register 0, scales it by a pot, and writes it to r1.

Code: Select all

cpy_cs	acc32, pot0_smth	; load in value from pot0 to acc32
multrr	r0, acc32		; multiply r0 by the value of pot0, result in acc32
cpy_cc	r1, acc32		; store result in r1
Seems familiar, right? Good! Now here's how you'd do the same thing with values into and out of MREGs:

Code: Select all

cpy_cs	acc32, pot0_smth	; load in a value from pot0 to acc32
cpy_cm	r0, mr0			; copy value from MREG 0 to CREG 0
multrr	r0, acc32		; multiply contents of MREG0 (copied to r0) by acc32
cpy_md	mr1, acc32		; store result in MREG 1
Not much else to talk about with MREGs. They're memory locations that you can use to stash whatever, and they just take an extra operation to get data out of them.

But there is one more thing... what about this CPY_CMX instruction? "Copy from MREG register to a core register using a second core register to address the MREG". That's a mouthful, but it's pretty easy to understand in context.

Have you ever seen an FV-1 program that uses a bunch of SKP instructions to divide up a pot into several options? Something like this:

Code: Select all

RDAX	pot0, 1.0		; read in pot0
AND		%01100000_00000000_00000000	; mask off 2 bits, leaves 4 options
SKP	zro, label1		; if zero go to label 1
SOF	1.0, -0.25		; otherwise subtract 1/4
SKP	zro, label2		; if the result is now zero skip to label 2
SOF	1.0, -0.25		; if not, subtract 1/4 more
SKP	zro, label3		; and skip

label4:				; don't need a skip to this label, since if we don't hit it we fall through
SOF 0.00, 0.999			; put a value in acc and store
WRAX	reg1, 0.00
SKP	run, finished		; jump out to finished

label3:				; same thing again and again
SOF 0.00, 0.75
WRAX	reg1, 0.00
SKP	run, finished

label2:
SOF 0.00, 0.50
WRAX	reg1, 0.00
SKP	run, finished

label1:
SOF 0.00, 0.25
WRAX	reg1, 0.00

finished:
; do the rest of the program here!


So this takes your pot input and cleanly divides it into four values. Want five, six, eight? No problem, just subtract the correct amount to divide 1 by your choice and add more skip routines. But this is a program space nightmare, occupying 18 lines or 14% of your available space. Fail.

Let's see if there is an easier way to accomplish this goal!

First, we're going to put our desired values in a series of MREGs. We'll use registers 0-3 but it could be any of them as long as they're in order. 0-3 makes the math easier as you'll see.

Code: Select all

; load values into MREGs - numbers are 32-bit signed
.mreg	mr0	0.25000
.mreg	mr1	0.5000
.mreg	mr2	0.75000
.mreg	mr3	0.99999
So our values are now stored in those MREGs at startup. Now we'll use the CPY_CMX instruction to access an MREG based on a register input, instead of referencing it directly.

Code: Select all

cpy_cs	acc32, pot0_smth	; read in pot to accumulator
sr	acc32, 28		; shift pot value 29 places to the right, so now it's only two bits
cpy_cmx	r0, acc32		; this puts the MREG at the address of acc32 into r0
And that's it! Want more values in your table? Add them in and go for it. You'll need to make sure that the pointer value (we used acc32 in our example) is big enough to point to your whole table. For example if you had 32 options in your table you'd shift the pointer by 26 instead of 28 to get five bits.

Pretty powerful stuff, I think you'll find. If you want to see more creative uses for MREGs check out the example programs "prg10_function_gen" (uses lookup tables) and "prg12_rotary_reverb" (uses extra MREGs as registers)
Last edited by DisasterArea on Sat Feb 25, 2023 8:19 pm, edited 2 times in total.
DisasterArea
Posts: 26
Joined: Sat Jul 25, 2020 7:07 pm

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by DisasterArea »

GETTING STARTED

Part 4: Bypass and LEDs

So this entry doesn't have anything to do with FV-1 stuff, but rather with the special hardware that FXCore has.

One of the things that I was super keen on for the new FXCore design was digital inputs. Sure, it's nice to have pots to control parameters, but you don't necessarily need a variable control to select a function. A switch works great for lots of different things, so I really pressed Frank and the team to include something. And they came through!

We'll cover the whole SWITCH register and tap tempo later on, and there's great info in the example programs and app notes. I'm going to focus on the BYPASS / ENABLE pin and the USER0 & USER1 outputs.

Looking at the data sheet and the dev board, you'll see the ENABLE pin. On the data sheet it's labeled ENABLE / BYPASS, with a bar above BYPASS. The bar indicates the state when the pin is held low, to 0V. This pin has a "pull-up" resistor on the chip, so if you leave the pin alone it'll automatically be pulled to 3.3V and the FXCore will be in the ENABLE state.

When the FXCore is enabled, your program runs as normal. When it is in BYPASS, the program still runs but the inputs and outputs are tied together in the hardware. IN0 goes to OUT0, IN1 to OUT1 and so forth. It works like the bypass footswitch on a normal pedal but your audio still runs through the CODEC. There are other ways to bypass the FXCore, depending on what you're trying to accomplish.

The cool thing is that even though the ENABLE pin is hardware, the FXCore can read its state. You can't SET it, but you can know what is happening in your program. ENABLE is part of the SWITCH register, so using it is a lot like using the regular switches.

Code: Select all

; read ENABLE pin and light LED 0
cpy_cs		acc32, switch		; read the switch register into acc32
andi		acc32, 0x8000		; mask off all bits except bit 15 = ENABLE
jz		acc32, doLED		; if zero then we are in bypass, skip out
ori		acc32, 0x0001		; if not zero then we are engaged, put 1 in LSB of acc32

doLED:
set 		user0|0, acc32		; set LED to on or off
That's it! Not too tough, and now you get a free bypass LED for your project.

The SET command lets you set that user0 or user1 bit and turn the LED on or off, but how do you dim or fade the LED? It's fun to show the output of an LFO, so let's look at that.

Before we can cover how to do this, we need to figure out a way to get a variable output when all we can do is turn the LED on or off. If we turn the LED on and off really fast, we can simulate a variable output by using "Pulse Width Modulation."

Here's a great explanation of how it works: https://circuitdigest.com/tutorial/what ... modulation

But the TL:DR is that the LED doesn't have to be on and off the same amount of time. If it's off more than it's on, it'll be darker than if it's on more than it's off. Note the line on the chart showing the "average output voltage," based on the duty cycle of the PWM output.

Image

It's possible to do this with the USER outputs on FXCore. In a lot of the example programs you'll see something called "Olaf's LED PWM" that accomplishes just this task. Olaf is one of the engineers who helped design FXCore and this is a really clever trick, let's look at how it works.

Code: Select all

.rn	bright r0
.rn	temp r1

cpy_cs    acc32, samplecnt        ; Get the sample counter
andi      acc32, 0xFF             ; Mask b[7:0]
jnz       acc32, doPWM            ; if it's not time to update, get out

; Reload new PWM value from LFO0_s into "bright"
cpy_cc	  acc32, temp	  	  ; copy "temp" register to acc32
sra       acc32, 23               ; shift the PWM value in place, we only want 8 bits
cpy_cc    bright, acc32           ; save it to bright

doPWM:
; Performing the decrement prior to driving the LED makes sure
; that the LED can go completly off.
addi      bright, -1    	  ; addi expects a decimal number and we want to subtract 1 LSB
cpy_cc    bright, acc32           ; Save updated "bright"
xor       acc32, acc32            ; Clear acc32 for the LED off case
jneg      bright, doLED           ; if the LED should be off, skip out and turn off LED
ori       acc32, 1                ; Set acc32[0] for the LED on case

doLED:
set       user0|0, acc32           ; set the usr1 output per the acc32 LSB

Yeah, right? What does all of THAT do?

It's actually pretty simple. The code consists of three main pieces.

1. Check the sample counter and update once every 256 samples

2. Convert the value of "bright" from a 32-bit number down to 8-bits (0-255)

3. Subtract 1 LSB from "bright" and if the result is negative, turn off the LED, otherwise turn it on.

In this case, Olaf is looking at the number coming in (temp) and subtracting one small piece of it each sample. If the result is negative, then turn off the LED, if it's still positive, turn the LED on.

So if the value of "temp" is 0.5, then we'd convert that equivalent 32-bit number (0x3FFFFFFF) to an 8 bit value by shifting it right by 23 places = 127 (0x7F.)

Each time the program runs, we'll subtract 1 LSB from bright and if the result is positive then we turn the LED on. So the first 128 times through, the LED will be on, and then once we start to go negative the next 127 will be off. The LED will be on for approximately half the time, it turns on and off every 128 samples and so the perceived brightness will be about half of the full-on value.

If temp instead is lower, like 10 or 30, then the LED will be on for only 10 or 30 samples out of every 256, resulting in 4% or 12% brightness. So we get what looks like an analog output from our digital pin, for the cost of a single register and a few lines of code. Pro tip: "bright" needs to survive past the end of the sample so don't reuse it, but you could easily use an MREG to store its value. You'd need to change the program as follows:

Code: Select all

.rn	temp r0
.rn	bright mr0

cpy_cs    acc32, samplecnt        ; Get the sample counter
andi      acc32, 0xFF             ; Mask b[7:0]
jnz       acc32, doPWM            ; if it's not time to update, get out

; Reload new PWM value from LFO0_s into "bright"
cpy_cc	  acc32, temp	  	  ; copy "temp" register to acc32
sra       acc32, 23               ; shift the PWM value in place, we only want 8 bits
cpy_mc    bright, acc32           ; save it to bright

doPWM:
; Performing the decrement prior to driving the LED makes sure
; that the LED can go completly off.
cpy_cm	acc32, bright		  ; copy bright out of MREG into acc
addi      acc, -1    		  ; addi expects a decimal number and we want to subtract 1 LSB
cpy_mc    bright, acc32           ; Save updated "bright"
jgez	  acc32, setLED		  ; if positive turn on LED
xor       acc32, acc32            ; Clear acc32 for the LED off case
jmp	  doLED			  ; and set LED

setLED:
ori       acc32, 1                ; Set acc32[0] for the LED on case

doLED:
set       user0|0, acc32          ; set the usr1 output per the acc32 LSB
As you can see, we had to switch around the order of setting and clearing the LED since we can't do a direct read or comparison on an MREG as we could on a CREG.

The resolution is 8 bits, since we are counting to 256 for each new value read the maximum number of different values we can get is 256. Since the clock speed on the FXCore dictates the frequency that you can sample, there is always a trade-off between PWM resolution and refresh rate.

Olaf gets 8-bit resolution by sampling the value every 256 samples, so at 48kHz the refresh rate is 48000 / 256 = 195 Hz. If you were okay with 7-bit resolution (0-127) you could change the sample counter and bit shifts to meet this, and you'd be updating twice as fast, almost 400Hz. You'll probably see the steps as the LED changes, but the LEDs will flicker less. You probably can't see the 195Hz flicker, but it might be visible on camera if the frame rates don't sync well.

So thanks to Frank and Olaf and the wonderful folks who asked for general purpose outputs (ahem, me,) you can do a lot of cool stuff with just FXCore.

Next time maybe we'll hit filters...
Last edited by DisasterArea on Tue Aug 25, 2020 1:03 pm, edited 4 times in total.
PhilHaw
Posts: 65
Joined: Thu Mar 07, 2019 5:37 pm

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by PhilHaw »

More great work! That lookup table idea will be very useful.

By the way, you can actually rename the MREGS, for example:

Code: Select all

.rn lp r1 ; Normal CREG rename

.rn lpsave2000 mr0 ; Rename mr0
.rn lpsave800 mr1 ; Rename mr1
.rn hpsave800 mr2 ; Rename mr2
Phil, Blue Nebula Design Team.
Philip Hawthorne

Blue Nebula Development Team
Frank
Posts: 159
Joined: Sun May 03, 2015 2:43 pm

Re: Rosetta Stone - Translate from FV-1 to FXCore

Post by Frank »

DisasterArea wrote: Tue Jul 28, 2020 8:14 am so I really pressed Frank and the team to include something.
Pressed? That's putting it mildly ;)

Matthew was a great help, especially in the beta testing as he beat the hell out of the part and found a bug in an early version of the chip.
Post Reply