nRF SDK:PPIでSPIMを駆動する

お、3回連続できたぞ！！！

# 前回までのあらすじ(笑)

https://qiita.com/yoshihiro0709/items/b865684c2c9bdbf636af

前回、今さらPPIを理解したということでTimerペリフェラルを入力、GPIOを出力にしてLEDに出力して動かしました。ところでこれが本当にCPUを使っていないのかどうかは分かりません(笑)が、きっと使っていないのでしょう。

:::note
本当にCPUが動いていないのであればWDTでリセットがかかる・・・のかな？
:::

# 本命はペリフェラル
別にPPIを使ってLEDをチカチカさせたい(通称：Lチカ)わけではなく、実際の組み込み機器で使うのであればやはりペリフェラルを駆動することではないでしょうか。それがタイマー経由なのか、ボタン経由なのか、あるいはそれ以外の信号なのかは分かりませんが、とにかく実機で使うとなるとそういう形になるはずです。
## でも待てよ
そもそもPPIで入力トリガーが直接出力に指定されているペリフェラルを動かすって・・・いったいどういうことなの？例えばTWIMにせよSPIMにせよ、送信するのは何かしらのコマンドじゃないですか。CPUを使わずにどうやってそれを設定するのさ？
自分で考えてみましたが全く分からなかった(笑)のでDevzoneを探してみると・・・ありました！

https://devzone.nordicsemi.com/f/nordic-q-a/13523/ppi-to-spim-task-how-to/51620#51620

なんと6年以上前の記事だそうです。すごいや、この人たちは僕よりも6年以上も早くPPIがとても使える機能だということに気が付いていたんだ！

:::note
注：僕は6年前はnRF52を触っていません・・・
:::

## 記事によると
あらかじめNRFX_SPIM_FLAG_HOLD_XFERフラグを付けた状態でSPIMを待機状態にして、その状態でトリガーが入ると送信をする、ということのようです。これはつまり「ひたすら同じコマンドしか送信できない」というこを意味しています。

:::note
もちろんステートマシンを持って遷移するたびに違うコマンドをセットすればきっと違うコマンドも送信できると思いますが、今回はそこまではやっていません。
:::

