Programmatically Defined Comptime Constant?

I’ve spent a few days on this already and everything I find doesn’t exactly fit what I’m asking. But I apologize ahead of time if this has been answered and I didn’t see it or didn’t quite understand the answer.

Here’s my scenario, I’m learning Zig by building a HAL (Hardware Abstraction Layer) for an ARM Processor. I’m trying to simplify the process of setting up the processor for the end user by deriving some of the needed calculations with comptime.

Here’s the issue I’m running into…

What I’d LIKE to do is, in my main.zig do something like

xosc.setFrequency(12000000,130000000);

With the two values being the crystal oscillator frequency and the final PLL frequency for the system.

Now, There are various places in the code (PLL, PWM, Clocks, etc) that use calculations that are based on the frequency the system is running at.

So what I want is when I use that function call above it would set a COMPTIME constant for the frequency the system is running at in a known place like the XOSC.zig file. I do not want to hardkey it as a constant because the user might change it based on their needs. I also want to prevent them from having to redeclare it or pass it again in every file for every peripheral.

I’d like to use comptime to derive the values they’d need to get the peripherals running with their system frequency and then they enable the peripheral if they want.

I’ve seen all kinds of reasoning about Zig not having compile time mutable things, but really I guess what I’m trying to do is have a dynamic constant (oxymoron?).

Please let me know if I can explain that better. But if anyone can point me to a way to achieve that, it would be much appreciated.

Thanks.

1 Like

A good pattern for this can be seen in the zig standard library, with std.Options

Here is the important bit:

const root = @import("root");

/// Stdlib-wide options that can be overridden by the root file.
pub const options: Options = if (@hasDecl(root, "std_options")) root.std_options else .{};

The root module is the top level file in an executable in zig. The std library is importing it, then checking if there is a declaration with the name std_options. If there is, that gets used to define the options const, otherwise the default value for the Options struct are used.

For your example, you could do something like this in your library hal.zig:

const root = @import("root");

// These options can then be used anywhere without having to re-declare the values
pub const options: Options = if (@hasDecl(root, "hal_options")) root.hal_options else .{};

pub const Options = struct {
    frequency_crystal: usize = 10000000,
    frequency_pll: usize = 100000000,
};

Then in the application’s root.zig:

// Assuming build.zig has added hal.zig as an import
const hal = @import("hal");
pub const hal_options = hal.Options{
    .frequency_crystal = 12000000,
    .frequency_pll = 130000000,
};
pub fn main() void {
    // Put code here
}

main in zig is actually defined in a similar way in the standard library too.

8 Likes

If configuration at build-time would work, there’s also Options for Conditional Compilation

4 Likes

Apologies if it seems like I’m being difficult, I’m definitely not trying to, just trying to find an “elegant” method to do this.

I did try that and several variations of setting comptime variables but they always ended up doing what you show in root.zig, which is akin to setting a constant.

I’m trying my hardest to avoid that because it’s one more thing for the user to remember to go set. I was trying to do it more “naturally” in the course of the user setting options they would need to anyways.

In this case, I picked the xosc function because it uses the crystal frequency to derive a wait time for it to become stable. Since I’m already “Declaring” it once there I just wanted it to propagate or store itself.

Or at the very least have a function that stores it at compiletime in a way that every other file can use ( not as a constant hard keyed, but comptime).

The only reason I seem so “anti” constant is so far I don’t have any settings that require that of the user, so to add in a requirement for them to go set one field seems inelegant.

I guess bigger picture wise, I don’t understand what stops Zig from having a function to set a constant at compile time.

As in something like…

const MyCompTimeConst: u32 = comptime undefined;

fn setComptimeConst(comptime val: u32) void {
         MyCompTimeConst = val;
}

That’s probably a bad way to write it, but you can see my intent.

Then if after build is complete and it’s not set, throw a compile error (“Comptime Constant not defined!”)

or if the function is used twice for some reason (“Comptime Const blahblah already defined!”) and show the code place it’s trying to call the second time (And preferably the first time).

Then I could do…

const xosc = @import("/hal/xosc.zig")

pub fn main() noreturn {
  xosc.Oscillator.enableXOSC(12000000,false); 
}

and in xosc


pub const xoscFreq: u32 = comptime undefined;

fn setFreq(comptime val: u32) void {
         xoscFreq = val;
}

    /// Enables the crystal oscillator, Takes the Frequency of the XOSC as
    /// an argument and uses it to calculate the start up delay to ensure
    /// stability. X4 is an option to multiply the delay for safety.
    pub fn enableXOSC(comptime freq: u32, X4: bool) void {
        // Define the global configuration at compile-time
        setFreq(freq);
        // Comptime derive the delay then set at runtime
        setXOSCStartupDelay(freq,X4);
        // Clear the ENABLE field (bits 23:12)
        base.AtomicClear(CTRL, 0xFFF000);
        // Set ENABLE to 0xFAB (Enables XOSC)
        base.AtomicSet(CTRL, 0xFAB << 12);
        // Wait until XOSC is stable
        while ((STATUS.* & (1 << 31)) == 0) {} // Bit 31 = 1 when stable
    }

