Page 1 of 2

My long path to configuring the RMT and PCNT to share GPIO

Posted: Sat Mar 10, 2018 4:32 pm
by clarkster
Background

I am working on a controller for an stepper motor. There are two types of steppers, relative and absolute. A relative stepper moves a certain number of steps from its current position. For example, you might command the relative stepper to move 137 CW steps or possibly move 42 steps CCW. With an absolute stepper, you command the stepper to move to an absolute position, say 45 degrees or 276 degrees.

A relative stepper can be programmed using only the RMT peripheral. The RMT can be programmed to send the required number of pulses, with the correct pulse width, to the stepper. The absolute stepper requires both the RMT peripheral as well as a PCNT peripheral. The RMT device is used to issue a continuous pulse stream to the stepper. The PCNT counts the pulses and stops the RMT pulse transmission when the stepper has moved to the target position. A home sensor is also used to clear the PCNT.

Once I understood how the RMT and PCNT peripherals worked, it was relatively easy to program the ESP32 RMT and PCNT devices. However to get to this point, I had to spend a lot of time with the TRM and the examples in the ESP-IDF.

Integrating the two peripherals together was a bit harder. To do that, I had to connect the RMT output to the PCNT input. I also used a GPIO for the direction output. This output was connected to the stepper controller. It also had to be connected to the PCNT control input. In summary there were two signals that were sent to the stepper controller, a step signal (generated by the RMT) and a direction signal (generated by application logic). Both of these signals also needed to be routed to the PCNT.

The Problem

It would have been very easy to configure the PCNT to use different GPIO for it’s inputs. For example my GPIO layout could have been as follows:

Step signal from RMT: GPIO15
Direction signal: GPIO4
PCNT count input: GPIO16
PCNT control input: GPIO5

Had I done this, I could jumper GPIO15 to GPIO16 and GPIO4 to GPIO5 and I would have been in business! But I did not want to do that. GPIO is a precious resource on micro controllers and I did not want to waste them.

What I wanted was to share the GPIO as follows:

Step signal from RMT: GPIO15
PCNT count input: GPIO15
Direction signal: GPIO4
PCNT control input: GPIO4

Initially this seemed pretty simple so I barged ahead and configured my RMT and PCNT as follows:

Code: Select all

rmt_config_t rmt_tx[8];

rmt_tx[channel].gpio_num = 15;
pcnt_config_t pcnt_config = {
	.pulse_gpio_num = 15,	
	.ctrl_gpio_num = 4,
	.channel = PCNT_CHANNEL_0,
	.unit = channel,
};
Unfortunately, this did not work! So I reversed the order of the code, configuring the PCNT first and the RMT second. That did not work either! At this point, I assumed that the PCNT configuration was conflicting with the RMT configuration, and visa versa. As it turns out, I was right. It also turns out that it was easily fixable. If you’ve stayed with me up to now, keep reading…

The Solution

At that point, I decided I would need to understand more about the RMT, PCNT, and GPIO peripherals including how the registers were set during configuration. I also needed to better understand the driver code in rmt.c and pcnt.c.

I soon reached another stumbling block - I could not understand the GPIO chapter (chapter 4) in the TRM. After reading it, I still couldn’t understand how to set GPIO configuration registers. After explaining my problem on this forum, the ESP engineers quickly rewrote Chapter 4. Outstanding work guys! They did a good job and now I was able to understand how the GPIOs functioned at the register level. If you are having problems with this, make sure you update to the latest version of the TRM.

After doing all this homework, I was finally ready to begin analyzing the relevant registers.

Here are the registers I was interested in monitoring:

GPIO_ENABLE_REG - the GPIO output enable register
GPIO_PIN15_REG - configure interrupt and drive for GPIO15
GPIO_PIN4_REG - configure interrupt and drive for GPIO4
GPIO_FUNC43_IN_SEL_REG - primarily assigns a GPIO to the PCNT signal input
GPIO_FUNC45_IN_SEL_REG - primarily assigns a GPIO to the PCNT control input
GPIO_FUNC15_OUT_SEL_REG - primarily assigns a peripheral to control output for GPIO15. In this case, the RMT output is used to control GPIO15.
IO_MUX_GPIO15_REG - enables GPIO input, IO_MUX function, pull up / down enable for GPIO15
IO_MUX_GPIO4_REG - enables GPIO input, IO_MUX function, pull up / down enable for GPIO15