### さっそく実装してみた
```main.c
void spim_callback_handler(const nrfx_spim_evt_t *p_event, void *p_context)
{
    switch (p_event->type)
    {
        case NRFX_SPIM_EVENT_DONE:
            NRF_LOG_INFO("SPIM done.");
            break;
    }
}

void main()
{
    // Timer
    uint32_t time_ticks;
    nrfx_timer_config_t timer_cfg = NRFX_TIMER_DEFAULT_CONFIG;

    // Configure timer1
    err_code = nrfx_timer_init(&m_timer1, &timer_cfg, timer_event_handler);
    APP_ERROR_CHECK(err_code);
    // Set timer interval
    time_ticks = nrfx_timer_ms_to_ticks(&m_timer1, 1000);
    nrfx_timer_extended_compare(&m_timer1, NRF_TIMER_CC_CHANNEL1, time_ticks, NRF_TIMER_SHORT_COMPARE1_CLEAR_MASK, true);

    // SPIM
    nrfx_spim_config_t spim0_config = NRFX_SPIM_DEFAULT_CONFIG;
    spim0_config.sck_pin   = SER_APP_SPIM0_SCK_PIN;   /* 27 */
    spim0_config.mosi_pin  = SER_APP_SPIM0_MOSI_PIN;  /*  2 */
    spim0_config.miso_pin  = SER_APP_SPIM0_MISO_PIN;  /* 26 */
    spim0_config.ss_pin    = SER_APP_SPIM0_SS_PIN;    /* 1, 13 */
    spim0_config.frequency = SPIM_FREQUENCY_FREQUENCY_M4;
    spim0_config.mode      = NRF_SPIM_MODE_1;
    APP_ERROR_CHECK(nrfx_spim_init(&m_spim0, &spim0_config, spim_callback_handler, NULL));

    uint8_t tx_buffer[8], rx_buffer[8];
    tx_buffer[0] = 0x01;
    tx_buffer[1] = 0x23;
    tx_buffer[2] = 0x45;
    tx_buffer[3] = 0x67;
    tx_buffer[4] = 0x89;
    tx_buffer[5] = 0xAB;
    tx_buffer[6] = 0xCD;
    tx_buffer[7] = 0xEF;

    nrfx_spim_xfer_desc_t spim_buffer = NRFX_SPIM_XFER_TRX(tx_buffer, sizeof(tx_buffer), rx_buffer, sizeof(rx_buffer));
    APP_ERROR_CHECK(nrfx_spim_xfer(spim, &spim_buffer, NRFX_SPIM_FLAG_HOLD_XFER));

    // PPI
    err_code = nrf_drv_ppi_init();
    APP_ERROR_CHECK(err_code);

    nrf_ppi_channel_t channel;
    err_code = nrfx_ppi_channel_alloc(&channel);
    APP_ERROR_CHECK(err_code);

    err_code = nrfx_ppi_channel_assign(channel,
        nrfx_timer_compare_event_address_get(&m_timer1, NRF_TIMER_CC_CHANNEL1),
        nrfx_spim_start_task_get(&m_spim0));
    APP_ERROR_CHECK(err_code);

    /* Enable PPI channel1. */
    err_code = nrfx_ppi_channel_enable(channel);
    APP_ERROR_CHECK(err_code);

    nrfx_timer_enable(&m_timer1);
}
```
PPIに関わる部分のみの抜粋ですがこんな感じでしょうか。
### さっそく動かしてみた
ビルドして動かしてみると・・・お、ちゃんとSPIM Doneと表示される！
ということはちゃんとコールバックが呼び出されている、つまりSPIMが動いているということか。な～んだ、簡単じゃん。
念のため信号も見てみるか・・・あれ、なんか変だぞ？CSが動いていない？
## 先ほどの記事をもう一度読んでみると
>There is however one problem here, the slave select pin is not toggled by itself, only the SCK, MISO and MOSI will be transferred. To include the slave select pin you might configure the compare0 event to trigger two tasks by setup two PPI channels:

ただし、スレーブセレクトピンはそれ自体ではトグルせず、SCK、MISO、MOSIだけが転送されることになるという問題があります。スレーブセレクトピンを含めるには、2つのPPIチャネルをセットアップして、compare0イベントが2つのタスクをトリガするように構成する必要があります。

ΩΩΩ ＜ な、なんだってー！

