-
Notifications
You must be signed in to change notification settings - Fork 13
Implement union peripherals with clustering (TCA single mode / split mode on attiny's) #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Rahix
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, thanks a lot for working on this! I think the approach looks very reasonable. Although I could not find notes about using SVD clusters for overlapping regions, if svd2rust can handle it, it shall be fine for us. The resulting API from svd2rust makes sense to me at least.
I left some detailed review comments below, but generally I think there is still a bit of chaos in the data-structures representing the register groupings. Specifically, I do not like the way RegisterGroupHeader is structured, for a few reasons:
- The name
Headerdoes not really make sense when the actual registers are stored inside as well. - You allow storing a lot of "unused" data, in various ways. This is a recipe for bugs because it means some data from the
Chipstructure is silently ignored during SVD generation.- The
Peripheralcontains aBTreeMapofRegisterGroupHeaderbut you only ever access one of them directly viamodule_register_group_header(). The other "headers" are then referenced indirectly viaget_union_register_group_headers(). To me, these should be subelements then. - You have a lot of code gated on
if p.is_union() { ... } else { ... }which is a pointer to me that there should really be an enum separating these two options instead. Right now, you can store registers alongside register groups but only one of them will be used. - Related, only the
unionclass is currently dealt with. I don't think we should allow storing other classes then, when they cannot be handled properly. - There is a field for ungrouped registers inside
RegisterGroupHeader, which seems to me like it could just be theregistersfromPeripheralinstead?
- The
I feel, it would be more reasonable to do it as follows. This should only represent data that's actually used and prevents bugs from accessing things that aren't actually in use.
pub struct Peripheral {
// ...
// this is just the top-level group we find in the peripheral instance. From
// my findings, there should only ever be one `<register-group /> inside each
// peripheral instance. I think we should definitely error out if we ever
// find more, because this will need to be fixed then.
pub registers: RegisterGroup,
}
pub struct RegisterGroup {
pub name: String,
pub description: Option<String>,
pub offset: usize,
pub registers: BTreeMap<String, Register>,
pub subgroups: Vec<RegisterSubgroup>,
}
pub enum RegisterSubgroup {
Union(RegisterGroupUnion),
// this would be for the remainder of issue #4, subgroups for creating
// structure. We should properly detect arrays so svd2rust can built the
// correct accessors. But this should be added in a later PR I think.
Single(RegisterGroup),
Array(RegisterGroupArray),
}
pub struct RegisterGroupUnion {
pub name: String,
pub description: Option<String>,
pub offset: usize,
pub variants: BTreeMap<String, RegisterGroup>,
}The parsing then needs to be adjusted accordingly. I think it makes sense to create a new src/atdf/register_group.rs module module for the register group parsing.
I pushed a change to main to add the ATxmega128A1 as another regression testcase. Please rebase to pull it in for your changeset as well.
src/atdf/register.rs
Outdated
|
|
||
| let size = register_group_item_el.attributes.get("size").and_then(|d| { | ||
| if !d.is_empty() { | ||
| util::parse_int(d).ok() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is dangerous, you silently place None into the size when parsing the number fails. You should propagate the error outwards, e.g. using
| util::parse_int(d).ok() | |
| Some(util::parse_int(d)?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the size code was removed
src/atdf/register.rs
Outdated
| let offset = register_group_item_el | ||
| .attributes | ||
| .get("offset") | ||
| .and_then(|d| { | ||
| if !d.is_empty() { | ||
| util::parse_int(d).ok() | ||
| } else { | ||
| None | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, don't .ok() the parse_int() result. Propagating the error gets a little more difficult, though. I think I would do it like this:
let offset = register_group_item_el
.attributes
.get("offset")
.filter(|d| !d.is_empty())
.map(|d| d.parse::<usize>())
.transpose()?;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
applied in register_group.rs reference
src/atdf/register.rs
Outdated
| let count = register_group_item_el | ||
| .attributes | ||
| .get("count") | ||
| .and_then(|d| { | ||
| if !d.is_empty() { | ||
| util::parse_int(d).ok() | ||
| } else { | ||
| None | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And same error silencing problem here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the count code was removed
src/atdf/register.rs
Outdated
| let class = register_group_header_el | ||
| .attributes | ||
| .get("class") | ||
| .and_then(|d| if !d.is_empty() { Some(d) } else { None }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .and_then(|d| if !d.is_empty() { Some(d) } else { None }) | |
| .filter(|d| !d.is_empty()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed class from the register group struct
src/atdf/register.rs
Outdated
| let description = register_group_header_el | ||
| .attributes | ||
| .get("caption") | ||
| .and_then(|d| if !d.is_empty() { Some(d) } else { None }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .and_then(|d| if !d.is_empty() { Some(d) } else { None }) | |
| .filter(|d| !d.is_empty()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
applied in register_group.rs
src/svd/peripheral.rs
Outdated
| let registers: Vec<svd_rs::RegisterCluster> = match p.is_union() { | ||
| true => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't match on a bool value, use if {} else {} instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed the code, but I learned that you can do an inline if {} else {}. Thanks!
src/chip.rs
Outdated
| pub size: Option<usize>, | ||
| pub offset: Option<usize>, | ||
| pub count: Option<usize>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it really sensible to have these as optionals? At least for offset, you seem to encode a default value somewhere in the svd-generation code below, so I think it would be better to store the proper values here already. If the ATDF doesn't give input, default values should be set in the ATDF-parsing code already.
For size and count I couldn't find a usage site at all? Did I miss it or are the values never used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I have removed the option from offset, and defaulted it to 0.
sizecan be useful for defining address blocks in the svd peripheral, but removed it for nowcountI am not entirely sure about, I have removed it
src/chip.rs
Outdated
| #[derive(Debug, Clone)] | ||
| pub struct RegisterGroupHeader { | ||
| pub name: String, | ||
| pub class: Option<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, do we ever see classes other than union?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are supposed to be others like (struct, bitfield, array).
But I did not encounter any in the current .atdf files. I only encountered class attributes (always union) in the ATDF files coming from avr-mcu.
The newer/current ATDF files coming from the Atmel site seem to use another format. They seem to work with modes instead of nested register groups. I will probably make a PR for this one later on.
<register-group caption="16-bit Timer/Counter Type A" name="TCA" size="0x40">
<mode caption="Single Mode"
name="SINGLE"
qualifier="TCA.SINGLE.CTRLD.SPLITM"
value="0"/>
<mode caption="Split Mode"
name="SPLIT"
qualifier="TCA.SPLIT.CTRLD.SPLITM"
value="1"/>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are supposed to be others like (struct, bitfield, array).
Out of curiosity, where did you get this info?
The newer/current ATDF files coming from the Atmel site seem to use another format.
Are all ATDFs updated for this newer schema? Or just some newer MCUs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/svd/chip.rs
Outdated
| return peripheral | ||
| .get_union_register_group_headers() | ||
| .iter() | ||
| .any(|(header, _)| !header.registers.values().len() > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this just a convoluted way to say header.registers.is_empty()? Also I think this is actually inverted? The return should be true when the list is not empty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code has been removed
src/svd/chip.rs
Outdated
| if peripheral.is_union() { | ||
| return peripheral | ||
| .get_union_register_group_headers() | ||
| .iter() | ||
| .any(|(header, _)| !header.registers.values().len() > 0) | ||
| } | ||
| let regs = !peripheral.registers.is_empty(); | ||
| if !regs { | ||
| log::warn!("No registers found for peripheral {}", peripheral.name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are again using an if { return } return pattern here and this time it actually introduced a bug: You are skipping the empty peripheral warning further down in the function with your early return. Use if {} else {} instead, in this case I'd do it like this:
let regs = if peripheral.is_union() {
// ...
} else {
!peripheral.registers.is_empty()
};
if !regs {
log::warn!("No registers found for peripheral {}", peripheral.name);
}
regsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code has been removed
* WIP - improve the register group structure and update the parsing accordingly * fix the dynamic address calculation * remove unused base_address fn * rename parse_list args & run cargo fmt
|
Thanks @Rahix for the thorough review, it is much appreciated! I've rebased the branch to include the ATxmega128A1 regression test as requested. Implementation Updates
<register-group caption="USB Device Controller EP_TABLE" name="USB_EP_TABLE" size="0x122">
<!-- containing multiple registers -->
<register-group caption="USB Device Controller EP"
count="0x10"
name="EP"
name-in-module="USB_EP_PAIR"
offset="0x20"
size="0x100"/>
</register-group>
<register-group caption="USB Device Controller EP_PAIR" name="USB_EP_PAIR" size="0x10">
<register-group caption="USB Device Controller OUT"
name="OUT"
name-in-module="USB_EP"
offset="0x00"
size="0x08"/>
<register-group caption="USB Device Controller IN"
name="IN"
name-in-module="USB_EP"
offset="0x08"
size="0x08"/>
</register-group>
<register-group caption="USB Device Controller EP" name="USB_EP" size="0x8">
<!-- containing multiple registers -->
</register-group>
Address & Offset HandlingI noticed some complexities with the current address/offset calculation approach:
I've modified this to more closely align with how ATDF and SVD both use relative offsets between parents and children, which simplifies the nested register-group handling. |
* fix invalid address block regressions * re-implement baselining the start address & fix address block issue * apply PR comments * run format
|
Hi @Rahix , just checking in to see if you’ve had a chance to take a look at the updated PR. Let me know if there’s anything else I can do to move this forward. |
|
Didn't get to it yet, but your PR is on my list :) Will ping you once I have reviewed your updated changeset! |

This PR addresses part of issue #4, specifically adding support for union types.
The TCA0 modes were already implemented at some point, but the functionality was lost in the commit history. I reintroduced them to peripherals with both modes clustered.
I noticed that the svd2rust crate allows peripherals to use clustered registers instead of direct registers. This feature allows a good handling for the TCA Peripheral as it has 2 different modes, Single or Split.
Example solution
Clustering example in svd
Example ATDF:
Testing: Tests passed locally.
Please review and let me know if additional changes or tests are needed!
notes
This only partially fixes the #4, as there are sub register-groups without the module register group being a union class (f.i. avr64du28 - "USB" peripheral). This was out of scope for my changes. This would need further analyses of how to include the registers of these sub groups. I already prepared the atdf peripherals to include these references, but without further implementation. It looks like these sub register-groups can be recursive as well.