Here is the code I wrote to print out these registers:

Code: Select all

void printGPIORegisters() {
	ESP_LOGI(STEPPER_TAG, "ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN")
	ESP_LOGI(STEPPER_TAG, "0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x\n",
			REG_READ(GPIO_ENABLE_REG), REG_READ(GPIO_PIN15_REG),
			REG_READ(GPIO_FUNC43_IN_SEL_CFG_REG), REG_READ(GPIO_FUNC15_OUT_SEL_CFG_REG),
			REG_READ(IO_MUX_GPIO15_REG), REG_READ(GPIO_PIN4_REG),
			REG_READ(IO_MUX_GPIO4_REG), REG_READ(GPIO_FUNC45_IN_SEL_CFG_REG)
	);
}
So I called this code after configuring the RMT and the PCNT. I ran two tests. The first configured the RMT before the PCNT. The second configured the PCNT before the RMT. I got different results with each test. Here are my results:

Code: Select all

**********  RMT initialized before PCNT  **********
After RMT init
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00038350 0x00000000 0x00000030 0x00000058 0x00002900 0x00000000 0x00002880 0x00000030

After PCNT init
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00030340 0x00000000 0x0000008f 0x00000100 0x00002b00 0x00000000 0x00002a80 0x00000084

**********  PCNT initialized before RMT  **********
After PCNT init
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00030140 0x00000000 0x0000008f 0x00000100 0x00002b00 0x00000000 0x00002a80 0x00000084

After RMT init
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00038350 0x00000000 0x0000008f 0x00000058 0x00002900 0x00000000 0x00002880 0x00000084
As you can see, the GPIO_PIN15_REG and GPIO_PIN4_REG registers were not changed during any of my tests. Also the FUNC_43_IN_SEL_REG and FUNC_45_IN_SEL_REG were not modified by the RMT. These registers were not causing any problems.

The bad news was that the GPIO_ENABLE_REG, GPIO_FUNC15_OUT_SEL_REG, IO_MUX_GPIO15_REG, and IO_MUX_GPIO4_REG were being set to incorrect values. However the results varied depending on which peripheral was configured first. Here is a summary of the changes:

If the RMT was configured first:

The RMT peripheral used the GPIO_ENABLE_REG to set GPIO15 as an output. The PCNT used this register to set it as an input. It had to be set as an output or the RMT peripheral could not send the pulses to the stepper controller.

The RMT peripheral used the GPIO_FUNC15_OUT_SEL_REG to allow the RMT to control GPIO15. The PCNT disconnected GPIO15 from the RMT peripheral.

The solution was to issue specific instructions to set these registers to their correct value. The code to do this is as follows:

Code: Select all

REG_SET_BIT(GPIO_ENABLE_REG, 32784);
REG_SET_FIELD(GPIO_FUNC15_OUT_SEL_CFG_REG, GPIO_FUNC15_OUT_SEL, RMT_SIG_OUT1_IDX);
If the PCNT was configured first:

The PCNT peripheral used the IO_MUX_GPIO15_REG to allow it to read GPIO15 (which is the PCNT signal input). The RMT cleared this bit. This prevented the PCNT from counting RMT’s pulses.

The PCNT peripheral used the IO_MUX_GPIO4_REG to it to allow it to read GPIO4 (which is the PCNT control input). The logic that setup GPIO4 as an output cleared this bit. This prevented the PCNT detecting the direction signal to the stepper controller.

The solution was to issue specific instructions to set these registers to their correct value. The code to do this is as follows:

If the PCNT was configured first:

Code: Select all

PIN_INPUT_ENABLE(GPIO_PIN_MUX_REG[15]);
REG_SET_BIT(IO_MUX_GPIO4_REG, 512);
After issuing these instructions, both the RMT and the PCNT peripherals began working correctly, no matter which was configured first! Outstanding!

Here is a debug printout showing the setting before and after the registers were fixed:

Code: Select all

**********  RMT initialized before PCNT  **********
After RMT init
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00038350 0x00000000 0x00000030 0x00000058 0x00002900 0x00000000 0x00002880 0x00000030

