Skip to content
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

rp2040: implementation of i2c target mode #3301

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

kenbell
Copy link
Member

@kenbell kenbell commented Nov 20, 2022

Implements i2c target mode on RP2040

Includes example where a single RP2040 can implement both controller and target on it's two I2C controllers (requires them to be physically wired together).

The API is intentionally low-level since not all devices implement a 'memory map' over I2C:

  • Some devices have 16 bit registers (not 8 bit)
  • Some devices are triggered by accesses to certain addresses

I'd envisage libraries outside Tinygo could provide simpler abstractions (e.g. an interrupt-safe 8-bit / 16-bit registers, etc)

I've updated terminology to controller/target. The RP2040 spec continues to use the terms 'master/slave', but I thought it preferable to align on the 'controller/target' terminology in TinyGo as this is the first i2c target implementation. controller/target is used in the latest 'official' spec for I2C: UM10204 Rev. 7.0 — 1 October 2021, which includes this note: Updated the terms "master/slave" to "controller/target" throughout to align with MIPI I3C specification and NXP's Inclusive Language Project

@deadprogram
Copy link
Member

deadprogram commented Nov 20, 2022

Very cool!

Also see conversation in #1904 for some of what we were previously discussing.

//
// The return value should be one or more bytes to return to the
// controller (when evt is I2CRequest), else it is ignored.
type I2CHandler func(evt I2CTargetEvent, r byte) []byte
Copy link
Contributor

@soypat soypat Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate all the work that went into this, awesome stuff!

Just have one question regarding the I2CHandler- it receives one byte, which I presume is the last read. So is this called one time for each byte received? Maybe worth documenting that each byte in the returned byte slice is added to the buffer (which is max 16 length)?

Copy link
Member Author

@kenbell kenbell Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - it's one byte at a time for read. We could aggregate into an array, but then it needs a pre-allocated buffer, so some decision on the max number of permitted bytes from the controller.

To make it device neutral, I could define a constant (MaxI2CHandlerResponseBytes?) that is set to the size of the underlying hardware buffer.

Copy link
Contributor

@soypat soypat Nov 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think I'd avoid not writing the bytes over wire immediately (based on intuition alone though). The constant sounds like a good idea to me 👍

}

type I2C struct {
Bus *rp.I2C0_Type
restartOnNext bool
targetMode bool
Copy link
Member

@deadprogram deadprogram Nov 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this would be better as:

type I2CMode int
const (
   I2CModeController I2CMode = iota
   I2CModeTarget
)

and then this can be defined as:

type I2C struct {
...
   mode I2CMode
...
}

Copy link
Member Author

@kenbell kenbell Nov 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - I prefer this for selecting the mode

@kenbell
Copy link
Member Author

kenbell commented Nov 29, 2022

Thanks for the comments. I'm working on an nrf52 implementation and thinking about re-doing the API on the basis of that.

Something like I2C.WaitForEvent and I2C.Reply, so app logic would look more like this...

var (
	// Max read from Controller is 3 bytes (addr + 16-bit register content)
	readBuf := make([]byte, 3)
	// Max write to Controller is 2 bytes (16-bit register content)
	writeBuf := make([]byte, 2)

	// 256 16-bit registers simulated
	regs := make([]uint16, 256)
	ptr := 0
)



i2c.Listen(addr)

for {
	evt, count, err := i2c.WaitForEvent(readBuf, timeout)
	if err != nil {
		// handle error
	}
	if evt == machine.I2CTargetWrite {
		ptr := buf[0]
		if count > 2 {
			reg[ptr] = uint16(readBuf[1] << 8) | uint16(readBuf[2])
			ptr++
		}
	} else if evt == machine.I2CTargetRead {
		writeBuf[0] = byte(reg[ptr] >> 8)
		writeBuf[1] = byte(reg[ptr] & 0xff)
		ptr++
		i2c.Reply(writeBuf)
	} else {
		panic("invalid i2c event")
	}
}

One problem I see is that although WaitForEvent should be blocking, it should not block other Go routines. Would need some kind of backdoor (linker hack) to allow machine package to call runtime.Gosched.

Alternative would be to have I2C.IsEventWaiting (non-blocking) and let the app logic handle the tight loop + yield.

I think this pattern is better since it avoids app logic from having to honor interrupt handler restrictions and allows implementations in the machine package that don't use interrupts.

@aykevl
Copy link
Member

aykevl commented Dec 12, 2022

I appreciate the thought you put in API design to get it right :)

I'm working on an nrf52 implementation and thinking about re-doing the API on the basis of that.

Thanks a lot! Having more than one implementation helps to avoid tying the API to a particular implementation.
Can you please also make a PR to update the documentation in https://tinygo.org/docs/reference/machine/#i2c? I've often found that writing this documentation surfaces inconsistencies in a proposed API.

One problem I see is that although WaitForEvent should be blocking, it should not block other Go routines. Would need some kind of backdoor (linker hack) to allow machine package to call runtime.Gosched.

There are many peripherals that run into this. Essentially, you want to a semaphore: block inside the goroutine, and unblock from within the interrupt. We have some support for that through channels but it requires some chip specific support (which isn't implemented for the RP2040 yet) and channels are a somewhat heavy data structure for this when all we want is a simple semaphore.

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

Successfully merging this pull request may close these issues.

None yet

4 participants