おそらくですが、SPIMにパラレルにデバイスが接続されている場合を考慮してCSはあえて非同期で動かすようにしたのだと思われます。いや、そんなめんどくさい配慮要らないからもっと簡単に動かせるようにしてよ(笑)
### 追加実装
```main.c
void spim_callback_handler(const nrfx_spim_evt_t *p_event, void *p_context)
{
    switch (p_event->type)
    {
        case NRFX_SPIM_EVENT_DONE:
            NRF_LOG_INFO("SPIM done.");
            nrfx_gpiote_set_task_trigger(SER_APP_SPIM0_SS_PIN);
            break;
    }
}

void main()
{
    // Timer
    uint32_t time_ticks;
    nrfx_timer_config_t timer_cfg = NRFX_TIMER_DEFAULT_CONFIG;

    // Configure timer1
    err_code = nrfx_timer_init(&m_timer1, &timer_cfg, timer_event_handler);
    APP_ERROR_CHECK(err_code);
    // Set timer interval
    time_ticks = nrfx_timer_ms_to_ticks(&m_timer1, 1000);
    nrfx_timer_extended_compare(&m_timer1, NRF_TIMER_CC_CHANNEL1, time_ticks, NRF_TIMER_SHORT_COMPARE1_CLEAR_MASK, true);

    // SPIM
    nrfx_spim_config_t spim0_config = NRFX_SPIM_DEFAULT_CONFIG;
    spim0_config.sck_pin   = SER_APP_SPIM0_SCK_PIN;   /* 27 */
    spim0_config.mosi_pin  = SER_APP_SPIM0_MOSI_PIN;  /*  2 */
    spim0_config.miso_pin  = SER_APP_SPIM0_MISO_PIN;  /* 26 */
    spim0_config.ss_pin    = SER_APP_SPIM0_SS_PIN;    /* 1, 13 */
    spim0_config.frequency = SPIM_FREQUENCY_FREQUENCY_M4;
    spim0_config.mode      = NRF_SPIM_MODE_1;
    APP_ERROR_CHECK(nrfx_spim_init(&m_spim0, &spim0_config, spim_callback_handler, NULL));

    uint8_t tx_buffer[8], rx_buffer[8];
    tx_buffer[0] = 0x01;
    tx_buffer[1] = 0x23;
    tx_buffer[2] = 0x45;
    tx_buffer[3] = 0x67;
    tx_buffer[4] = 0x89;
    tx_buffer[5] = 0xAB;
    tx_buffer[6] = 0xCD;
    tx_buffer[7] = 0xEF;

    nrfx_spim_xfer_desc_t spim_buffer = NRFX_SPIM_XFER_TRX(tx_buffer, sizeof(tx_buffer), rx_buffer, sizeof(rx_buffer));
    APP_ERROR_CHECK(nrfx_spim_xfer(spim, &spim_buffer, NRFX_SPIM_FLAG_HOLD_XFER));

    // GPIOTE Handler
    if (!nrfx_gpiote_is_init())
    {
        err_code = nrfx_gpiote_init();
        APP_ERROR_CHECK(err_code);
    }

    // Output
    nrfx_gpiote_out_config_t const out_config = {
        .action = NRF_GPIOTE_POLARITY_TOGGLE,
        .init_state = 1,
        .task_pin = true,
    };

    err_code = nrfx_gpiote_out_init(SER_APP_SPIM0_SS_PIN, &out_config);
    APP_ERROR_CHECK(err_code);

    nrfx_gpiote_out_task_enable(SER_APP_SPIM0_SS_PIN);

    // PPI
    err_code = nrf_drv_ppi_init();
    APP_ERROR_CHECK(err_code);

    nrf_ppi_channel_t channel1, channel2;
    err_code = nrfx_ppi_channel_alloc(&channel1);
    APP_ERROR_CHECK(err_code);
    err_code = nrfx_ppi_channel_alloc(&channel2);
    APP_ERROR_CHECK(err_code);

    err_code = nrfx_ppi_channel_assign(channel1,
        nrfx_timer_compare_event_address_get(&m_timer1, NRF_TIMER_CC_CHANNEL1),
        nrfx_spim_start_task_get(&m_spim0));
    APP_ERROR_CHECK(err_code);

    err_code = nrfx_ppi_channel_assign(channel2,
        nrfx_timer_compare_event_address_get(&m_timer1, NRF_TIMER_CC_CHANNEL1),
        nrfx_gpiote_out_task_addr_get(SER_APP_SPIM0_SS_PIN));
    APP_ERROR_CHECK(err_code);

    /* Enable PPI channel1. */
    err_code = nrfx_ppi_channel_enable(channel1);
    APP_ERROR_CHECK(err_code);
    err_code = nrfx_ppi_channel_enable(channel2);
    APP_ERROR_CHECK(err_code);

    nrfx_timer_enable(&m_timer1);
}
```
一つのタイマーでGPIOTEとSPIMを両方駆動するような形にしてみました。これでちゃんと動くはず、ということで動かしてみたのがこちらです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340602/67698791-cca3-c58b-2993-b7b531019656.png)
ちゃんとCSがSCLKに合わせて動いています。後ろが若干長いですが、SPIMのコールバックで制御しているのでどうしてもこれくらいの時間はかかってしまうのかも知れません。
PPIでGPIOTEを制御しているのはあくまでH→Lだけなので、SPIMの駆動が終わったら戻してあげる必要があります。それがコールバックでのGPIOTEのセットになります。
# 
結構調べるのに時間がかかりました・・・。