After PCNT init
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00030340 0x00000000 0x0000008f 0x00000100 0x00002b00 0x00000000 0x00002a80 0x00000084

After Fixup
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00038350 0x00000000 0x0000008f 0x00000058 0x00002b00 0x00000000 0x00002a80 0x00000084

**********  PCNT initialized before RMT  **********
After PCNT init
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00030140 0x00000000 0x0000008f 0x00000100 0x00002b00 0x00000000 0x00002a80 0x00000084

After RMT init
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00038350 0x00000000 0x0000008f 0x00000058 0x00002900 0x00000000 0x00002880 0x00000084

After Fixup
ENA_REG    PIN15_REG  FUNC43_IN  FUNC15_OUT IO_MUX_15  PIN4_REG   IOMUX_4    FUNC45_IN
0x00038350 0x00000000 0x0000008f 0x00000058 0x00002b00 0x00000000 0x00002a80 0x00000084
While I was only concerned with the RMT and PCNT peripherals, this method of analyzing the register settings should work for most GPIO configuration problems.

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Mon Aug 27, 2018 9:02 am
by paulhun
I’ve been trying for what seems to be eternity to understand and do registering GPIO’s via IO_MUX, cutting out the need to go via the GPIO mux, sadly I have to admit failure.

Can anyone point me in the direction of an example or indeed provide one whereby I can see how to register 2 input and 2 output GPIO’s via IO_MUX for faster data transmissions. Also how to attach an ISR to an input

Above post is almost there but I’m missing something, maybe obvious but I can’t see the trees for the woods on this one!!

Paul

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Mon Aug 27, 2018 10:24 am
by WiFive
The IO_MUX contains one register per GPIO pad. Each pad can be configured to perform a ”GPIO” function
(when connected to the GPIO Matrix) or a direct function (bypassing the GPIO Matrix). Some high-speed
digital functions (Ethernet, SDIO, SPI, JTAG, UART) can bypass the GPIO Matrix for better high-frequency
digital performance. In this case, the IO_MUX is used to connect these pads directly to the peripheral.)
You can't bypass the gpio matrix unless you are using one of the mentioned peripherals.

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Mon Aug 27, 2018 3:22 pm
by paulhun
Thanks WiFive for the response. I was aware of the paragraph but higher in this post from clarkster he makes reference to registering the GPIO4 (and others) directly to the IO_Mux to achieve the faster throughput, so I'm looking for an expansion on the info provided as it's like a 'taste but not the full meal'

I'm working on a project that monitors steps to a machine driver with the purpose of checking a step has resulted in a stepper motor movement, in other words, if a step is received the motor must move and encoders will confirm movement - else deal with the error...
What I'm getting by connecting GPIO's in the 'convention way' is stuttering when the duty cycle exceeds 600us, I will loose a step perhaps every 15,000 on average - I can't afford to loose any. It's more apparent when I connect the encoders so I've deduced the 'stuttering' relates to interrupt handling/latency. Therefore I want to speed up the throughput.

I hope this makes sense.

Paul

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Tue Aug 28, 2018 3:16 am
by clarkster
Hello Paul

Nothing I did was to improve throughput. I was using the RMT and PCNT send pulses to a stepper motor and to count those pulses. And I wanted the RMT and PCNT to share GPIO. I did not want to have to utilize two GPIO for the RMT and two GPIO for the PCNT, jumping the IO together, when I suspected there was a way to configure the IOMux to let the two peripherals share IO. If I could do that I would only need two GPIO. That's what my original posting was all about.

Unfortunately if you are running into performance limitations because you cannot make decisions as fast as the data is coming in, I see only a few ways to improve your program:

1. Use the fastest possible processor clock speed
2. Optimize your code
3. Don't use an RTOS

Maybe WiFive has some other ideas.

If you cannot do these things, then I can only think of one other possible solution - find another processor that has a much faster clock speed.

I fully realize that most of these ideas might be difficult to implement, but for sure, there is no way (that I know of) to somehow modify the configuration of the IOMux to improve throughput. I'm sorry that my posting somehow caused you to think otherwise.

Clark

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Tue Aug 28, 2018 4:01 am
by Deouss
RMT can also be used to count pulses as well. Seems like you are overcomplicating things

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Fri Aug 31, 2018 10:10 am
by paulhun
Thanks Deouss, clarkster for the heads up.
PCNT works really well for capturing my encoder inputs as do RMT for the step pulses - no stuttering / latency issues.

