-
Notifications
You must be signed in to change notification settings - Fork 947
all: prototype of RAM persistence across reset #1715
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: dev
Are you sure you want to change the base?
Conversation
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 really neat! Only a few questions/comments added
// SubAllocation creates a new persistence object that | ||
// represents a sub-allocation of the main persistence | ||
// area | ||
SubAllocation(offset int, len int) (p Region, err error) |
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.
What is the imagined use-case of this? Seems like it could be left out (for now) to simplify the interface until implementations have been fleshed out and tested.
It seems like more of a convenience that isn't truly essential for this initial eval.
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.
I was thinking along the lines of slices in Go - being able to divide the space up, so parts can be handed off to different libraries without risk of bounds overflow.
compiler/ramallocation.go
Outdated
|
||
func (b *builder) createRAMAllocation(instr *ssa.CallCommon) error { | ||
// Get the size, which must be a compile-time constant. | ||
size, ok := instr.Args[0].(*ssa.Const) |
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.
It could be really useful to be able to pass in a type and determine the size to allocate accordingly.
This would let users define persistent variables; when their type definition changes, the reserved region grows/shrinks accordingly, without having to redo any arithmetic and update the NewRAM
call separately (or risk forgetting to do so).
Just an idea, which may be a bit more elaborate than necessary for now.
Example use case:
type Config struct {id int}
func main() {
p := persistence.NewRAM(Config)
// would be cool to either re-use the `make` builtin somehow, or create an equivalent:
// p := persist(Config)
// q := persist([]rune, 1024)
}
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.
That may sound like a good idea, but has some issues in practice. For example: you can't put pointers in this area (and thus no slices, strings, etc.) because they live on the heap which is reinitialized on reset.
This issue details why this is a bad idea for a very similar use case on server hardware: golang/go#43810
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.
Yeah - i think serialization needs to be explicit.
targets/arm.ld
Outdated
_persist_start = _spersist; | ||
_persist_end = _epersist; |
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.
Can you not just define these above in place of _spersist
and _epersist
? There doesn't appear to be anything else using these symbols, so I suggest getting rid of them.
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.
I was just aiming to follow the pattern in the file - i'm more than happy to just use _spersist, _epersist. It would work fine.
*(.persist) | ||
*(.persist.*) |
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.
I've never understood the convention with these includes, are they both necessary? Wouldn't *(.persist*)
, or, in this case, simply *(.persist)
be sufficient?
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.
It's simply a glob. So .persist
matches exactly the string .persist
while .persist.*
matches anything that starts with .persist.
. This emulates somewhat the similar conventions around -ffunction-sections
and -fdata-sections
but may not be necessary in this case.
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.
While I think this is an interesting idea, I have a few concerns with this implementation.
- I do not think we should be mixing RAM and flash here. While reading RAM and flash is usually similar (just read as any normal pointer), writing most definitely is not. For example: in flash, usually a whole page (1024 bytes, 4069 bytes, or even bigger) needs to be erased at a time.
- I don't really see the purpose of the special Region type? Why not return a byte slice from
NewRAM
? - Why shouldn't libraries allocate persistent RAM?
- How can an application detect whether the memory area has been initialized or not? Should it care?
- What about compatibility with the Go toolchain?
Especially the last point is important to me. I think it is fairly simple to make this compatible with the Go toolchain by doing something similar to the new file embedding support in Go 1.16. I think the API could be simplified to the following:
//go:persistent
var somePersistentMemory [100]byte
The semantics could for example be: the memory area is reset to zero with a power-on reset but otherwise left as-is (for example, with a wake up from deep sleep). In the Go toolchain the variable is always zero initialized on startup.
That requires detecting a power-on reset on every supported chip. However, I think this feature is almost useless without a way to know whether some data has been written to memory or not: RAM is not necessarily all zeroes on startup, it's more likely to be random data.
Overall, I'm curious whether you have a use case in mind? You've mentioned a few examples where it could be useful, but few practical examples that currently apply to TinyGo. It really helps to know how it will be used to shape what the API looks like.
compiler/ramallocation.go
Outdated
|
||
func (b *builder) createRAMAllocation(instr *ssa.CallCommon) error { | ||
// Get the size, which must be a compile-time constant. | ||
size, ok := instr.Args[0].(*ssa.Const) |
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.
That may sound like a good idea, but has some issues in practice. For example: you can't put pointers in this area (and thus no slices, strings, etc.) because they live on the heap which is reinitialized on reset.
This issue details why this is a bad idea for a very similar use case on server hardware: golang/go#43810
compiler/ramallocation.go
Outdated
// _persist$buf is the allocated space. | ||
bufType := llvm.ArrayType(llvm.Int8Type(), int(size.Int64())) | ||
buf := llvm.AddGlobal(b.mod, bufType, "_persist$buf") | ||
buf.SetLinkage(llvm.ExternalLinkage) |
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.
Why external linkage? It should be internal linkage to help the optimizer.
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.
I'll admit - i don't really know what the values do, I was bashing on values until one worked :)
I'll take another look.
src/examples/persistence/main.go
Outdated
) | ||
|
||
func main() { | ||
p := persistence.NewRAM(uint32(unsafe.Sizeof(byte(0)) * 100)) |
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 can assume unsafe.Sizeof(byte)
is always 1. Doing something different would break the Go language specification: https://golang.org/ref/spec#Size_and_alignment_guarantees
I have absolutely no plans of supporting architectures that do not support byte addressing. There is hardware that doesn't support byte addressing, but it's all very special purpose (DSPs etc.). And even if TinyGo would support this hardware, it would still follow the Go specification (whatever that might look like in practice).
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.
Yeah - you're right, that example is excessively complex - should just be 'i need a region of X bytes'
Reading your thoughts, I think you're right.. There's enough different between them that trying to pretend they're the same will add a lot of complexity without much (if any) clear benefit.
I wasn't sure how that would interact with the GC. It would be a lot cleaner if that was the interface. I'll investigate.
There's an ordering constraint I'm not sure how to fix. I don't think the compiler / linker chain guarantees the order symbols are packed into sections - so on a rebuild the layout could vary. For RAM, this probably isn't a concern (good chance the RAM is wiped anyway on a re-flash) - but for Flash, would be good to make the ordering explicit, which is why I left it to the app to allocate the data to libraries. I'm hoping we can have it so that on a reasonable sub-set of devices, flashing a new app will leave the config intact. This may be another good reason to separate RAM and Flash, since letting libraries allocate persistent RAM on the understanding it will not be guaranteed (or even likely) to be preserved across re-builds / re-flashing seems a good approach.
I'm thinking magic values guarding the memory region, or some kind of CRC - would be use-case specific. If the guard values are wrong, or the CRC is invalid then assume uninitialized and do the initialization. STM32 does the same for RTC - it has a few bytes that are explicitly there for detecting if the RTC has been initialized - they document to set to a magic value of the developer's choosing after initializing the RTC.
I really like the idea of using |
I've put a sketch of an prototypical app in the issue / proposal #1716, here (#1716 (comment)) |
b344fd3
to
bddb925
Compare
These pragmas weren't really tested anywhere, except that some code might break if they are not properly applied. These tests make it easy to see they work correctly and also provide a logical place to add new pragma tests. I've also made a slight change to how functions and globals are created: with the change they're also created in the IR even if they're not referenced. This makes testing easier.
This patch adds a new pragma for functions and globals to set the section name. This can be useful to place a function or global in a special device specific section, for example: * Functions may be placed in RAM to make them run faster, or in flash (if RAM is the default) to not let them take up RAM. * DMA memory may only be placed in a special memory area. * Some RAM may be faster than other RAM, and some globals may be performance critical thus placing them in this special RAM area can help. * Some (large) global variables may need to be placed in external RAM, which can be done by placing them in a special section. To use it, you have to place a function or global in a special section, for example: //go:section .externalram var externalRAMBuffer [1024]byte This can then be placed in a special section of the linker script, for example something like this: .bss.extram (NOLOAD) : { *(.externalram) } > ERAM
bddb925
to
3e0e27c
Compare
Just wondering if this is actually now better addressed via the recent flash interface changes? |
No, I don't think the flash API is appropriate for this. Flash is very different from RAM. I do quite like the design here: #1716 (comment) @kenbell any plans to continue working on this? Perhaps start by rebasing on the dev branch. |
Yeah - I'd kinda forgotten about this. I'll take a look in the next few days. |
See issue #1716 which proposes adding persistence regions to TinyGo