Just finished chapter 10 of the Rust book and I thought I’d try digging into some real code. I’m a rust noob, but I’ve got 15 years with embedded C.
I’m working my way through the source code of the stm32l0xx-hal crate. Inside adc.rs on line 96, an implementation for the Adc struct is created for the type Adc<Ready> where Ready is defined on line 324.
/// Analog to Digital converter interface
pub struct Adc<State> {
rb: ADC,
sample_time: SampleTime,
align: Align,
precision: Precision,
_state: State,
}
impl Adc<Ready> {
pub fn new(adc: ADC, rcc: &mut Rcc) -> Self {
// Enable ADC clocks
ADC::enable(rcc);
adc.cr.modify(|_, w| w.advregen().set_bit());
Self {
rb: adc,
sample_time: SampleTime::T_1_5,
align: Align::Right,
precision: Precision::B_12,
_state: Ready,
}
}
...
}
...
/// Indicates that the ADC peripheral is ready
pub struct Ready;
I’m a little confused about how the associated function new() works. I understand that it generates a new Adc<Ready>, but I don’t fully understand why it’s inside the impl Adc<Ready> block.
It’s not a method that would be called on an Adc<Ready> type, and it doesn’t take any arguments of type Ready. Couldn’t it just as easily have been inside a impl<T> Adc<T> block?
Or is it that impl types go both ways. Like you can’t return an Adc<Ready> unless you’re inside an impl Adc<Ready>?
Sidenote: Since the Ready struct has no fields, does “Ready” actually create a new instance of a Ready type? (like you don’t need Ready{}?)
You’ve essentially understood things except for one minor detail. The piece you’re missing is that the function returns the special type of
Self, which because it’s in the impl block forAdc<Ready>is effectively an alias forAdc<Ready>as opposed to some arbitraryAdc<T>.newcould have been put into the genericAdcimpl but thenSelfcouldn’t have been used and it wouldn’t be conventional. The Rust convention is to have a function callednewon a type that returnsSelfto construct instances of some type.Structs with no fields are known as Zero-Sized Types and are commonly used as markers or tags for state, and can be “created” using just their name as you assumed. You’ll sometimes also see
PhantomData<T>which is used to keep track of some type paramTthat is otherwise not used without taking up extra memory in a struct. The reason that isn’t used forAdcis that whileActiveis a ZST, some of the other states aren’t, so space for them will be reserved in theAdcstruct.Edit: also be careful not to confuse
AdcwithADC, those are two different types and you mixed them up a couple times in your post.So it really comes down to convention? It’ll compile fine in either case? What’s the motivation behind that convention?
Thanks for the tip on PhantomData and the edit! I corrected my post.
If you put it into a generic
impl<T> Adc<T>block, then the type needs to be specified during initialization, like so:let adc = Adc::<Ready>::new(); //or like so: let adc: Adc<Ready> = Adc::new();But what’s much more tricky, is that you can’t create a generic
Tobject inside thenew()function.
If you don’t need to transport data, then you can make it work withPhantomData(which does not require initialization): https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=ea9c3eed4897a1a480eb5c5b4708fc2dIf you do need to transport data, then you may need to require all
Ts to implement some shared trait with a constructor, or you could switch to an enum: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=2b995b4f854061fad8aa4c0a05d8c304Enum is often the simplest, but wouldn’t work for
stm32l0xx-halhere, because they use the generic type to limit how their API can be used (certain methods can only be called onAdc<Ready>, not onAdc<Active>, for example).But yeah, using an
impl Adc<Ready>block, like in the original code, is a good solution here. This is definitely on the advanced side, though. You only need this amount of nuance, if you’re designing library APIs for others to use. I also have to say that while I’ve probably seen usage ofimpl Adc<Ready>before, I did not have it on my radar that this is a possible solution here.You only need this amount of nuance, if you’re designing library APIs for others to use
Maybe I bit off a lot in my first foray, but my ultimate goal is to write a driver for the segment LCD controller in this chip. I have a very simple project already written in C that uses it, and I’d like to try getting it working in embedded Rust. There isn’t much nuance to the controller (just setting a few register values), so it’ll likely be simpler than the ADC, but I’m digging into the existing drivers for some guidance.
Yeah, I mean, you’ve got previous programming experience. You evidently know that you’re tackling a harder problem and probably know when to step back and try an easier problem first, if it becomes too frustrating. I just wanted to give a bit of context, so that you know it’s more exotic knowledge, because that’s not always going to be obvious when learning a new language…
Oh I super appreciate the advice. Thanks!
The problem with putting
newintoimpl<T> Adc<T>is that becauseselfandTaren’t used by the function the compiler has no way to work out whatTis supposed to be which would force you to define it (even though it doesn’t actually matter). So E.G. if you attempted to invokelet adc = Adc::new(...);the compiler would complain that it was unable to determine whatTis even though it doesn’t matter. You would for instance need to do something likelet adc = Adc::<()>::(...). By putting it inside theimpl Adc<Ready>block you’ve constrained the type ofTtoReadyand therefore there’s no need to specify it when callingnew. So while both approaches are functionally identical, one is more convenient to use than the other.That makes sense! Thank you!
It’s funny how much of Rust appears to be writing footnotes for the compiler.
When you think about it, that’s all programming really is, it’s just that Rust pays a bit more attention than other languages and does a better job at spotting problems at compile time instead of letting them turn into runtime problems.