But, I need to know when a step pulses arrives which direction the stepper motor is moving in, in other words, on a step input I need to know the direction pin state. Not a problem you may say I just need to look at the interrupter 'RMT_CHn_RX_END_IN' within a handler. There is actually where the problems lye.

Can anyone (I'd be most appreciative) point me to an example for an RMT receive ISR handler. I only need the basics as in how to associate RMT receive to the handler. I can see if I do "rmt_driver_install(rmt_rx.channel, 1000, ESP_INTR_FLAG_IRAM)" I can set the intr_alloc_flags but can't understand how to associate it a handler.

Many thanks in advance Paul

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Fri Aug 31, 2018 1:59 pm
by jcsbanks
I read a continuous pulse train so never get a receive interrupt. So I use a 10ms task to process the RMT buffer, which works OK for my frequency.

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Mon Sep 03, 2018 6:18 pm
by paulhun
Well I've sorted how to do this. PCNT works well to capture the encoder inputs and RMT (receive) is almost there.

RMT (receive) will count the input pulses without loss, but, when there has been a period of inactivity for between 5/6 seconds it becomes inactive. I can only getting working again by resetting the chip.

Any ideas of how to keep it active / live?

Many thanks Paul

Re: My long path to configuring the RMT and PCNT to share GPIO

Posted: Tue Sep 04, 2018 4:42 am
by ESP_Angus
Hi Paul,

I have a couple of suggestions, replies inline below:
paulhun wrote:

Code: Select all

REG_SET_BIT(GPIO_ENABLE_REG, 32784);
This is a minor thing, but as a small tip: it's generally best to write values like this as (1<<15) + (1<<4) or similar. Same binary output, but easier to see the meaning when reading the code. :)
paulhun wrote:I’ve been trying for what seems to be eternity to understand and do registering GPIO’s via IO_MUX, cutting out the need to go via the GPIO mux, sadly I have to admit failure.
The IOMUX doesn't allow you to share pin functions in the same way that GPIO Matrix does, and (as WiFive points out) only works for some high speed peripherals as given in the TRM.

These are the peripherals where this is needed, as 25ns additional latency (1 clock cycle at 40MHz APB bus) may matter for these peripherals. For stepper motor driving/sensing, I don't think you'll see any meaningful performance benefit from switching to IOMUX even if you could.
paulhun wrote:Can anyone (I'd be most appreciative) point me to an example for an RMT receive ISR handler. I only need the basics as in how to associate RMT receive to the handler. I can see if I do "rmt_driver_install(rmt_rx.channel, 1000, ESP_INTR_FLAG_IRAM)" I can set the intr_alloc_flags but can't understand how to associate it a handler.
The rmt_driver_install() function installs a default (driver-provided) ISR function which does some high level handling of the driver functions. This driver ISR manages writing and reading from ringbuffers to the RMT peripheral - ie on the RX side, it is responsible for filling the RX ringbuffer allowing you to call xRingbufferReceive() to read items from the RMT.

You can replace this ISR by calling rmt_isr_register() instead, but you'll also need to replicate any other RMT driver functions you need, inside the custom ISR.

This said: If you are aiming to get an interrupt each time the step pin level changes, RMT may not be necessary - you can use a regular GPIO edge interrupt instead (and read the other pin in the GPIO interrupt handler).

An interrupt on every step is much slower than using a DMA-style method like RMT (or I2S or SPI), where the CPU normally only gets an interrupt after every memory block is filled with data. Interrupt latency on ESP32 is quite high (particularly compared to clock speed), which is why a DMA-style approach is generally preferred for performance.

There are a few things I can think of that may work, but to help guide this:
  • What's the maximum step rate you expect?
  • What latency do you need to respond to a step event (ie after the step edge, what is the maximum allowable time before the ESP32 makes some decision based on this?) Is it OK to batch events and process every Xms, or do you need to compute some decision on every step?
RMT (receive) will count the input pulses without loss, but, when there has been a period of inactivity for between 5/6 seconds it becomes inactive. I can only getting working again by resetting the chip.
Can you post some sample code of how you're using the RMT driver?