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

raspberrypi: Implement rotaryio, improve StateMachine #4329

Merged
merged 9 commits into from
Mar 9, 2021

Conversation

jepler
Copy link
Member

@jepler jepler commented Mar 4, 2021

Fixes #4226.

Now rotaryio is implemented in this port. For the attached example, either connect a rotary encoder at GP2/3, or connect GP2/3 and GP20/21 to use a synthesized signal.

Click to show Python code
import board
import rotaryio
import digitalio
import time

class QuadratureGenerator:
    def __init__(self, pin_a, pin_b):
        self.a = digitalio.DigitalInOut(pin_a)
        self.b = digitalio.DigitalInOut(pin_b)
        self.a.switch_to_output()
        self.b.switch_to_output()
        self.state = 0

    def __call__(self):
        direction = -1 if (time.monotonic_ns() // 1000000000 % 3) else 1
        state = self.state = (self.state + direction) % 4

        if state == 0:
            self.a.value = 0
            self.b.value = 0
        elif state == 1:
            self.a.value = 1
            self.b.value = 0
        elif state == 2:
            self.a.value = 1
            self.b.value = 1
        else:
            self.a.value = 0
            self.b.value = 1

gen = QuadratureGenerator(board.GP20, board.GP21)


r = rotaryio.IncrementalEncoder(board.GP2, board.GP3)
op = None
while True:
    gen()
    p = r.position
    if op != p:
        print(p)
        op = p

The following functionality was added to StateMachine:

  • The ability to query the depth of the rx fifo
  • The ability to set pull up/down/bus-keep
  • A function to check if pins are sequential

Also a few other things "crept in":

  • A missing deinit call in PDMIn
  • StateMachine.h was not a self-sufficient header, it needed the declaration of mcu_pin_obj_t

With the Python API improvements, it's also possible to implement a form of IncrementalEncoder in pure Python:

Click to show Python code
# This example is adapted in part from micropython:
# https://github.com/micropython/micropython/pull/6894/files

import adafruit_pioasm
import board
import rp2pio
import array

class IncrementalEncoder:
    _state_look_up_table = array.array("b", [
            #Direction = 1
            0, # 00 to 00
           -1, # 00 to 01
           +1, # 00 to 10
           +2, # 00 to 11

           +1, # 01 to 00
            0, # 01 to 01
           +2, # 01 to 10
           -1, # 01 to 11

           -1, # 10 to 00
           +2, # 10 to 01
            0, # 10 to 10
           +1, # 10 to 11

           +2, # 11 to 00
           +1, # 11 to 01
           -1, # 11 to 10
            0, # 11 to 11

            #Direction = 0
            0, # 00 to 00
           -1, # 00 to 01
           +1, # 00 to 10
           -2, # 00 to 11

           +1, # 01 to 00
            0, # 01 to 01
           -2, # 01 to 10
           -1, # 01 to 11

           -1, # 10 to 00
           -2, # 10 to 01
            0, # 10 to 10
           +1, # 10 to 11

           -2, # 11 to 00
           +1, # 11 to 01
           -1, # 11 to 10
            0, # 11 to 11
        ])

    _sm_code = adafruit_pioasm.assemble("""
    again:
        in pins, 2
        mov x, isr
        jmp x!=y, push_data
        mov isr, null
        jmp again
    push_data:
        push
        ; irq wait 0 rel
        mov y, x
    """)
    _sm_init=adafruit_pioasm.assemble("set y 31")

    def __init__(self, pin_a, pin_b):
        if not rp2pio.pins_are_sequential([pin_a, pin_b]):
            raise ValueError("Pins must be sequential")

        self._sm = rp2pio.StateMachine(
            self._sm_code,
            160_000,
            init=self._sm_init,
            first_in_pin=pin_a,
            in_pin_count=2,
            pull_in_pin_up=0b11,
            in_shift_right=False
        )

        self._counter = 0
        self._direction = 0
        self._lut_index = 0 
        self._buffer = bytearray(1)

    def _update_state_machine(self, state):
        lut_index = self._lut_index | (state & 3)
        lut = self._state_look_up_table[lut_index]
        self._counter += lut
        if lut:
            self._direction = 1 if (lut > 0) else 0
        self._lut_index = ((lut_index << 2) & 0b1100) | (self._direction << 4)

    def deinit(self):
        self._sm.deinit()

    @property
    def value(self):
        while self._sm.in_waiting:
            self._sm.readinto(self._buffer)
            self._update_state_machine(self._buffer[0])
        return self._counter



encoder = IncrementalEncoder(board.GP2, board.GP3)

old_value = None
while True:
    value = encoder.value
    if old_value != value:
        print("Encoder:", value)
        old_value = value

jepler added 5 commits March 4, 2021 11:14
I named the property `in_available` because it is similar to pyserial.
However, it indicates the number of words in the fifo, not the number
of bytes.
This can be used where the standard API calls for a list of pins, to check that they satisfy the requirements of the rp2pio state machine, e.g.,
```python
    def __init__(self, pin_a, pin_b):
        if not rp2pio.pins_are_sequential([pin_a, pin_b]):
            raise ValueError("Pins must be sequential")
```
@jepler jepler marked this pull request as ready for review March 4, 2021 18:38
@tannewt tannewt self-requested a review March 4, 2021 18:49
tannewt
tannewt previously approved these changes Mar 4, 2021
Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

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

Thanks! I think you need a make translate. Code looks good otherwise!

@jepler
Copy link
Member Author

jepler commented Mar 4, 2021

@tannewt done -- if you get a chance to check again, that'd be dandy

@jepler jepler requested a review from tannewt March 4, 2021 20:23
Any two consecutive pins can be used for an IncrementalEncoder

Testing performed: Put a synthesized (few hundred counts per second) quadrature signal into GP2/3 and read the encoder out.  Performed filesystem operations at the same time to stress test it.

The reasons for not using common_hal_rp2pio_statemachine_readinto are commented on.
@jepler jepler changed the title StateMachine: Additional functionality needed for rotaryio raspberrypi: Implement rotaryio, improve StateMachine Mar 5, 2021
jepler added a commit to adafruit/Adafruit_CircuitPython_PIOASM that referenced this pull request Mar 5, 2021
This example requires adafruit/circuitpython#4329 to be merged in order to work
Copy link
Collaborator

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

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

I tested with a volume-control program I use daily on a Trinket M0. It works just fine!

Did you diagnose the "dead spot" problem from #3967? I don't see that issue immediately.

@jepler
Copy link
Member Author

jepler commented Mar 5, 2021

No, I used the same quadrature decoder as the samd port.

Copy link
Collaborator

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

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

One style thing. Thanks for doing this!

ports/raspberrypi/mpconfigport.mk Outdated Show resolved Hide resolved
Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

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

This looks good to me! Thank you! I think @DavePutz will want to integrate this into the PulseIn work too.

@@ -71,4 +71,6 @@ bool common_hal_rp2pio_statemachine_get_rxstall(rp2pio_statemachine_obj_t* self)
void common_hal_rp2pio_statemachine_clear_rxfifo(rp2pio_statemachine_obj_t *self);
size_t common_hal_rp2pio_statemachine_get_in_waiting(rp2pio_statemachine_obj_t *self);

void common_hal_rp2pio_statemachine_set_interrupt_handler(rp2pio_statemachine_obj_t *self, void(*handler)(void*), void *arg, int mask);
Copy link
Member

Choose a reason for hiding this comment

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

For next time, I'd put this in ports/raspberrypi/common-hal/rp2pio/StateMachine.h because it's a port-specific API that is unrelated to the Python API. It's a bit weird since this module is itself port-specific.

@tannewt tannewt merged commit 775f1b9 into adafruit:main Mar 9, 2021
This was referenced Mar 11, 2021
@jepler jepler deleted the rp2-rotaryio branch November 3, 2021 21:10
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.

rp2040: implement rotaryio
3 participants