Fixed Addresses, Volatile and Hardware Access
Sec provides typed access to hardware without forcing normal code to build raw pointers,
scatter numeric addresses or manually express every register access in masks and shifts.
The preferred model is not “some integer happens to be a useful address”. The preferred
model is that the selected platform or project defines named hardware endpoints,
and source code binds register types to those endpoints through @address.
This chapter is therefore not only about volatile reads and writes. It defines the full
source-level contract between Sec code, the active CompilationPlan, the
platform's hardware registry and the physical endpoint operations that eventually reach
a device, peripheral block, CSR bank or memory-mapped register region.
Platform.GPIOA or
Project.FpgaControl. Raw numeric addresses are a restricted fallback, not
the primary source model.
Purpose
Register types already describe what a register layout looks like. This chapter describes where the storage comes from and how accesses are interpreted. The language must know more than a number: it may need address-domain information, alignment, legal access widths, alias relationships, read side effects, write-completion rules and whether multiple symbolic endpoints really refer to one physical resource.
Sec therefore separates four things:
- the register type definition;
- the named endpoint or validated address binding;
- the logical register operation requested by source code;
- the physical endpoint operation plan selected by the compiler and target model.
source field read/write
|
v
logical register operation
|
v
register-operation plan
|
v
physical endpoint operation(s)
Source Model
The normal source pattern is a register type plus an addressed binding.
type GpioRegisters register[32] {
Mode: bit[2]
Output: bit
_: bit[29]
}
@address(Platform.GPIOA)
let mut gpio: GpioRegisters
In this model, gpio is not an allocated object. It is a typed view of
existing external storage described by the active platform model. The endpoint name
identifies the resource. The register type identifies the layout. The binding does
not silently write any initial value to the device.
This is intentionally different from a raw-pointer model. With a named endpoint, the compiler and tooling can reason about the resource identity, validate the address-domain contract, explain diagnostics using symbolic names and preserve correctness when aliases, mirrors or platform-specific bus details exist.
Named Hardware Endpoints
A named endpoint is the canonical representation of a hardware access location. It is not merely a constant containing an integer. It represents compiler-known metadata about one endpoint or endpoint family.
A platform or project endpoint may carry facts such as:
- the address domain or bus space;
- the physical base address or platform-specific endpoint selector;
- required alignment and access-width restrictions;
- resource identity and alias relationships;
- permissions such as read-only, write-only or read-write;
- read, write or completion semantics required by the target;
- whether different symbolic names refer to the same physical resource.
Typical endpoint namespaces are:
Platform.GPIOA
Platform.UART0
Platform.Timer2
Project.FpgaControl
Project.SensorWindow
Platform-defined endpoints are the normal source for standard SoC, MCU or board resources. Project-defined endpoints are appropriate for external FPGA blocks, memory windows or application-specific device mappings.
The @address Binding
@address(...) binds a declaration to existing hardware storage.
The declaration must name the storage type explicitly. Normal addressed register
bindings are written as let or let mut declarations.
@address(Platform.GPIOA)
let mut gpio: GpioRegisters
@address(Platform.Status)
let status: StatusRegister
An addressed binding implies all of the following:
- the storage already exists outside ordinary program allocation;
- the binding is non-owning;
- ordinary reads and writes are observable hardware operations;
- the compiler must respect the endpoint contract from the active platform model;
- the declaration itself does not initialize the hardware.
Defining Endpoints
This chapter expects that stable hardware locations are defined once and then reused. A project should not spread “magic addresses” through application code when those locations are part of the platform contract.
In practice that means platform packages, target definitions or project platform files should define the relevant endpoints first, and ordinary code should refer to those names.
// preferred use in normal source code
@address(Platform.GPIOA)
let mut gpio: GpioRegisters
// project-specific endpoint
@address(Project.FpgaControl)
let mut control: FpgaControlRegisters
This improves readability and portability, and it gives the compiler enough context to produce diagnostics that talk about a named resource instead of an unexplained integer literal.
Numeric Addresses
A raw numeric form may still be accepted when a platform permits it:
@address(0x40021000)
let mut gpio: GpioRegisters
However, this is not the preferred model. A numeric address is acceptable only when the compiler can validate it for the selected platform and bind it to a valid address domain or canonical region contract. It must not weaken the platform model into “every integer is automatically a safe MMIO endpoint”.
When the resource is already known and should be named, use the named endpoint form. Numeric addresses are better suited to bring-up work, tightly constrained platform layers or cases where the platform deliberately exposes a validated numeric region.
@address should not
replace the platform hardware registry.
Volatile Semantics
Every addressed hardware binding is volatile by construction. Sec does not require
a separate volatile keyword for normal fixed-address hardware access.
The language already knows that the storage is external and observable because the
binding was established through @address.
Therefore the compiler must not assume that repeated reads are identical, and it must not remove writes merely because they appear unused by ordinary optimization.
let first := gpio.Output
let second := gpio.Output
These are two observable endpoint reads when gpio is an addressed
binding. They may still read the same value, but the compiler must not collapse the
two operations into one ordinary cached load.
Mutability and Writability
Volatility and mutability are different properties.
@address + letmeans read-only from Sec code and volatile.@address + let mutmeans writable from Sec code and volatile.
@address(Platform.Status)
let status: StatusRegister
@address(Platform.Control)
let mut control: ControlRegister
The absence of mut does not mean the hardware cannot change. It means
only that the Sec program may not write through that binding. External hardware may
still update the underlying storage at any time.
Platform endpoint metadata may further restrict legal operations. For example, a register block might expose only byte or word accesses, or a named endpoint may be read-only even if a general-purpose register type could in theory express writes.
Ordinary Values and Snapshots
Volatility belongs to the binding, not to the register type itself. A register type may also exist as an ordinary value.
let snapshot: StatusRegister := ReadStatusSnapshot()
snapshot is a normal value. Reads from it are not automatically volatile.
A method or field access on an ordinary value follows ordinary value semantics, while
the same operation on an addressed binding follows hardware endpoint semantics.
This distinction is important for temporary snapshots, cached copies, unit tests and simulations. Copying a register value does not preserve the addressed binding unless the source language explicitly keeps operating through the binding.
Logical Operations and Access Planning
Source code expresses logical register operations such as “read this field”, “write this bit” or “update this multi-bit field”. The compiler must not assume that every logical operation maps to one trivial machine access. Instead it forms a register-operation plan based on the endpoint contract.
The plan may need to consider:
- which endpoint identity is being accessed;
- which resource identity that endpoint belongs to;
- whether the source operation is a whole-register access or a field update;
- which physical access widths are legal;
- whether read-modify-write is allowed or forbidden;
- whether reads or writes have collateral effects;
- whether completion or ordering barriers are required.
For many ordinary MMIO registers, a field write can lower to a volatile read, mask/shift update and volatile write. But the language rule is not “always perform a generic RMW”. The rule is that the compiler chooses a valid plan for the selected endpoint while preserving the source-level logical meaning.
control.Enabled = true
// conceptual plan for one possible endpoint:
// 1. read register
// 2. preserve unrelated bits
// 3. set Enabled bit
// 4. write register
Aliasing and Shared Resources
Two endpoint names may refer to the same underlying hardware resource, to aliased views of one block, or to mirrored addresses with different access semantics. A raw integer address alone is often too weak to express these distinctions.
The platform model may therefore track both endpoint identity and resource identity. This lets the compiler reason about situations such as:
- two symbolic endpoints that share the same physical register bank;
- one control register exposed through separate set and clear windows;
- a debugger or DMA-visible alias of the same physical memory area;
- multiple legal access paths that differ in width or side effects.
Language-level safety rules are not relaxed merely because aliases exist. When alias behavior matters, the platform or endpoint definition must describe it explicitly.
Width, Ordering and Completion
Hardware access is not only about values. The endpoint contract may require specific access widths, alignment, completion guarantees or ordering constraints.
The active platform may define facts such as:
- only 32-bit accesses are legal for this endpoint;
- sub-word writes are illegal or have destructive side effects;
- reads must complete before subsequent accesses to another domain;
- writes may require a fence or target-specific completion operation;
- bit ordering or endianness rules are platform-defined, not guessed by source code.
These are endpoint and target facts, not ad-hoc behavior invented by optimization. Sec source code stays stable while the compiler and platform model determine how to realize the required physical operations correctly.
Methods and Properties
Register types may have ordinary impl members. Methods and properties
belong to the type, not to the endpoint storage itself. They do not change layout and
they do not introduce hidden vtables or metadata.
impl StatusRegister {
fn IsReady(ref self) bool {
return self.Ready
}
}
The same method behaves differently depending on receiver storage.
- On an addressed binding, field access must preserve addressed hardware semantics.
- On an ordinary value, field access uses ordinary value semantics.
This keeps the source model simple: methods describe behavior on the register type, while the binding determines whether that behavior talks to hardware or to a normal value copy.
@address vs RawPtr
@address is the preferred safe typed entry point for statically known
hardware resources. RawPtr[T] is the escape hatch for operations that
the compiler cannot validate from the platform model alone.
Use @address when you have:
- a stable endpoint identity;
- a register layout or other platform-defined storage contract;
- a target that can validate the binding;
- no need for arbitrary pointer arithmetic in source code.
Use RawPtr and unsafe when you need:
- dynamic addresses;
- manual pointer arithmetic;
- FFI-facing pointer exchange;
- operations outside the validated endpoint model.
A program should not convert every hardware access into raw pointers by habit. Doing so throws away endpoint identity, target validation and most of the higher-level diagnostics this chapter is trying to preserve.
Target Resolution and Cross Compilation
Hardware access must always be interpreted through the resolved
CompilationPlan. The host computer running the compiler is irrelevant.
A build performed on Linux for a Cortex-M target still uses the target's endpoint
registry, access domains, device model and hardware constraints.
This chapter therefore depends directly on the platform model from chapter 49. Named endpoints are resolved from the selected platform and target profile, not from source directory names or the host operating system.
Diagnostics
Good diagnostics should talk about the actual resource model, not only about syntax. Examples include:
- unknown endpoint
Platform.GPIOAfor the selected platform; - binding type
TimerRegisteris incompatible with endpointPlatform.UART0; - numeric address
0x40021000is not valid in the selected address domain; - field write requires a mutable addressed binding;
- requested 8-bit access is illegal for endpoint
Platform.Control; - operation may affect aliased endpoint
Platform.ControlSet.
When a named endpoint is used, diagnostics can surface the symbolic identity and its provenance from the platform definition. This is another reason to prefer endpoint names over magic numbers.
Current Implementation Status
The language model for this chapter is broader than the currently implemented front-end
subset. Today, the parser and semantic analyzer already support register types,
@address bindings, volatile marking in semantic analysis, rejection of
initializers on addressed bindings and basic field-width validation.
The currently implemented subset is still closer to the earlier integer-address model. In particular, existing front-end checks presently require a compile-time address expression and do not yet implement the full named-endpoint registry and operation-plan system described here.
This chapter documents the intended normative source model: named endpoints are the preferred form, numeric addresses are a restricted fallback, and hardware access is mediated through endpoint-aware planning rather than naive “just load from this integer” semantics.
Best Practices
- Define stable hardware locations once as named platform or project endpoints.
- Use
@address(Platform.Name)in ordinary code rather than scattering integer literals. - Treat a numeric address as a fallback, not as the main design pattern.
- Keep register layout declarations separate from platform endpoint definitions.
- Remember that
@addressbinds existing storage and does not initialize hardware. - Separate volatility from mutability: read-only bindings may still change externally.
- Prefer ordinary register values for snapshots and tests; use addressed bindings for real endpoint access.
- Do not bypass endpoint validation by converting everything to
RawPtr. - Let the platform model describe aliases, width rules and side effects explicitly.
- When diagnostics matter, choose symbolic endpoint names so the compiler can explain the resource meaningfully.
Summary
- Sec's preferred hardware-access model is a named endpoint bound through
@address. - A named endpoint is more than an integer; it carries canonical platform metadata.
- Ordinary code should define and reuse endpoint names rather than spread magic addresses.
- Numeric
@addressis a restricted fallback, not the primary design pattern. - An addressed binding refers to existing external storage and never silently allocates or initializes it.
- Every addressed binding is volatile by construction; no separate volatile keyword is required.
- Mutability controls whether Sec code may write; it does not control whether hardware may change the value.
- Volatility belongs to the binding, not to the register type itself.
- Source code expresses logical register operations that the compiler lowers through an endpoint-aware access plan.
- Endpoint identity and resource identity may matter separately because aliases and mirrors exist.
- Access width, alignment, collateral effects, ordering and completion are target facts, not ad-hoc optimizer choices.
- Methods on register types do not change layout; the receiver binding determines whether accesses are hardware or ordinary value operations.
@addressis the preferred typed safe hardware-access entry point;RawPtrremains the unsafe escape hatch.- Cross compilation always uses the target's endpoint and device model, never the compiler host's.
- The current front end implements only part of the full endpoint-aware model, but the normative direction is clear.