Motor encoders are useful when you want to calculate how many times your DC motor has revolved since power-on. They generate rectangular pulses according to the angle of the DC motor’s shaft. So, you can see the angle when you read the rectangular pulses properly.
If you use an ESP32 processor, the easiest way to count the rectangular pulses is to use the embedded pulse counter in the processor. I’ve also use this pulse counter (PCNT) to record the number of motor revolutions to capture how many times has the motor been revolving (The main example of it is that I try to estimate the precise position of a robot.)
But when I upgraded the developing environment to ver.4.4-beta, I couldn’t compile the same codes as I’d used for a long time. Reading the I/O address of PCNT directly in your interrupt routine is obsolete in the version 4.4… I managed to adjust my source codes for version 4.4, but I could compile but I couldn’t detect any rectangle pulse from the motor encoder..
When you try to google ESP32 and PCNT on the Internet, in the most cases, they say the following interrupt routine is written and sending a message from it to the main task’s message queue is one of the popular solutions.
typedef struct {
int unit;
uint32_t status;
} pcnt_evt_t;
pcnt_evt_t pcnt_queue;
static void IRAM_ATTR encoder_isr(void *arg)
{
uint32_t intr_status = PCNT.int_st.val;
int pcnt_unit = (int)arg;
portBASE_TYPE wake = pdFALSE;
pcnt_evt_t evt;
for (int i = 0; i < PCNT_UNIT_MAX; i++) {
if (intr_status & (BIT(i))) {
evt.unit = i;
evt.status = PCNT.status_unit[i].val;
PCNT.int_clr.val = BIT(i); // clear the interrupt
xQueueSendFromISR(pcnt_queue, &evt, &wake);
if (wake == pdTRUE) {
portYIELD_FROM_ISR();
}
}
}
}
...
// The main task.
void motor_task(void *param)
{
pcnt_queue = xQueueCreate(10, sizeof(pcnt_evt_t));
if (pcnt_queue == NULL) {
ESP_LOGE(XEBEC_MOTOR_TAG, "couldn't create pcnt_queue!");
esp_restart();
}
while (1) {
res = xQueueReceive(pcnt_queue, &evt, portMAX_DELAY);
switch(evt.status) {
// procedure which deals with the motor revolution.
}
}
}
From version 4.4, the PCNT I/O address can’t be accessed in an interrupt routine. The interrupt routine mentioned above reads PCNT.int_st.val to get the status of the specified pulse counter, but this conduct is obsolete from version 4.4 and using pcnt_get_event_status function is recommended. So, it would be cumbersome for us to get rid of the differences.
PCNT functionality in version 4.4
In the latest Espressif company’s sample code, you don’t have to specify an interrupt routine directly. Instead, you build an ISR service and add some ISR handler functions to it. But the implementation behind the new concept doesn’t change drastically. (In the implemantation, an general interrupt routine is built and an ISR handler function is attached to it. So, an ISR handler function can deal with a minute event.)
But I don’t see why my source codes are wrong, I had no choice looking into the contents of ESP-IDF SDK, hoping that something strange is in the SDK…By the way, I thought if I found a bug or something, the chance of my publishing bug report happened. 😉
Relatively easy to read and maintain…!
The current ESP SDK source codes are easy to read and maintain, in spite of my anticipation. The functions and the hardware-specific configuration are separated beautifully (e.g., component/<function> and soc/<system name> hal/<hardware device>). Getting a pulse counter’s current status and clearing an interrupt is executed in the components/hal/esp32/include/hal/pcnt_ll.h file. (ll stands for Low Level…Huh?) The register names such as int_st and int_clr are equivalent prior to version 4.3, so you can imagine what carries out in this function.
/**
* @brief Get PCNT interrupt status
*
* @param hw Peripheral PCNT hardware instance address.
* @return Interrupt status word
*/
__attribute__((always_inline)) static inline uint32_t pcnt_ll_get_intr_status(pcnt_dev_t *hw)
{
return hw->int_st.val;
}
/**
* @brief Clear PCNT interrupt status
*
* @param hw Peripheral PCNT hardware instance address.
* @param status value to clear interrupt status
*/
__attribute__((always_inline)) static inline void pcnt_ll_clear_intr_status(pcnt_dev_t *hw, uint32_t status)
{
hw->int_clr.val = status;
}
I’ve found to the extent that the PCNT itself is referenced as an externally one. I wonder what the real PCNT variable is defined and looked into the contents of soc directory…
Linker script! That’s you I annoyed!
I’ve found that the start I/O address of the peripheral devices were embedded in the components/soc/esp32/ld/esp32.peripherals.ld linker script file. Judging from it, the embedded PCNT could work correctly if the linker script were set and read correctly. By the way, if you use an ESP32S2 processor, the esp32s2.peripherals.ld linker script file which is different from esp32.peripherals.ld are read so that the slight difference between processors can be adjusted. These files are selected according to the configuration file (sdkconfig), which is dealt with CMakefile.txt file.
I hate to tell you this, but I have enough skill reading / writing CMakefile.txt files. I can add compiled source codes and some conditional phrases, but I can’t write a complicated function in CMakefile.txt. (I will study how to write CMakefile.txt file some day…)
/*
* SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
PROVIDE ( UART0 = 0x3ff40000 );
PROVIDE ( SPI1 = 0x3ff42000 );
PROVIDE ( SPI0 = 0x3ff43000 );
PROVIDE ( GPIO = 0x3ff44000 );
PROVIDE ( SIGMADELTA = 0x3ff44f00 );
PROVIDE ( RTCCNTL = 0x3ff48000 );
PROVIDE ( RTCIO = 0x3ff48400 );
PROVIDE ( SENS = 0x3ff48800 );
PROVIDE ( HINF = 0x3ff4B000 );
PROVIDE ( UHCI1 = 0x3ff4C000 );
PROVIDE ( I2S0 = 0x3ff4F000 );
PROVIDE ( UART1 = 0x3ff50000 );
PROVIDE ( I2C0 = 0x3ff53000 );
PROVIDE ( UHCI0 = 0x3ff54000 );
PROVIDE ( HOST = 0x3ff55000 );
PROVIDE ( RMT = 0x3ff56000 );
PROVIDE ( RMTMEM = 0x3ff56800 );
PROVIDE ( PCNT = 0x3ff57000 ); // It matches the espressif hardware document (ESP32 SoC)
PROVIDE ( SLC = 0x3ff58000 );
PROVIDE ( LEDC = 0x3ff59000 );
PROVIDE ( MCPWM0 = 0x3ff5E000 );
PROVIDE ( TIMERG0 = 0x3ff5F000 );
PROVIDE ( TIMERG1 = 0x3ff60000 );
PROVIDE ( SPI2 = 0x3ff64000 );
PROVIDE ( SPI3 = 0x3ff65000 );
PROVIDE ( SYSCON = 0x3ff66000 );
PROVIDE ( I2C1 = 0x3ff67000 );
PROVIDE ( SDMMC = 0x3ff68000 );
PROVIDE ( EMAC_DMA = 0x3ff69000 );
PROVIDE ( EMAC_EXT = 0x3ff69800 );
PROVIDE ( EMAC_MAC = 0x3ff6A000 );
PROVIDE ( TWAI = 0x3ff6B000 );
PROVIDE ( CAN = 0x3ff6B000 );
PROVIDE ( MCPWM1 = 0x3ff6C000 );
PROVIDE ( I2S1 = 0x3ff6D000 );
PROVIDE ( UART2 = 0x3ff6E000 );
Byproducts after this investigation
From version 4.4 SDK, you don’t have to the PCNT constant directly in your source code. The following code should be enough to track a rotator’s status and the number of revolutions.
pcnt_set_event_value(unit, PCNT_EVT_THRES_1, PCNT_THRESH1_VAL);
pcnt_event_enable(unit, PCNT_EVT_THRES_1);
pcnt_set_event_value(unit, PCNT_EVT_THRES_0, PCNT_THRESH0_VAL);
pcnt_event_enable(unit, PCNT_EVT_THRES_0);
pcnt_event_enable(unit, PCNT_EVT_ZERO);
/* Initialize PCNT's counter */
pcnt_counter_pause(unit);
pcnt_counter_clear(unit);
pcnt_isr_service_install(0);
pcnt_isr_handler_add(unit, handler, 0, (void *)unit);
And the interrupt flags are automatically cleared in the ISR service function which is created pcnt_isr_servier_install function. This is why you can only the following ISR callback function. According to one of the official documents, you don’t have to specify the IRAM_ATTR attribute, but I haven’t verify whether it’s alright to use the xQueueSend function, instead of xQueueSendFromISR.
static void IRAM_ATTR pcnt_example_intr_handler(void *arg)
{
int pcnt_unit = (int)arg;
pcnt_evt_t evt;
evt.unit = pcnt_unit;
/* Save the PCNT event type that caused an interrupt
to pass it to the main program */
pcnt_get_event_status(pcnt_unit, &evt.status);
xQueueSendFromISR(pcnt_evt_queue, &evt, NULL);
}
OK, but how to specify the appropriate linker script?
The easiest way to specify the appropriate linker script is to execute the following command. (The official github document has been silently modified. So, it would be a little difficult for those who are familiar with this development environment)
idf.py set-target <chip-name>
This script modifies the sdkconfig file to fit the target chip (e.g., esp32, esp32s2…), and then
ipf.py build
Lessons I had
- When you use the SDK which frequently updates, you have to pull from the git repository regularly, but you must also carry out their full build.
- Laziness, impatience, and hubris are the three programmers’ virtues. Yes, you’re right. But you must also carry out their full build.
- If some documents are displayed on the github repository or on the official site, read it!
My Failure
I’ve reported the problem above on a github issue. 😀 Well, no one is perfect. I’ve explain the above on the github site and have my issue closed. Haha…