I did a bit more on this stuff and have a crude polled timer example I can share. Like the other stuff this is basic proof of concept stuff:
public static void PulseViaTimer()
{
using var rcc = Peripheral.OpenRcc();
using var timer = Peripheral.OpenTimer(GeneralTimer.TIM2);
using var gpioa = Peripheral.OpenGpio(GpioPort.GPIOA);
// Enable clocks for GPIOA and TIM2
rcc.MP_APB1ENSETR.TIM2EN = true;
rcc.MP_AHB4ENSETR[GpioPort.GPIOA] = true;
// Setup the GPIO pin 4 here
gpioa.MODER[4] = Mode.Output;
gpioa.OSPEEDR[4] = Speed.VeryHigh;
gpioa.PUPDR[4] = Pull.Down;
// Setup the timer registers
timer.PSC = 1000;
timer.ARR.ARR_LONG = 200;
timer.CNT_NRE.CNT = 0;
// Enable counting
timer.CR1.CEN = true;
// Crudely poll and toggle PA4 on each timer overflow
while(true)
{
while(timer.SR.UIF == false)
{
}
gpioa.BSRR = 0x_00000010;
gpioa.BSRR = 0x_00100000;
// Reset the flag
timer.SR.UIF = false;
}
}
This generates a 1KHz pulse, with an “on” pulse duration of about 400 ns when built in Debug configuration, when we build with Release configuration that pulse width drops to about 50 ns but of course the frequency remains the same because a timer is being used.
I played around with using the timer in compare mode, driving a port in alternate function mode. This works but the effort to define register structures and accessor properties is high and error prone even when one tries to be meticulous.
So I created a register source code generator and this can now generate an entire C# class for a peripheral and generate all of its registers, automatically, taking care of all the shifting, ANDing and ORing and so on, for all of the register sub-fields.
As I mentioned MS have made numerous improvements to C# that make working at this level both easier and more efficient at runtime, much more so than a few years ago.
There’s a simple “register description language” that is consumed and spurts out C# source, so the effort to gradually add more registers to peripherals is now very low.
This little sample causes a 1 MHz signal to appear on PA5:
public static void PulseViaTimerGpio()
{
using var clock = Peripheral.OpenRcc();
using var timer = Peripheral.OpenTimer(GeneralTimer.TIM2);
using var gpioa = Peripheral.OpenGpio(GpioPort.A);
// Enable clocks for GPIOA and TIM2
clock.MP_APB1ENSETR.TIM2EN = true;
clock.MP_AHB4ENSETR[GpioPort.A] = true;
// Setup the GPIO pin 5 here
gpioa.Moder[5] = Gpio.Mode.Alternate;
gpioa.Ospeedr[5] = Gpio.Speed.Low;
gpioa.Pupdr[5] = Gpio.Pull.None;
gpioa.Afrl[5] = Gpio.AlternateFunction.AF1;
// Setup the timer registers
timer.Psc = 5;
timer.Arr = 15;
timer.Ccmr1.OC1M = TimerGeneral.OCMode.ToggleOnMatch;
timer.Ccr1.AllBits = 0;
timer.Ccer.CC1E = true;
timer.Cnt_Nre = 0;
timer.Cr1.CEN = true;
// Sleep, freeing up the CPU
Thread.Sleep(Timeout.Infinite);
}
My core intertest here is in exploring how we can use C# 12 on .Net 8 to write low level code, avoiding interop (unless we’re talking to the OS) and exploiting C# performance oriented features.
Yes, that’s great goal actually given what I’ve got so far. I am refactoring the code daily and refining the patterns and so on to get the code to a high standard and it’s very clean and stable now so a new test is a good idea.
On the (very) back burner are dealing with interrupts and DMA, providing there’s no fundamental restriction here (like Linux and so on, limiting what user code can do) then these are things I very much want to explore.
So I’ll begin by playing with very basic DAC working, I’ve got a very good codebase now to attempt this, being able to generate robust error-free register access code from a simple descriptor language, makes this much less tedious too!
This went well, the following simple code generates an approx. 1 KHz sawtooth on PA4:
public static void PulseViaDac()
{
using var clocks = Peripheral.OpenRcc();
using var gpioa = Peripheral.OpenGpio(GpioPort.A);
using var dac1 = Peripheral.OpenDac(DacInstance.DAC1);
clocks.MP_APB1ENSETR.DAC12EN = true;
clocks.MP_AHB4ENSETR[(int)GpioPort.A] = true;
gpioa.MODER[4] = Mode.Analog;
dac1.CR.EN1 = true;
uint data = 0;
while (true)
{
dac1.DHR12R1.DACC1DHR = data++;
}
}
This somewhat “hacky” code generates an almost 4 KHz sine wave, again 12 bit resolution just cobbled together to see how it behaves:
public static void PulseViaDac()
{
double[] sine = new double[4096];
uint[] intsine = new uint[4096];
double full = Math.PI * 2;
double incr = full / 2048;
for (int i = 0; i < 4096; i++)
{
sine[i] = Math.Sin(i * incr);
intsine[i] = Convert.ToUInt32((sine[i] + 1) * 2048);
}
using var clocks = Peripheral.OpenRcc();
using var gpioa = Peripheral.OpenGpio(GpioPort.A);
using var dac1 = Peripheral.OpenDac(DacInstance.DAC1);
clocks.MP_APB1ENSETR.DAC12EN = true;
clocks.MP_AHB4ENSETR[(int)GpioPort.A] = true;
gpioa.MODER[4] = Mode.Analog;
dac1.CR.EN1 = true;
uint data = 0;
while (true)
{
for (int i = 0; i < 4096; i++)
{
dac1.DHR12R1.DACC1DHR = intsine[i];
}
}
}
Both of these DAC examples are built Release mode.
That jittering must simply be due to the OS scheduler preemptively interrupting the process from time to time, but nobody would generate a sine by such a CPU bound process anyway, not in real-life.