mruby/cで安く省電力しようと思ったらやっぱりPIC32だった

# はじめに
今までいくつかマイコンを触ってみて
PIC32MXというマイコンはドキュメントが見やすく32bitマイコンには珍しくDIP品もあるので回路を組みやすい。
脱マイコンボードをしたい方にはATmega328Pと同じくらいお勧めしたい素晴らしいマイコンだと思っています。

ただ個人的に電池駆動で長時間動かしてみたいという気持ちが。
PIC32MXというものは消費電力が高いのです。
そこで今回の記事に至ります。

# マイコン選定
マイコンを選ぶ際に考慮した項目
- mruby/cを搭載できそうなROM,RAMを持っている
- RAMを保持したままスリープできるモードがある
- 価格が安い（単品注文で1000円行かないくらい
- 現在入手できるもの

いろいろあって候補がこの2つ
- STM32L4x1
- PIC32CM Lx

それで私は標題にある通りPIC32を選ぶわけなのですが、大した理由があるわけでもなく。
価格(安い)と好奇心(なんか最近登場したみたい)から選んでます。


 # PIC32CM Lxとは
PIC32の型番にはMIPSコアのPIC32MからはじまるものとArmコアのPIC32Cからはじまるものがあります
今回はPIC32Cのマイコンを触るわけなのですがPIC32CM LxはCortex-M23で作られているらしいです。
はじめて聞いた

 https://www.arm.com/ja/products/silicon-ip-cpu/cortex-m/cortex-m23

どうやらmicrochipの中ではSAML10,11にも使われているらしい。
セキュリティと省電力化を焦点としているみたい。
ドキュメントでは暗号化に関しての記載などが登場します。

このマイコンの情報量は少ないですがARMなので同じARMコアのSAMD21あたりの情報が参考になります。


# 開発環境
IDE：MPLABXIDE
コンパイラ：MPLAB XC32 Compiler
書き込み機：Pickit4 (またはPickit5, SNAP )

