目次
開発環境のバージョンアップをしたらコンパイル通らなくなりましたorパルスがカウントできなくなりました…
DCモータなどについているエンコーダ(軸の回転に合わせて矩形パルスが出力されるやつですね)の読み取りをESP32で行うには、一番手っ取り早いのは内蔵のパルスカウンタを利用することです。私もこれまで、ESP32でモータのエンコーダなどを読み込むときはこの内蔵パルスカウンタを利用してモータの回転数を記録していたのですが、ESP-IDF開発環境をVer4.4にしたところ、突如コンパイルが通らなくなりました。何とかVer4.4に合わせる形に書き換えようとしたのですが、今度はコンパイルは通るけれど全くパルス検出ができなくなってしまいました…。
ネットでESP32, PCNTなどの検索ワードで検索していただくと、大抵は次に示すような割り込みルーチンが書かれており、メインタスクのメッセージキューにモータ位置メッセージを出すとうまく制御ができますよ、という物が大半を占めている現状なのですが。
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); /* 割り込みをクリアする */
xQueueSendFromISR(pcnt_queue, &evt, &wake);
if (wake == pdTRUE) {
portYIELD_FROM_ISR();
}
}
}
}
...
メインタスク側
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) {
・・・回転したことに依る処理
}
}
}
これが、Ver4.4よりPCNTレジスタが直接見られなくなったため、コンパイルエラーとなるようになりました・・・。パルスカウンタのステータスを見るために上記の割り込みルーチンでは先頭でPCNT.int_st.valを読み込んでいますが、これも非推奨な行為となり、 pcnt_get_event_status関数を使えよとなっていたりして整合性がとれなくなりました。
ワークアラウンドがうまく動作せず、本質を追う羽目に…
espressif社のサンプルコードでは、割り込みルーチンを直接指定する方法ではなくISRサービスとして登録する方法のソースコードに変更になりましたが、やっていることの本質はそんなに変わりません(結局内部で割り込みルーチンを作ってコールバック化しているだけなので)。なので、何が悪いのか良く分からなくなってしまいました。しょうがないので、それなりちゃんとSDKの中身を追いかけていく羽目になりました。何か変なことがあるのではという一縷の望みを託して…。バグだったら、pull requestを出すチャンスにもなるしーとかよこしまな思いもちょっとあった気はしますが。
あれ、意外ときれいにメンテされているぞ…?
最終的には、ハードウェア依存の下位コードにまできれいに降りて行きまして(まぁまぁきれいな方。ちゃんと機能とハードウェアが分かれている設計です)。下のcomponents/hal/esp32/include/hal/pcnt_ll.hで、パルスカウンタの状態と割込みのクリアが行われています。int_st.valとかint_clr.valとかのレジスタ名称はそのままなので、見当が付けやすいです。
/**
* @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;
}
PCNT自体はcomponents/soc/esp32/include/soc/pcnt_struct.hにて外部参照定義されている(どこかに実体があり、それを参照させていただきますよ、という定義)ところまで突き止めました。このPCNTがどこで定義されているのかなと思ってsoc周りを漁ってみると…
リンカスクリプト、君だったのか。私を苦しめてきたものは
components/soc/esp32/ld/esp32.peripherals.ldファイルにて各ペリフェラルの先頭I/Oレジスタ値が埋め込まれておりました…。 ということは、このリンカスクリプトが正しく読み込まれていればソフトウェアとしてはちゃんと動作するはずなんですよね…。ちなみにESP32S2だったら、esp32s2.peripherals.ldファイルが別に存在しており、PCNTのレジスタ番地は異なっておりました。リンカスクリプトの判定はCMakefile.txtファイルで行われているのは発見していますが、私CMakefile.txtに関してはファイルの追加と条件分岐くらいしか書けないペーペーなので(笑)、深追いはやめました。
/*
* 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 ); // hardware documentとも整合性取れているので合っていそうです。
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 );
内部動作から見たtips
ちなみに、ここまでの調査で分かった副産物としては、直接ここで定義されているPCNTを割り込みルーチンから参照するようなコードは書かなくて良く、
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);
なんてしておいて、カウンタがEVENT_1, EVENT_2, ZEROになったときにそれ用のhandlerを呼び出せば、内部的に割り込み発生後の、割込みハンドラ内ですべき割り込みフラグのクリアはpcnt_isr_servier_installが作った割込みハンドラにて内部的にやってくれます。なので、Ver4.4になったら今まで行っていた以下のような割り込みクリアとステータス取得をレジスタに直書きする必要はなくなるみたいです。いままではこのようなサンプルを書いていたかと思いますが、
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); /* 割り込みをクリアする */
xQueueSendFromISR(pcnt_queue, &evt, &wake);
if (wake == pdTRUE) {
portYIELD_FROM_ISR();
}
}
}
素直に次のようなコールバック関数を書けばよくなるみたいですね。一応IRAM_ATTR属性付けていますが、付けなくても良いとのドキュメントもありますので、もうちょっとすっきりとはするみたいです(その場合、xQueueSend関数はどうなるんだろう?というのはありますが…FromISR付けるの付けないの?)。
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);
}
リンカスクリプトに含まれるアドレス指定が整合しなくなってる…?
のかな?と思ってふとReadme.mdを読み直したところ、ビルド方法のこの部分がいきなり変わっている?!
idf.py set-target <chip-name>
これをやってsdkconfig設定ファイルを書き換えてから、再度
idf.py menuconfig
を実行せよと書かれておりました。さらっと記述を変えるなーと思いつつも従順な小市民である私は、手持ちがESP32-WROOM2でしたので、esp32をchip-nameに指定して、とりあえず手順を守って再ビルドかけてみています…。
結果
最初やっぱりうまくいかないなーと(モータ回転しっぱなしになってwatchdog発生)と思っていたのですが、割り込みルーチン登録(pcnt_isr_register)とISRサービス登録(pcnt_isr_handler_add )とが混ざって使われていたためと分かりまして。ISRサービス登録に統一したところ無事にエンコーダのステータスを取得することができました!
はー、デグレードしたのを直すのは根気が要ります。
教訓
- アップグレードの激しいSDKは、時折git pullするのはお約束だが、ちゃんとクリーンビルドもしようね。
- 短気・傲慢・無精はプログラマ三大美徳だとはいえ、ちゃんとクリーンビルドはしようね。
- ドキュメント整備してくれているのなら、それなりにはちゃんと読もうね(読む前に見切り発車良くない)
残った課題
えー、恥ずかしながらespressifのesp-idfリポジトリ向けにissueを上げちゃったのをどう落とし前付けるかですね。ごめん私のミスでしたってこれから書きます…。結構はずい。
ぼーっとした頭ではありましたがちゃんと経緯を書いてComment & Closeしておきましたわ。ケジメですから。(as of 2021/09/07 09:15)