Module atsamd_hal::clock::v2  
source · [−]Expand description
Version 2 of the clock module
Overview
This module provides a simple, ergonomic, and most of all safe API to create and manage the clock tree in ATSAMD5x and E5x devices. It uses type-level programming techniques to prevent users from creating invalid or unsound clocking configurations.
Note: Using a type-level API does place some limitations on how the clocks can be configured. The types must be checked at compile-time, which means the state of a given clock must also be known at compile-time. This is exceedingly reasonable for most clocking configurations, because most users set up their clocks once and never change them again. However, if you need to dynamically change the clocking configuration at run-time, you may find this API less ergonomic. A future, fully dynamic API has been discussed, but nothing has been developed so far.
The sections that follow provide an explanation of key concepts in the
module. We highly recommend users read through them to better understand the
clock module API. A complete example is also
provided.
Clock safety
A clock tree represents dependencies among clocks, where producer clocks feed consumer clocks. Root clocks are the original producers, as they are derived from oscillators or external clocks. Branch clocks are both producers and consumers, since they modify and distribute clocks. And leaf clocks are consumers only; they drive peripherals or external clock outputs but do not feed other clocks.
To safely create and use a clock tree, it is critical that producer clocks
not be modified or disabled while their consumer clocks are still in active
use. Stated differently, if clock B consumes clock A, then clock A
must not be modified or disabled while clock B is still in use.
Notice that this requirement mimics the principle of “aliased XOR mutable” underlying the Rust borrow checker. A producer clock can only be modified if it is not “borrowed” (consumed) by any other clocks.
The following sections will review the various type-level programming
techniques used to enforce this principle in the clock module.
Clock state machines
Each available clock is represented in Rust as a unique, singleton object.
Users cannot create two instances of the same clock without using unsafe.
However, a given clock is not always represented with the same type. Specifically, each clock has at least two representations, one for the configured and enabled clock, and another for the unconfigured and disabled clock.
These states are represented in Rust using distinct types, forming a
type-level state machine. Moreover, the disabled state is always represented
by a Token type. As the name implies, Tokens have no functionality on
their own; they can only be exchanged for a different type representing
another state.
Clock relationships
In general, there are two classes of clock in ATSAMD chips. Some clocks map
one-to-one (1:1) to a specific bus or peripheral. This is true for the AHB
clocks (AhbClks), APB clocks (ApbClks), GCLK outputs (GclkOuts),
peripheral channel clocks (Pclks), and RTC oscillator (RtcOsc).
Other clocks form one-to-many (1:N) relationships, like the external crystal
oscillator (Xosc), the 48 MHz DFLL (Dfll) or the two DPLLs
(Dpll).
The clock module uses a distinct approach for each class.
1:1 clocks
One-to-one relationships are easily modelled in Rust using move semantics.
For example, an enabled peripheral channel clock is represented as a
Pclk object. The respective peripheral API can move the Pclk and take
ownership of it. In that case, the Pclk acts as proof that the peripheral
clock is enabled, and the transfer of ownership prevents users from
modifying or disabling the Pclk while it is in use by the peripheral.
One-to-one clocks generally have little to no configuration. They are
typically converted directly from disabled Token types to fully enabled
clock types. For example, the Pclk type has only two methods,
Pclk::enable and Pclk::disable, which convert PclkTokens to
Pclks and vice versa.
1:N clocks
One-to-many relationships are more difficult to model in Rust.
As discussed above, we are trying to create something akin to “aliased XOR
mutable”, where producer clocks cannot be modified while used by consumer
clocks. A natural approach would be to use the Rust borrow checker directly.
In that case, consumer clocks would hold &Producer references to the
Producer clock object. The existence of outstanding shared borrows would
naturally prevent users from calling Producer methods taking &mut self.
Unfortunately, while this approach could work, there is a critical problem
with disastrous consequences for ergonomics. To satisfy the Rust borrow
checker, Producer clock objects could not be moved while &Producer
references were still held by consumer clocks.
However, this restriction is unnecessary. A Producer clock object is
merely a semantic object representing the “idea” of a producer clock. And
“borrowing” the producer is not meant to protect memory from corruption.
Rather, our goal is only to restrict the Producer API, to prevent it from
being modified or disabled once it has been connected to a consumer. We
don’t need to permanently hold the Producer object in place to do that.
It is possible to build a clock API based on the borrow checker, but it
would be extremely frustrating to use in practice, because of restrictions
on the movement of Producer objects.
Instead, the clock module takes a different approach. It uses type-level
programming to track, at compile-time, the number of consumer clocks, N,
fed by a particular producer clock. With this approach, we can move
Producer objects while still making them impossible to modify if N > 0.
The following sections will describe the implementation of this strategy.
Tracking N at compile-time for 1:N clocks
We have two specific goals. We need to both track the number of consumer clocks, N, that are actively using a given producer clock. And we need to restrict the producer clock API when N > 0.
A compile-time counter
First, we need to develop some way to track the number of consumer clocks, N, within the type system. To accomplish this, we need both a way to represent N in the type system and a way to increase or decrease N when making or breaking connections in the clock tree.
To represent N, we can use type-level, Unsigned integers from the
typenum crate (i.e. U0, U1, etc). And we can use a type
parameter, N, to represent some unknown, type-level number.
Next, we need a way to increase or decrease the type parameter N. The
typenum crate provides type aliases Add1 and Sub1 that map from
each Unsigned integer to its successor and predecessor types,
respectively. We can leverage these to create our own type with a counter
that we Increment or Decrement at compile-time. These two traits
form the foundation for our strategy for handling 1:N clocks in this module.
The Enabled wrapper
Our representation of a 1:N producer clock is Enabled<T, N>, which is a
wrapper struct that pairs some enabled clock type T with a type N
representing a consumer count. The wrapper restricts access to the
underlying clock type, T, allowing us to selectively define methods when
N = U0, that is, when there are no consumers of a given producer clock.
The Enabled type itself implements Increment and Decrement as
well, which allows type-level transformations to increment or decrement the
counter, e.g. Enabled<T, U0> to Enabled<T, U1>. Such transformations can
only be performed within the HAL; so users cannot change the consumer count
arbitrarily.
Acting as a clock Source
Finally, we need to define some generic interface for interacting with 1:N producer clocks. However, when designing this interface, we need to be careful not to lose information during type-level transformations.
In particular, the Enabled counter type alone is not enough for proper
clock safety. If we used consumer A to Increment producer P from
Enabled<P, U0> to Enabled<P, U1>, but then used consumer B to
Decrement the producer back to Enabled<P, U0>, we would leave consumer
A dangling.
To solve this problem, we need some way to guarantee that a given consumer
can only Decrement the same producer it Incremented. Stated differently,
we need a way to track the identity of each consumer’s clock source.
The Source trait is designed for this purpose. It marks
Enabled<T, N> producer clocks, and it’s associated type, Id, is the
identity type that should be stored by consumers.
Given that all implementers of Source are instances of Enabled<T, N>,
the naïve choice for Source::Id would be T. However, in a moment, we
will see why this choice is not ideal.
Id types
Many of the clock types in this module have additional type parameters that
track the clock’s configuration. For instance, Xosc0<M> represents one
of the external crystal oscillators. Here, the type parameter M represents
the XOSC’s Mode, which can either be CrystalMode or
ClockMode. Accordingly, methods to adjust the crystal current, etc. are
only available on Xosc0<CrystalMode>.
While these type parameters are important and necessary for configuration of
a given producer clock, they are not relevant to consumer clocks. A consumer
clock does not need to know or care which Mode the XOSC is using, but
it does need to track that its clock Source is XOSC0.
From this, we can see that Enabled<Xosc0<M>, N> should not implement
Source with Source::Id = Xosc0<M>, because that would require consumers
to needlessly track the XOSC Mode.
Instead, this module defines a series of Id types representing the
identity of a given clock, rather than the clock itself. This is like the
distinction between a passport and a person. A passport identifies a person,
regardless of changes to their clothes or hair. The Id types serve to
erase configuration information, representing only the clock’s identity.
For Xosc0<M>, the corresponding Id type is Xosc0Id. Thus,
Enabled<Xosc0<M>, N> implements Source with Source::Id = Xosc0Id.
Notes on memory safety
Register interfaces
Although HAL users see Token types as merely opaque objects, internally
they serve a dual purpose as the primary register interface to control the
corresponding clock. Moreover, they also fundamentally restructure the way
registers are accessed relative to the PAC.
Each of the four PAC clocking structs (OSCCTRL, OSC32KCTRL, GCLK
and MCLK) is a singleton object that controls a set of MMIO registers.
It is impossible to create two instances of any PAC object without unsafe.
However, each object controls a large set of registers that can be further
sub-divided into smaller sets for individual clocks. For example, the
GCLK object controls registers for 12 different clock generators and 48
peripheral channel clocks.
Token types serve to break up the large PAC objects into smaller,
more-targetted pieces. And in the process, they also remove the PAC objects’
interior mutability. But this is only possible because each Token is
also a singleton, and because individual clocks are configured through
mutually exclusive sets of registers.
Bus clocks
Bus clocks are fundamentally different from the other clock types in this
module, because they do not use mutually exclusive registers for
configuration. For instance, the registers that control Dpll0 are
mutually exclusive to those that control Dpll1, but ApbClk<Sercom0>
and ApbClk<Sercom1> share a single register.
This presents a challenge for memory safety, because we need some way to
guarantee that there are no data races. For example, if both
ApbClk<Sercom0> and ApbClk<Sercom1> tried to modify the APBAMASK
register from two different execution contexts, a read/modify/write
operation could be preempted, leading to memory corruption.
To prevent data races when controlling bus clocks, we introduce two new
types to mediate access to the shared registers. For AhbClks, this is
the Ahb type; and for ApbClks, this is the Apb type. In a sense,
the Ahb and Apb types represent the actual corresponding buses. Thus,
enabling an APB clock by converting an ApbToken into an ApbClk
requires exclusive access to the Apb in the form of &mut Apb.
Getting started
To set up a clock tree, start by trading the PAC-level
clocking structs for their HAL equivalents. Right now, the only way to do so
safely is using the clock_system_at_reset function, which assumes all
clocks are in their default state at power-on reset. If this is not the
case, because, for example, a bootloader has modified the clocks, then you
may need to manually create the matching configuration using unsafe code.
use atsamd_hal::clock::v2::clock_system_at_reset;
use atsamd_hal::pac::Peripherals;
let mut pac = Peripherals::take().unwrap();
let (buses, clocks, tokens) = clock_system_at_reset(
    pac.OSCCTRL,
    pac.OSC32KCTRL,
    pac.GCLK,
    pac.MCLK,
    &mut pac.NVMCTRL,
);At this point, you may notice that the function returned three different
objects, the Buses, Clocks and Tokens.
The Buses struct contains the Ahb and Apb objects, which
represent the corresponding AHB and APB buses. See the notes on memory
safety for more details on these types.
The Clocks struct contains all of the clocks that are enabled and
running at power-on reset, specifically:
- All of the AhbClks
- Some of the ApbClks
- The 48 MHz Dfll, running in open-loop mode, represented as asEnabled<Dfll, U1>.N = U1here becauseGclk0consumes it. See above for details onEnabled<T, N>.
- Gclk0, sourced by the- Dflland represented as- Enabled<Gclk0<DfllId>, U1>. Note the use of- DfllIdas an- Idtype here. Although- Gclk0is not consumed by any clock represented in this module, it is consumed by the processor’s main clock. We represent this by setting- N = U1, which we use to restrict the available API. Specifically,- EnabledGclk0has special methods not available to other- Gclks.
- The OscUlp32kBaseclock, which can act as aSourcefor theOscUlp1kandOscUlp32kclocks. It has no consumers at power-on reset, so it is represented asEnabled<OscUlp32kBase, U0>. However, it can never be disabled, so we provide no.disable()method.
The Tokens struct contains all of the available Tokens, which
represent clocks that are disabled at power-on
reset. Each Token can be exchanged for a corresponding clock object.
Example clock tree
Finally, we will walk through the creation of a simple clock tree to illustrate some of the remaining concepts inherent to this module.
Starting from the previous snippet, we have the Buses, Clocks and
Tokens to work with, and our clock tree at power-on reset looks like
this.
DFLL (48 MHz)
└── GCLK0 (48 MHz)
    └── Main clock (48 MHz)
