How does one implement a new interface (or extend an existing HAL interface) for a given peripheral (like say i2c)?

Context: I have an nRF52 micro dev board and I'm trying to communicate with a specific device over i2c; where nRF52 is the master and device is the slave.

My problem: I'm wondering if this particular device (i.e. i2c slave) requires a certain number of retries before it ACKs my start sequences.

My solution: Let me implement my own interface for the on-board i2c (twim) peripheral as the current twim interface implementation in nrf-hal-common does not have an option to add retries.

Where it doesn't make sense:

  1. My board triggers the i2c transmit sequence correctly when I use the default Twim interface that's included in nrf-hal-common. This is verified by my logic analyzer's output.
  2. However, when I use the exact same interface defined in (nrf-hal-common) as a module in my crate (i.e. not import it from nrf-hal-common), the i2c start sequence does not trigger.
  3. After compiling and debugging both versions, I see all i2c register writes prior to the start sequence trigger are in order, in both cases.

Register writes for the 'default Twim interface' included in nrf-hal-common

=> 0x324 <atecc608a::__cortex_m_rt_main+520>:	movs	r0, #96	; 0x60 
   0x326 <atecc608a::__cortex_m_rt_main+522>:	str.w	r0, [r10]         // i2c ADDRESS write
   0x32a <atecc608a::__cortex_m_rt_main+526>:	str.w	r4, [r10, #-68]   // i2c twim PTR register write
   0x32e <atecc608a::__cortex_m_rt_main+530>:	str.w	r9, [r10, #-64]   // i2c twim MAXCOUNT register write
   0x332 <atecc608a::__cortex_m_rt_main+534>:	str.w	r9, [r6, #-252]   // i2c transmit trigger register write
   0x336 <atecc608a::__cortex_m_rt_main+538>:	ldr	r0, [r6, #92]	; 0x5c

Register writes for the 'my version of the Twim interface'

=> 0x1a0 <nRFiic::__cortex_m_rt_main+150>:	movs	r3, #96	; 0x60
   0x1a2 <nRFiic::__cortex_m_rt_main+152>:	str	r3, [r0, #124]	; 0x7c   // i2c ADDRESS write
   0x1a4 <nRFiic::__cortex_m_rt_main+154>:	str	r2, [r0, #56]	; 0x38   // i2c twim PTR register write
   0x1a6 <nRFiic::__cortex_m_rt_main+156>:	movs	r2, #1               
   0x1a8 <nRFiic::__cortex_m_rt_main+158>:	str	r2, [r0, #60]	; 0x3c   // i2c twim MAXCOUNT register write
   0x1aa <nRFiic::__cortex_m_rt_main+160>:	str.w	r2, [r1, #-252]      // i2c transmit trigger register write
   0x1ae <nRFiic::__cortex_m_rt_main+164>:	ldr	r2, [r1, #92]	; 0x5c

As all register writes and reads are 'intrinsics', I don't see a problem with this except that the default implementation does full word (or 4 byte) register writes while my version does not. Semantically speaking, they both achieve the same result. I have verified this with VSCode's cortex-debug extension.

Here's a link to a repo with my version of Twim

Questions:

  1. Any idea why this is happening?
  2. How does one implement an interface for a given peripheral? Am I missing something?
  3. Is there a simpler way to just extend the existing nrf-hal-common interface implementation in my project?

@hannobraun @chocol4te - noticed you're the author. Suggestions or input would be helpful.

Copy and paste from what I said in Matrix, in case it's useful for people who aren't there:

So, there are two ways to change/extend a HAL:

  1. You can fork nrf-hal-common , and use the patch directive to make your dependencies use this.

You can make the changes to the hal that you need, and submit them as a PR to the nrf-hal repo.

For 1, you would do something like this:

git clone https://github.com/nrf-rs/nrf-hal
cd my-project

Then in your Cargo.toml of your project, you would add a line like:

[patch.crates-io]
nrf-hal-common = { version = "0.10", path = "../nrf-hal" }

then you can make changes to nrf-hal locally, and your project will use that version of the HAL instead.

  1. You can write a separate "alternate HAL component". For example, I wanted an automatic DMA driven UARTE driver, instead of the blocking one from the main HAL. For this, I made a separate crate: https://github.com/jamesmunns/home-fleet/blob/main/embedded/fleet-uarte/src/buffer.rs

In this case, you can use the main HAL for all of your other components, (SPI, or Timers, etc.), but replace the nrf-hal-common::Twim with nihals-hal::Twim , which acts differently1 is a better choice if you are improving the main HAL in ways that would be useful to multiple people. 2 is a better choice if your use case is a little esoteric, or like my fleet-uarte crate, it makes specific design decisions that not everyone wants.

Since both HALs just take ownership of a PAC peripheral, you can mix and match by doing something like:

let p = Peripherals::take().unwrap();

// use the main HAL for timers and pins
let timer = nrf52832_hal::Timer::new(p.TIMER0);
let parts = nrf52832_hal::gpio::Parts::new(p.P0);

// use a custom HAL for Twim
let twim = nihals_hal::Twim::new(p.TWIM0, parts.p0_05, parts.p0_06);

I did a stream a while back on how to write a HAL for a Rust crate: https://www.youtube.com/watch?v=pj2Rk-ftcWAIt's a little long, but it goes over the basics of using a PAC peripheral to implement HAL functionality.

I hope that helps :slight_smile:

3 Likes