# 部品・回路図
今回使用するマイコンは[PIC32CM5164LE00048](https://www.microchip.com/en-us/product/PIC32CM5164LE00048)です。
![Schematic_PIC32CMlx_2023-12-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195542/3f1cee29-40cf-1027-d550-6857f25cc232.png)

https://oshwlab.com/yoshihiroogura/PIC32CMlx

# Lチカ(C言語

## 動作クロックの設定
本当は48MHzにしようと思ったけど、説明を書いていたら思っていたより量が多かったので単純に内部クロック16MHzで動作させます。
ドキュメントの7.Device Start-upに起動直後の動作について書いてあります。
初期では内部16MHzを4MHzにしてMCLKへ与えているようです。

今回16MHz動作なのでパフォーマンスレベルの変更が必要です。
PL0とPL2があり、PL0では最大12MHzで動作するようです。
なので最大48MHz動作のPL2へ変更します。
```c
PM_REGS->PM_PLCFG = (uint8_t)PM_PLCFG_PLSEL_PL2;
while((PM_REGS->PM_INTFLAG & PM_INTFLAG_PLRDY_Msk) == 0U){}
```

主たる動作clockを指定します。
ドキュメントの9.1 Clocks Distribution
経路を短くまとめるとこんな感じ
```mermaid
graph LR;
OSC16M-->GCLK0;
GCLK0-->MCLK;
```
このあたりはharmonyで一度生成してみるとわかりやすいです。

内部16MHzオシレータを有効化します。
あわせてFSELを3設定します(16MHz)

:::note alert
FSELを0としていた場合4MHzで動作します(初期状態と同じ
:::
```c
OSCCTRL_REGS->OSCCTRL_OSC16MCTRL = OSCCTRL_OSC16MCTRL_FSEL(0x3U) | OSCCTRL_OSC16MCTRL_ENABLE_Msk;
```

GCLK0をOSC16Mで動作するように設定してあげます。(初期状態と同じなので不要かも
`GCLK_GENCTRL_SRC = 5`

```c
GCLK_REGS->GCLK_GENCTRL[0] = GCLK_GENCTRL_DIV(1U) | GCLK_GENCTRL_SRC(5U) | GCLK_GENCTRL_GENEN_Msk;
while((GCLK_REGS->GCLK_SYNCBUSY & GCLK_SYNCBUSY_GENCTRL0_Msk) == GCLK_SYNCBUSY_GENCTRL0_Msk){}
```

MCLK周りのレジスタは初期値のままで問題ないはずです。

## LEDピン設定

ドキュメントの31.6.3.1 Pin Configurations Summaryを見るとDIRとINENの設定だけすればよさそうです
DIRを1に設定しないとOUTには到達しません
今回はLED出力のみなのでINENを0にしておきます。
INENはPINCFGの2bit目にあります。

```c
// 初期化
PORT_REGS->GROUP[0].PORT_OUTCLR = 0x000F;
PORT_REGS->GROUP[0].PORT_DIRSET = 0x000F;
PORT_REGS->GROUP[0].PORT_PINCFG[0] = 0;
PORT_REGS->GROUP[0].PORT_PINCFG[1] = 0;
PORT_REGS->GROUP[0].PORT_PINCFG[2] = 0;
PORT_REGS->GROUP[0].PORT_PINCFG[3] = 0;
```

## delay
今回はdelayが無いので自分で作る必要があります。

:::note info
システムタイマ(SysTick)が存在する
今後使うかもしれないメモ
:::

今回はmruby/cで動作が目的なのでここは適当に実装する

```c
static void delay_ticks( uint32_t ticks )
{
  for(;ticks>0;ticks--){
    Nop();
  }
}

void __delay_ms( uint32_t ms )
{
  delay_ticks( ms * 16 * 100 );
}
```

## プログラム全体
PORTの出力設定部分はPORT_OUTに直接書き込めない事に注意してください。
PORT_OUTSETで値を設定してPORT_OUTCLRでクリアします。

```c
#include <xc.h>

static void delay_ticks( uint32_t ticks )
{
  for(;ticks>0;ticks--){
    Nop();
  }
}

void __delay_ms( uint32_t ms )
{
  delay_ticks( ms * 16 * 100 );
}

void onboard_led( int num, int on_off )
{
  int led = 1 << (num - 1);
  if(!!on_off){
    PORT_REGS->GROUP[0].PORT_OUTSET = led;
  }
  else
  {
    PORT_REGS->GROUP[0].PORT_OUTCLR = led;
  }
}

void main(void) {
  PM_REGS->PM_PLCFG = (uint8_t)PM_PLCFG_PLSEL_PL2;
  while((PM_REGS->PM_INTFLAG & PM_INTFLAG_PLRDY_Msk) == 0U){}

  OSCCTRL_REGS->OSCCTRL_OSC16MCTRL = OSCCTRL_OSC16MCTRL_FSEL(0x3U) | OSCCTRL_OSC16MCTRL_ENABLE_Msk;

  GCLK_REGS->GCLK_GENCTRL[0] = GCLK_GENCTRL_DIV(1U) | GCLK_GENCTRL_SRC(5U) | GCLK_GENCTRL_GENEN_Msk;
  while((GCLK_REGS->GCLK_SYNCBUSY & GCLK_SYNCBUSY_GENCTRL0_Msk) == GCLK_SYNCBUSY_GENCTRL0_Msk){}
  
  PORT_REGS->GROUP[0].PORT_OUTCLR = 0x000F;
  PORT_REGS->GROUP[0].PORT_DIRSET = 0x000F;
  PORT_REGS->GROUP[0].PORT_PINCFG[0] = 0;
  PORT_REGS->GROUP[0].PORT_PINCFG[1] = 0;
  PORT_REGS->GROUP[0].PORT_PINCFG[2] = 0;
  PORT_REGS->GROUP[0].PORT_PINCFG[3] = 0;
  
  while(1){
    onboard_led( 1, 1 );
    __delay_ms(1000);
    onboard_led( 1, 0 );
    __delay_ms(1000);
  }
  return;
}
```

# mruby/cに対応させる
:::note info
ソースコードの記載は無いですがメインクロックは48MHzへ変更しています
:::
## タイマー
省電力化を目的としているので内部の32kHzを使用してsleepで使用する1msタイマーを実装します。
必要条件：
- 1ms間隔で割り込み発生
- スリープ時でも動作する

```mermaid
graph LR;
    OSC32K-->GCLK2;
    GCLK2-->TC0;
```

まずは内部32kHzを有効化します
```c
OSC32KCTRL_REGS->OSC32KCTRL_RTCCTRL = OSC32KCTRL_RTCCTRL_RTCSEL(0U);
```

GCLK2に32KHzを割り当てます
```c
GCLK_REGS->GCLK_GENCTRL[2] = GCLK_GENCTRL_DIV(1U) | GCLK_GENCTRL_SRC(3U) | GCLK_GENCTRL_RUNSTDBY_Msk | GCLK_GENCTRL_GENEN_Msk;
while((GCLK_REGS->GCLK_SYNCBUSY & GCLK_SYNCBUSY_GENCTRL2_Msk) == GCLK_SYNCBUSY_GENCTRL2_Msk){}
```

GCLK2をTC0,1へ割り当てます
ドキュメントのTable 17-8. PCHCTRLm MappingにPCHCTRLに関する対応表があります。
23がTC0,1です。
```c
GCLK_REGS->GCLK_PCHCTRL[23] = GCLK_PCHCTRL_GEN(0x2)  | GCLK_PCHCTRL_CHEN_Msk;
while ((GCLK_REGS->GCLK_PCHCTRL[23] & GCLK_PCHCTRL_CHEN_Msk) != GCLK_PCHCTRL_CHEN_Msk){}
```

TC0の初期設定をします。(ほとんどharmonyから持ってきた。

スリープ時でも動作するようにCTRLAのRUNSTDBYは有効化しておきます。
32KHzなので1ms割り込みのためにCCには32を指定します。
```c
void TC0_TimerInitialize( void )
{
  /* Reset TC */
  TC0_REGS->COUNT16.TC_CTRLA = TC_CTRLA_SWRST_Msk;
  while((TC0_REGS->COUNT16.TC_SYNCBUSY & TC_SYNCBUSY_SWRST_Msk) == TC_SYNCBUSY_SWRST_Msk){}
  /* Configure counter mode & prescaler */
  TC0_REGS->COUNT16.TC_CTRLA = TC_CTRLA_MODE_COUNT16 | TC_CTRLA_PRESCALER_DIV1 | TC_CTRLA_PRESCSYNC_PRESC | TC_CTRLA_RUNSTDBY_Msk ;
  /* Configure in Match Frequency Mode */
  TC0_REGS->COUNT16.TC_WAVE = (uint8_t)TC_WAVE_WAVEGEN_MPWM;
  /* Configure timer period */
  TC0_REGS->COUNT16.TC_CC[0U] = 32U;
  /* Clear all interrupt flags */
  TC0_REGS->COUNT16.TC_INTFLAG = (uint8_t)TC_INTFLAG_Msk;
  /* Enable interrupt*/
  TC0_REGS->COUNT16.TC_INTENSET = (uint8_t)(TC_INTENSET_OVF_Msk);
  while((TC0_REGS->COUNT16.TC_SYNCBUSY) != 0U){}
}
```

```c
void TC0_Handler(void){
  mrbc_tick();
  TC0_REGS->COUNT16.TC_INTFLAG = (uint8_t)TC_INTFLAG_Msk;
}
```


NVICにてTC0からの割り込みを許可します。

```c
__enable_irq();
NVIC_SetPriority(TC0_IRQn, 1);
NVIC_EnableIRQ(TC0_IRQn);
```

## 省電力プログラム
ドキュメント22.Power Manager (PM)
このマイコンのスリープモードにはIDLE,STANDBY,OFFの3種類あります。
今回はSRAMを保持したままスリープできるSTANDBYモードに挑戦してみます。

STANDBYモード時の電源やRAMの扱いについて定義します。
```c
PM_REGS->PM_STDBYCFG = (uint16_t)(PM_STDBYCFG_BBIASHS_Msk| PM_STDBYCFG_VREGSMOD(0UL)| PM_STDBYCFG_BBIASTR_Msk);
```

## halファイルを書く
```hal.h
#include <xc.h>
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# define hal_init()        ((void)0)
void hal_enable_irq(void);
void hal_disable_irq(void);
void hal_idle_cpu(void);
```

今後追加要素があるかもしれないのでいくつかCの方へ書いています。
SLEEPMODEにSTANDBYを指定して、WFIへ入った際にSTANDBYモードへ移行するようになっています。

```hal.c
#include "hal.h"
void hal_enable_irq(void)
{
  NVIC_EnableIRQ(TC0_IRQn);
}

void hal_disable_irq(void)
{
  NVIC_DisableIRQ(TC0_IRQn);
}

void hal_idle_cpu(void)
{
  PM_REGS->PM_SLEEPCFG = (uint8_t)PM_SLEEPCFG_SLEEPMODE_STANDBY_Val;
  while ((PM_REGS->PM_SLEEPCFG & PM_SLEEPCFG_SLEEPMODE_STANDBY_Val) == 0U){}
  __WFI();
}
```

## LED用メソッドの追加

```c
void c_leds_write(mrb_vm *vm, mrb_value *v, int argc) {
  int led = GET_INT_ARG(1);

  onboard_led( 1, led & 0x01 );
  onboard_led( 2, led & 0x02 );
  onboard_led( 3, led & 0x04 );
  onboard_led( 4, led & 0x08 );
}
```

## rubyプログラム
```test.rb
while true
  leds_write(1)
  sleep(5)
  leds_write(0)
  sleep(5)
end
```
mrbcを使用してコンパイルします。
私が使用したのはmruby3.1の時のコンパイラです。
`mrbc.exe -Bcode test.rb`

codeの中身はmain.cに入れておきます。
```test.c
#include <stdint.h>
#ifdef __cplusplus
extern
#endif
const uint8_t code[] = {
0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x00,0x71,0x4d,0x41,0x54,0x5a,
0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0x55,0x30,0x33,0x30,0x30,
0x00,0x00,0x00,0x49,0x00,0x01,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,
0x07,0x02,0x2d,0x01,0x00,0x01,0x0b,0x02,0x2d,0x01,0x01,0x01,0x06,0x02,0x2d,0x01,
0x00,0x01,0x0b,0x02,0x2d,0x01,0x01,0x01,0x25,0xff,0xe5,0x11,0x01,0x38,0x01,0x69,
0x00,0x00,0x00,0x02,0x00,0x0a,0x6c,0x65,0x64,0x73,0x5f,0x77,0x72,0x69,0x74,0x65,
0x00,0x00,0x05,0x73,0x6c,0x65,0x65,0x70,0x00,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,
0x08,
};
```

## mruby/c
TC0をmruby/cの動作前にスタートしておきます。
あとはmruby/cのいつものやつ
```main.c
#include "mrubyc.h"
#include <xc.h>

#define MEMORY_SIZE (1024*30)
uint8_t memory_pool[MEMORY_SIZE];

// ~~~~~~~~~~~~~~~~~~~~~~~~

int main ( void )
  // ~~~~~~~~~~~~~~~~~~~~~~~~
  TC0_TimerInitialize();
  TC0_REGS->COUNT16.TC_CTRLA |= TC_CTRLA_ENABLE_Msk;
  while((TC0_REGS->COUNT16.TC_SYNCBUSY & TC_SYNCBUSY_ENABLE_Msk) == TC_SYNCBUSY_ENABLE_Msk){}

  // mruby/cのいつものやつ
  mrbc_init(memory_pool, MEMORY_SIZE);
  // LED用メソッド追加
  mrbc_define_method(0, mrbc_class_object, "leds_write", c_leds_write);
  mrbc_create_task(code, 0);
  mrbc_run();
  
  return ( EXIT_FAILURE );
}
```



# 省電力動作テスト
テスターにて電流値を計測した内容です。
```mermaid
graph LR;
3.3V電源-->テスター;
テスター-->マイコン(VCC);
```
今回mruby/cを使ってSleep時にSTANDBYモードへ切り替える事により0.1mAを実現できました。
LEDの出力を切り替えるときなど一瞬48MHzで動作しているはずです。
実際にSleepを外した状態のコードで実行すると6.9mAで動作しました。
```ruby
while true
  leds_write(0)
end
```

以下表にまとめます。

| 言語 | 動作モード | メインクロック | 電流 |
|:-----------|------------:|:------------:|------------:|
| C言語| active        | 16MHz         | 2.4mA |
| mruby/c     | active      | 48MHz       | 6.9mA |
| mruby/c       | active + STANDBY        | 48MHz | 0.1mA |


# さいごに
PIC32CMは基本的にharmonyを使用する事が前提のような気がするのでharmony使った方がいいです。
今はコードを公開していないけれど公開するかもしれません。


mruby/cをマイコンに対応させて思うのは、
やはりいろいろなマイコンでほぼ共通して書けるArduinoってすごい　という事。
あのように多種多様なマイコンでmruby,mruby/cを使いたいですね。