Our goal will be a clock tree that looks like this:
XOSC0 (8 MHz)
└── DPLL0 (100 MHz)
    └── GCLK0 (100 MHz)
        ├── Main clock (100 MHz)
        ├── SERCOM0 peripheral clock
        └── Output to GPIO pin
We will use an external crystal oscillator running at 8 MHz to feed a DPLL, which will increase the clock frequency to 100 MHz. Then, we will reconfigure GCLK0 to use the 100 MHz DPLL clock instead of the 48 MHz DFLL clock.
First, let’s import some of the necessary types. We will see what each type represents in turn.
use atsamd_hal::{
    clock::v2::{
        clock_system_at_reset,
        dpll::Dpll,
        pclk::Pclk,
        xosc::Xosc,
    },
    gpio::Pins,
    pac::Peripherals,
    time::U32Ext,
};To create an instance of Xosc, we will first need to identify which of
the two XOSC clocks we will use. Suppose an external crystal is attached to
pins PA14 and PA15. These pins feed the XOSC0 clock, so we will want to
create an instance of Xosc0. Note that Xosc0<M> is merely an alias for
Xosc<Xosc0Id, M>. Here, Xosc0Id represents the
identity of the XOSC0 clock, rather than the clock
itself, and M represents the XOSC Mode.
Next, we access the Tokens struct to extract the corresponding
XoscToken for XOSC0, and we trade the PAC PORT struct for the
gpio::Pins struct to access the GPIO pins. We can then call
Xosc::from_crystal to trade the token and Pins to yield an instance
of Xosc0. In doing so, we also provide the oscillator frequency.
Finally, we can chain a call to the Xosc::enable method to enable the
XOSC and return an instance of EnabledXosc0<M, N>, which is simply an
alias for Enabled<Xosc0<M>, N>. In this case, we get
EnabledXosc0<CrystalMode, U0>.
let pins = Pins::new(pac.PORT);
let xosc0 = Xosc::from_crystal(
    tokens.xosc0,
    pins.pa14,
    pins.pa15,
    8.mhz(),
).enable();Next, we want to use a DPLL to multiply the 8 MHz crystal clock up to 100
MHz. Once again, we need to decide between two instances of a clock, because
each chip has two Dplls. This time, however, our decision between
Dpll0 and Dpll1 is arbitrary.
Also note that, like before, Dpll0<I> and Dpll1<I> are aliases for
Dpll<Dpll0Id, I> and Dpll<Dpll1Id, I>. Dpll0Id and Dpll1Id
represent the identity of the respective DPLL, while I represents the
Id type for the Source driving the DPLL. In this
particular case, we aim to create an instance of Dpll0<Xosc0Id>.
Only certain clocks can drive the DPLL, so I is constrained by the
DpllSourceId trait. Specifically, only the Xosc0Id, Xosc1Id,
Xosc32kId and GclkId types implement this trait.
As before, we access the Tokens struct and use the corresponding
DpllToken when creating an instance of Dpll. However, unlike before,
we are creating a new clock-tree relationship that must be tracked by the
type system. Because DPLL0 will now consume XOSC0, we must Increment
the Enabled counter for EnabledXosc0.
Thus, to create an instance of Dpll0<XoscId0>, we must provide the
EnabledXosc0, so that its U0 type parameter can be incremented to U1.
The Dpll::from_xosc method takes ownership of the EnabledXosc0 and
returns it with this modified type parameter.
This is the essence of clock safety in this module. Once the counter type
has been incremeneted to U1, the EnabledXosc0 can no longer be modified
or disabled. All further code can guarantee this invariant is upheld. To
modify the EnabledXosc0, we would first have to use Dpll::free_source to
disable the DPLL and Decrement the counter back to U0.
let (dpll0, xosc0) = Dpll::from_xosc(tokens.dpll0, xosc0);Next, we set the DPLL pre-divider and loop divider. We must pre-divide
the XOSC clock down from 8 MHz to 2 MHz, so that it is within the valid
input frequency range for the DPLL. Then, we set the DPLL loop divider, so
that it will multiply the 2 MHz clock by 50 for a 100 MHz output. We do not
need fractional mutiplication here, so the fractional loop divider is zero.
Finally, we can enable the Dpll, yielding an instance of
EnabledDpll0<XoscId0>.
let dpll0 = dpll0.prediv(4).loop_div(50, 0).enable();So far, our clock tree looks like this
DFLL (48 MHz)
└── GCLK0 (48 MHz)
    └── Main clock (48 MHz)
