Skip to content

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

Draft
wants to merge 10 commits into
base: dev
Choose a base branch
from

Conversation

kenbell
Copy link
Member

@kenbell kenbell commented Mar 14, 2021

See issue #1716 which proposes adding persistence regions to TinyGo

Copy link
Contributor

@ardnew ardnew left a 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)
Copy link
Contributor

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.

Copy link
Member Author

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.


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)
Copy link
Contributor

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)
}
Copy link
Member

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

Copy link
Member Author

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
Comment on lines 85 to 86
_persist_start = _spersist;
_persist_end = _epersist;
Copy link
Contributor

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.

Copy link
Member Author

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.

Comment on lines +39 to +41
*(.persist)
*(.persist.*)
Copy link
Contributor

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?

Copy link
Member

@aykevl aykevl Mar 14, 2021

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.

Copy link
Member

@aykevl aykevl left a 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.

  1. 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.
  2. I don't really see the purpose of the special Region type? Why not return a byte slice from NewRAM?
  3. Why shouldn't libraries allocate persistent RAM?
  4. How can an application detect whether the memory area has been initialized or not? Should it care?
  5. 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.


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)
Copy link
Member

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

// _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)
Copy link
Member

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.

Copy link
Member Author

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.

)

func main() {
p := persistence.NewRAM(uint32(unsafe.Sizeof(byte(0)) * 100))
Copy link
Member

@aykevl aykevl Mar 14, 2021

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).

Copy link
Member Author

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'

@kenbell
Copy link
Member Author

kenbell commented Mar 14, 2021

  1. I do not think we should be mixing RAM and flash here.

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.

  1. I don't really see the purpose of the special Region type? Why not return a byte slice from NewRAM?

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.

  1. Why shouldn't libraries allocate persistent RAM?

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.

  1. How can an application detect whether the memory area has been initialized or not? Should it care?

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.

  1. What about compatibility with the Go toolchain?

I really like the idea of using []byte and //go:persistent - feels a lot more elegant. I'll take a look at trying to do that.

@kenbell
Copy link
Member Author

kenbell commented Mar 14, 2021

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.

I've put a sketch of an prototypical app in the issue / proposal #1716, here (#1716 (comment))

@kenbell kenbell force-pushed the low-power-persistence branch from b344fd3 to bddb925 Compare April 6, 2021 02:51
@kenbell kenbell mentioned this pull request Apr 6, 2021
aykevl and others added 10 commits April 7, 2021 22:37
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
@kenbell kenbell force-pushed the low-power-persistence branch from bddb925 to 3e0e27c Compare April 8, 2021 05:45
@deadprogram
Copy link
Member

Just wondering if this is actually now better addressed via the recent flash interface changes?

@aykevl
Copy link
Member

aykevl commented Apr 4, 2023

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)
I don't think the magic seed in that example is really necessary though: practically all MCUs provide a way to read the reset reason and therefore can know whether the reset was due to a power-on reset (RAM is gone) or a software reset (RAM is still available). We just need to make this available in the machine package.

@kenbell any plans to continue working on this? Perhaps start by rebasing on the dev branch.

@kenbell
Copy link
Member Author

kenbell commented Apr 4, 2023

Yeah - I'd kinda forgotten about this. I'll take a look in the next few days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
4 participants