7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

mrubyファミリーAdvent Calendar 2023

Day 8

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

Last updated at Posted at 2023-12-07

はじめに

今までいくつかマイコンを触ってみて
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で作られているらしいです。
はじめて聞いた

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

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

開発環境

IDE:MPLABXIDE
コンパイラ:MPLAB XC32 Compiler
書き込み機:Pickit4 (またはPickit5, SNAP )

部品・回路図

今回使用するマイコンはPIC32CM5164LE00048です。
Schematic_PIC32CMlx_2023-12-05.png

Lチカ(C言語

動作クロックの設定

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

今回16MHz動作なのでパフォーマンスレベルの変更が必要です。
PL0とPL2があり、PL0では最大12MHzで動作するようです。
なので最大48MHz動作のPL2へ変更します。

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
経路を短くまとめるとこんな感じ

このあたりはharmonyで一度生成してみるとわかりやすいです。

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

FSELを0としていた場合4MHzで動作します(初期状態と同じ

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

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

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目にあります。

// 初期化
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が無いので自分で作る必要があります。

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

今回はmruby/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でクリアします。

#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に対応させる

ソースコードの記載は無いですがメインクロックは48MHzへ変更しています

タイマー

省電力化を目的としているので内部の32kHzを使用してsleepで使用する1msタイマーを実装します。
必要条件:

  • 1ms間隔で割り込み発生
  • スリープ時でも動作する

まずは内部32kHzを有効化します

OSC32KCTRL_REGS->OSC32KCTRL_RTCCTRL = OSC32KCTRL_RTCCTRL_RTCSEL(0U);

GCLK2に32KHzを割り当てます

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です。

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を指定します。

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){}
}
void TC0_Handler(void){
  mrbc_tick();
  TC0_REGS->COUNT16.TC_INTFLAG = (uint8_t)TC_INTFLAG_Msk;
}

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

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

省電力プログラム

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

STANDBYモード時の電源やRAMの扱いについて定義します。

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用メソッドの追加

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 );
}

省電力動作テスト

テスターにて電流値を計測した内容です。

今回mruby/cを使ってSleep時にSTANDBYモードへ切り替える事により0.1mAを実現できました。
LEDの出力を切り替えるときなど一瞬48MHzで動作しているはずです。
実際にSleepを外した状態のコードで実行すると6.9mAで動作しました。

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を使いたいですね。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?