XOSC0 (8 MHz)
└── DPLL0 (100 MHz)
Our next task will be to swap GCLK0 from the 48 MHz DFLL to the 100 MHz
DPLL. To do that, we will use the special swap_sources method on
EnabledGclk0 to change the base clock without disabling GCLK0 or the
main clock.
This time we will be modifying two Enabled counters simultaneously.
We will Decrement the EnabledDfll count from U1 to U0, and
we will Increment the EnabledDpll0 count from U0 to U1.
Again, we need to provide both the DFLL and DPLL clocks, so that their
type parameters can be changed.
let (gclk0, dfll, dpll0) = clocks.gclk0.swap_sources(clocks.dfll, dpll0);At this point, the DFLL is completely unused, so it can be disbled and
deconstructed, leaving only the DfllToken.
let dfll_token = dfll.disable().free();Our clock tree now looks like this:
XOSC0 (8 MHz)
└── DPLL0 (100 MHz)
    └── GCLK0 (100 MHz)
        └── Main clock (100 MHz)
We have the clocks set up, but we’re not using them for anything other than the main clock. Our final steps will create SERCOM APB and peripheral clocks and will output the raw GCLK0 to a GPIO pin.
To enable the APB clock for SERCOM0, we must access the Apb bus struct.
We provide an ApbToken to the Apb::enable method and receive an
ApbClk in return. APB clocks are 1:1 clocks,
so the ApbClk is not wrapped with Enabled.
let apb_sercom0 = buses.apb.enable(tokens.apbs.sercom0);To enable a peripheral channel clock for SERCOM0, we must provide the
corresponding PclkToken, as well as the instance of EnabledGclk0, so
that its counter can be incremented. The resulting clock has the type
Pclk<Sercom0, Gclk0Id>.
let (pclk_sercom0, gclk0) = Pclk::enable(tokens.pclks.sercom0, gclk0);Like Dpll<D, I>, Pclk<P, I> also takes two type parameters. The
first represents the corresponding peripheral, while the second is again an
Id type representing the Source driving the Pclk,
which is restricted by the PclkSourceId trait. Because peripheral
channel clocks can only be driven by GCLKs, PclkSourceId is effectively
synonymous with the GclkId trait.
Finally, we would like to output GCLK0 to a GPIO pin. Doing so takes a
slightly different approach. This time, we provide a GPIO Pin to the
Gclk, which creates a GclkOut and Increments the consumer count
for EnabledGclk0.
let (gclk0, gclk0_out) = gclk0.enable_gclk_out(pins.pb14);We have arrived at our final, desired clock tree. Putting the whole example together, we get
use atsamd_hal::{
    clock::v2::{
        clock_system_at_reset,
        dpll::Dpll,
        pclk::Pclk,
        xosc::Xosc,
    },
    gpio::Pins,
    pac::Peripherals,
    time::U32Ext,
};
let mut pac = Peripherals::take().unwrap();
let (mut buses, clocks, tokens) = clock_system_at_reset(
    pac.OSCCTRL,
    pac.OSC32KCTRL,
    pac.GCLK,
    pac.MCLK,
    &mut pac.NVMCTRL,
);
let pins = Pins::new(pac.PORT);
let xosc0 = Xosc::from_crystal(
    tokens.xosc0,
    pins.pa14,
    pins.pa15,
    8.mhz(),
)
.enable();
let (dpll0, xosc0) = Dpll::from_xosc(tokens.dpll0, xosc0);
let dpll0 = dpll0.prediv(4).loop_div(50, 0).enable();
let (gclk0, dfll, dpll0) = clocks.gclk0.swap_sources(clocks.dfll, dpll0);
let dfll_token = dfll.disable().free();
let apb_sercom0 = buses.apb.enable(tokens.apbs.sercom0);
let (pclk_sercom0, gclk0) = Pclk::enable(tokens.pclks.sercom0, gclk0);
let (gclk0, gclk0_out) = gclk0.enable_gclk_out(pins.pb14);