then in other files I can dynamically do things like

const xosc = @import("xosc.zig")

pub fn enableSystemPLL() void {
    if (xosc.xoscFreq == undefined) {
       @compileError("Can not enable system PLL, XOSC is not enabled first!");
    }
}

or

const xosc = @import("xosc.zig")

pub fn setClockSource(comptime src: ClockSrc) void {
    if (src == ClockSrc.XOSC and xosc.xoscFreq == undefined) {
       @compileError("Can not set source as XOSC! It's not enabled!");
    }
}

but more importantly I can use the xoscFreq to derive other values at comptime to reduce my binary file size and increase efficiency.

Again, Trying to avoid setting a constant by hand. I’d like to pass the value to a function because that’s the way everything else in my HAL works. So it would be elegant and streamlined.

Thanks!

First up, welcome to the place - it’s always nice to see another embedded guy here. There’s a few of us around. :slight_smile:

I think you can get all the behaviour you want with a mix of a comptime variable and a build option, like @squeek502 mentioned. Your code snippet with setComptimeConst is almost exactly the semantics of a build option, at least.

Your user would pass the frequency as a build flag instead of defining it in their code:

zig build -Dxosc_frequency=12000000

and then their code becomes:

const xosc = @import("/hal/xosc.zig")

pub fn main() noreturn {
  xosc.Oscillator.enableXOSC(false); //frequency now implicit at the API level
}

while your HAL uses:

const options = @import("hal_options");

pub fn enableXOSC(X4: bool) void {
   if(options.xosc_frequency)|freq|{ 
      // setup code goes here
   } else {
      @compileError("Attempted to enable XOSC with undefined frequency - define frequency with `zig build -Dxosc_frequency=<number>`");
   }
}

pub fn enableSystemPLL() void {
    _ = options.xosc_frequency orelse @compileError("Can not enable system PLL, XOSC is not enabled first!");
    . . .
}

pub fn setClockSource(comptime src: ClockSrc) void {
    if (src == ClockSrc.XOSC) {
        _ = options.xosc_frequency orelse @compileError("Can not set source as XOSC! It's not enabled!");
        . . .
    }
}

You could combine that with comptime variables to track that setup functions are called at appropriate times.

I realize having the frequency as an API-level thing that exists in user code is nicer ergonomics if this is how everything else in your HAL works (and what people are used to coming from C, which is a plus for embedded), but I’m not sure Zig can express the semantics you want entirely in comptime.

(happy to be proven wrong though, there’s definitely some wizards around :sweat_smile:)

1 Like

the only way to get this with a function call would be:

pub fn Xosc(comptime freq: u32) type {
    return struct {
        pub const xocFreq: u32 = freq;
        //....
    };
}
// user
pub const xosc = Xosc(120000);
xosc.xoscFreq

also naming convensions prefer xosc.freq

2 Likes

Thanks for the welcome and the reply.

I thought as much, maybe in a future version there will be a method for making compile time constants.

To the other point though, I absolutely love Zig for Embedded Systems so far. It’s so nice to get away from CMake/C and all that. I tried Rust but it’s too… I guess convoluted is the best word for it, for me.

Zig is nice and low level and does one thing and does it right. It’ll be awesome to see how it picks up speed after 1.0

1 Like

I also think that a build option would be best, you also can define a default value for build options, so that only users that want to change it have to specify it.

Also users have the choice to specify it on the command-line (by exposing it as a build option in their application) or hard code it in their build.zig as the option given to the dependency.

Build options are Zigs version of a compile time constant that can be configured from the build system.

1 Like

I agree, most people are used to setting compile options anyways. I was just trying to see if there was a super elegant way to do it. Guess the consensus is, not right now (or maybe ever) in the way I’m thinking of it.

Oh well, build option isn’t the worst thing in the world.

I believe the functionality you are looking for was intentionally removed, and likely will not be supported. See e.g. this post.

You shouldn’t compare values with undefined.

Language Reference: undefined:

“Not a meaningful value. Using this value would be a bug. The value will be unused, or overwritten before being used.”

In Debug mode, Zig writes 0xaa bytes to undefined memory. This is to catch bugs early, and to help detect use of undefined memory in a debugger. However, this behavior is only an implementation feature, not a language semantic, so it is not guaranteed to be observable to code.

2 Likes