見出し画像

ポケモンSV&ポケモンLA CH552マイコンで三角関数をなんちゃらかんちゃらした話

ぐーるぐーるまーわる


前書き

 数年前まではポケットモンスターのソフトの自動化ではArduino UNOだったり、Arduino Leonardoだったりが有名でしたが、両方ともコロナ禍のなごりか価格が高騰してしまいました。
 そして、最近その代わりのマイコンとなりそうなのが、Raspberry Pi Picoやぼんじり様のCH552だと思うのですが、自分はPoke-Controller-Modifiedで使うCH552-SERIALを買うついでにまとめ買いで値段が安くなるCH552-MCUを10個大人買いしてしまいました。

CH552-MCUはNintendo Switchの自動化ライブラリが用意されており、導入の詳細な説明は公式ショップから参照できるURLに記載されているので本記事では割愛させていただきます。本記事は、CH552-MCU10個を有効利用するための一環であれこれした話となります。

注意

  • この記事の内容には、なんでそんな回りくどいもの実装したの?とか、この記事の作者C言語トーシロなの?とかいうツッコミが出るかと思いますが、自分の失敗談でも色々勉強になるかと思い執筆させて頂きました。ぜひ生暖かい目で読んでいただけると幸いです。

  • 意図せず長文記事になってしまったので、とりあえず作ったもののコードや動作を見たい方は"作ったもの"へジャンプください。

三角関数のおさらいと三角関数でできること

三角関数はジョイコンの操作における角度をそれぞれスティックのX方向倒し量とY方向倒し量を計算するのに使われます。

ますたー様がArduino Leonardoで実装した際の記事が大変わかりやすいので参考にしてみてください。

しかし、関数sin()、cos()はArduino Leonardoのライブラリでは用意されていましたが、CH552では用意されておりませんでした。

GachigumaRide.ino:435: warning 112: function 'sin' implicit declaration
GachigumaRide.ino:436: warning 112: function 'cos' implicit declaration
GachigumaRide.ino:435: error 101: too many parameters 
GachigumaRide.ino:435: error 47: indirections to different types assignment   
from type 'void'
  to type 'int fixed'
GachigumaRide.ino:436: error 101: too many parameters 
GachigumaRide.ino:436: error 106: invalid operand for 'multiplication' operation
exit status 1
ボードCH552 Boardに対するコンパイル時にエラーが発生しました。


テーブルを使った実装方式

処理内容

一番手取り早いのが角度データをROM内に直値で保存する方式です。
今回はcosの角度データを1deg毎にlong型(4byte)で0〜90degまで保持する配列を用意しました。

// 角度データcos
const long cos_data[] = {
  6553600L, // cos(0)*65536*100
  6552602L, // cos(1)*65536*100
  6549608L, // cos(2)*65536*100
  6544619L, // cos(3)*65536*100
  6537636L, // cos(4)*65536*100
  6528662L, // cos(5)*65536*100
  6517699L, // cos(6)*65536*100
  6504750L, // cos(7)*65536*100
  6489821L, // cos(8)*65536*100
  6472914L, // cos(9)*65536*100
  6454036L, // cos(10)*65536*100
  6433192L, // cos(11)*65536*100
  6410388L, // cos(12)*65536*100
  6385632L, // cos(13)*65536*100
  6358930L, // cos(14)*65536*100
  6330291L, // cos(15)*65536*100
  6299725L, // cos(16)*65536*100
  6267239L, // cos(17)*65536*100
  6232844L, // cos(18)*65536*100
  6196551L, // cos(19)*65536*100
  6158370L, // cos(20)*65536*100
  6118313L, // cos(21)*65536*100
  6076392L, // cos(22)*65536*100
  6032621L, // cos(23)*65536*100
  5987012L, // cos(24)*65536*100
  5939579L, // cos(25)*65536*100
  5890337L, // cos(26)*65536*100
  5839300L, // cos(27)*65536*100
  5786485L, // cos(28)*65536*100
  5731908L, // cos(29)*65536*100
  5675584L, // cos(30)*65536*100
  5617532L, // cos(31)*65536*100
  5557768L, // cos(32)*65536*100
  5496311L, // cos(33)*65536*100
  5433181L, // cos(34)*65536*100
  5368395L, // cos(35)*65536*100
  5301974L, // cos(36)*65536*100
  5233938L, // cos(37)*65536*100
  5164307L, // cos(38)*65536*100
  5093104L, // cos(39)*65536*100
  5020349L, // cos(40)*65536*100
  4946065L, // cos(41)*65536*100
  4870274L, // cos(42)*65536*100
  4793000L, // cos(43)*65536*100
  4714265L, // cos(44)*65536*100
  4634095L, // cos(45)*65536*100
  4552513L, // cos(46)*65536*100
  4469544L, // cos(47)*65536*100
  4385214L, // cos(48)*65536*100
  4299548L, // cos(49)*65536*100
  4212573L, // cos(50)*65536*100
  4124314L, // cos(51)*65536*100
  4034799L, // cos(52)*65536*100
  3944055L, // cos(53)*65536*100
  3852109L, // cos(54)*65536*100
  3758991L, // cos(55)*65536*100
  3664727L, // cos(56)*65536*100
  3569346L, // cos(57)*65536*100
  3472879L, // cos(58)*65536*100
  3375354L, // cos(59)*65536*100
  3276800L, // cos(60)*65536*100
  3177248L, // cos(61)*65536*100
  3076729L, // cos(62)*65536*100
  2975272L, // cos(63)*65536*100
  2872909L, // cos(64)*65536*100
  2769671L, // cos(65)*65536*100
  2665589L, // cos(66)*65536*100
  2560696L, // cos(67)*65536*100
  2455022L, // cos(68)*65536*100
  2348600L, // cos(69)*65536*100
  2241463L, // cos(70)*65536*100
  2133643L, // cos(71)*65536*100
  2025174L, // cos(72)*65536*100
  1916087L, // cos(73)*65536*100
  1806417L, // cos(74)*65536*100
  1696196L, // cos(75)*65536*100
  1585459L, // cos(76)*65536*100
  1474239L, // cos(77)*65536*100
  1362570L, // cos(78)*65536*100
  1250486L, // cos(79)*65536*100
  1138021L, // cos(80)*65536*100
  1025209L, // cos(81)*65536*100
  912085L, // cos(82)*65536*100
  798683L, // cos(83)*65536*100
  685038L, // cos(84)*65536*100
  571184L, // cos(85)*65536*100
  457156L, // cos(86)*65536*100
  342989L, // cos(87)*65536*100
  228717L, // cos(88)*65536*100
  114376L, // cos(89)*65536*100
  0L, // cos(90)*65536*100
};

このテーブルを利用してLスティックのをぐりぐりできる処理をますたー様が開発した関数TiltLeftStickをベースに実装しました。

//direction_deg:上0deg、右90deg、下180deg、左270deg
//power:スティックを倒す強さ(0~100)
//holdtime:スティックを倒し続ける時間(ms)
//delaytime:スティックを話した後の時間(ms)
void TiltLeftStick(int direction_deg, int power, unsigned long int holdtime, unsigned long int delaytime){
  int deg, cos_s,sin_s;
  int x, y;

  deg = direction_deg % 360;
  if(deg < 90){
    cos_s = 1;
    sin_s = 1;
  }else if(deg < 180){
    deg  = 180 - deg;
    cos_s = -1;
    sin_s = 1;
  }else if(deg < 270){
    deg = deg - 180;
    cos_s = -1;
    sin_s = -1;
  }else{
    deg  = 360 - deg;
    cos_s = 1;
    sin_s = -1;
  }
  x = (long)sin_s * cos_data[90-deg] * power / 6553600L;
  y = -(long)cos_s * cos_data[deg] * power / 6553600L;
  if(x >= 100) x=100; if(x <= -100) x=-100;
  if(y >= 100) y=100; if(y <= -100) y=-100;

  setStickTiltRatio(x,y,0,0);
  if(holdtime> 0){ // holdtime=0のときは押しっぱなし。
    delay(holdtime);
    setStickTiltRatio(0,0,0,0); // 傾きを直す
  }
  if(delaytime>0) delay(delaytime);
  return;
}

設計意図

説明しやすいのでQ&A方式で説明します。

テーブルはcosだけなの?sinはないの?あとなんで0〜90degまでなの?

三角関数は以下の関係があり、0〜90degまでのcosの角度データだけで全ての角度を実現だからです。目的はROMサイズを削減することです。

$$
\sin\theta =\cos(90\degree-\theta)\\
\cos(180\degree-\theta) =\cos\theta\\
\cos(180\degree+\theta) =-\cos\theta\\
$$

1degあたりlong型で4byte使用しており、それが0〜90degだと4byte × 91個 = 364byte使われております。これが0〜359degだと 1.2kbyte使うことになります。CH552マイコンではROMが16KBが用意されてますが、削減できるなら削減するべきです。
なお、自分のその時の気分でcosにしただけなので、sinのテーブルで実装してもいいと思います。

なんで1deg毎なの?例えば0.1deg毎ににしてテーブルの要素数を増やせばもっと精度でない?

自分もいくつかジョイステックをぐりぐりするSVや剣盾の自動孵化プログラム、空をとぶプログラムを拝見しましたが、自分が知る限り全て1deg単位でした。
また、テーブルの要素数を増やすイコール使用するROMサイズが増えることになります。0.1deg毎だと4byte × 901個 = 約3.6kbyteとなります。必要性があれば3.6kbyte用意することも検討すべきでしょうが、その理由が自分にはわかりませんでした!

なんで角度データを65536*100倍して整数値にしてるの?小数じゃだめなの?

自分が小数が苦手だからです。以上!












半分嘘です。小数こと浮動小数点数は時間だと"0.1秒"、比率だと"0.5"といったように、"人間がソースコードを読む上で"、可読性が上がるというメリットはあります。しかし、浮動小数点数はマイコンによって精度が異なるイコール算出結果が変わることがあります。いい例があったのでPythonでサンプルコード書いて実行してみました。

$ cat testFloat.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

a = 0.1
b = 0.2
c = 0.3

print("a =", a, "\nb =", b, "\nc =", c)
print("a + b + c =", (a + b + c))

print("'a == 0.1' is", a == 0.1)
print("'b == 0.2' is", b == 0.2)
print("'c == 0.3' is", c == 0.3)
print("'(a + b + c) == 0.6' is", (a + b + c) == 0.6)
$ python3 testFloat.py
a = 0.1
b = 0.2
c = 0.3
a + b + c = 0.6000000000000001
'a == 0.1' is True
'b == 0.2' is True
'c == 0.3' is True
'(a + b + c) == 0.6' is False
$

ライブラリに標準実装してある関数の引数や戻り値が浮動小数点ならfloat型やdouble型も使用することもありますが、そうでない場合は使用しないように心がけています。
また、整数値にするにしても65536*100倍した理由は4byteの整数値の範囲は-2,147,483,648〜2,147,483,647であり、関数TiltLeftStick内でLスティックのX,Y方向倒し量を算出する過程でスティックを倒す強さpower(0~100)との乗算結果がオーバフローしない程度に精度が出る値をcos_dataに設定したかったためです。

  x = (long)sin_s * cos_data[90-deg] * power / 6553600L;
  y = -(long)cos_s * cos_data[deg] * power / 6553600L;

まあ、精度一番に考えるなら7FFFFFFFh/100(の切捨て)倍なんですが、ある程度ゆとりを持たせたということで。。。

const int cos_data[] = {
  21474836, // cos(0)*int(7FFFFFFFh/100)
  21471565, // cos(1)*int(7FFFFFFFh/100)
  (途中は省略)
  374788, // cos(89)*int(7FFFFFFFh/100)
  0, // cos(90)*int(7FFFFFFFh/100)
};

近似関数を利用した実装方式

基本思想

Arduino LeonardoのマイコンであるAVRではどうやらavr-libcでCライブラリやMATHライブラリを用意していて三角関数の中核はfp_sinus.Sらしいです。

ENTRY __fp_sinus
	push	ZL

	sbrs	ZL, 0
	rjmp	1f
	ldi	rBE, LO40_PIO2
	ldi	rB0,  lo8(HI40_PIO2)
	ldi	rB1,  hi8(HI40_PIO2)
	ldi	rB2, hlo8(HI40_PIO2)
	ldi	rB3, hhi8(HI40_PIO2 | 0x80000000)
	XCALL	_U(__addsf3x)

1:	XCALL	_U(__fp_round)

	pop	r0
	inc	r0
	sbrc	r0, 1
	subi	rA3, 0x80

	ldi	ZL, lo8(.L_table)
	ldi	ZH, hi8(.L_table)
	LDI_XH_hh8(.L_table)
	XJMP	_U(__fp_powsodd)
ENDFUNC

	PGMX_SECTION(.sinus)
.L_table:
	.byte	5
	.byte	     0xa8,0x4c,0xcd,0xb2	; -0.0000000239
	.byte	0xd4,0x4e,0xb9,0x38,0x36	;  0.0000027526
	.byte	0xa9,0x02,0x0c,0x50,0xb9	; -0.0001984090
	.byte	0x91,0x86,0x88,0x08,0x3c	;  0.0083333315
	.byte	0xa6,0xaa,0xaa,0x2a,0xbe	; -0.1666666664
	.byte	0x00,0x00,0x00,0x80,0x3f	;  1.0000000000
	.end

#endif /* !defined(__AVR_TINY__) */

AVRアセンブラはさっぱり妖精ですが、なんか近似関数を使ってるっぽいですね。調べたらテイラー展開でした。高校の数学ね。

$$
\def\arraystretch{1.5}
\begin{array}{cl}
\sin x &=\sum_{k=0}^\infty (-1)^k\frac{x^{2k+1}}{(2k+1)!}\\
 &=x-\frac{x^3}{3!}+\frac{x^5}{5!}-\frac{x^7}{7!}+\frac{x^9}{9!}+\cdots
\end{array}
$$

処理内容

avr-libcのfp_sinus.Sではk=5までの近似関数で実現しており、自分も同様にしました。また極力計算が減るようにk=5→1の順で計算し、$${\frac{(-1)^k}{(2k+1)!}}$$の部分はテーブルで保持するようにしました。

$$
\def\arraystretch{1.5}
\begin{array}{cl}
\sin x &= x-\frac{x^3}{3!}+\frac{x^5}{5!}-\frac{x^7}{7!}+\frac{x^9}{9!}-\frac{x^{11}}{11!}+\cdots\\
&\fallingdotseq x+c_{4 }x^{3}+c_{3}\ x^{5}+c_{2} x^{7}+c_{1} x^{9}+c_{0} x^{11} \\
&=((((((c_{0}x^2)+c_{1} )x^2+c_{2})x^2+c_{3})x^2+c_{4 })x^2+1)x
\end{array}
$$

実際の$${c_{0}~c_{4}}$$までの値は実際の$${\frac{(-1)^k}{(2k+1)!}}$$とは異なるのですが、$${0\leq x\leq \frac{\pi}{2}}$$までの範囲で$計算精度が10桁程度の際に最適化された値であるとのことです。

$$
\def\arraystretch{1.5}
\begin{array}{rcrcrcr}
-\frac{1}{11!}&=& -0.00000002505\cdots& \hspace{10mm}&c_{0}&=& -0.0000000239&\\
\frac{1}{9!}&=& 0.00000275573\cdots& \hspace{10mm}&c_{1}&=& 0.0000027526&\\
-\frac{1}{7!}&=& -0.00019841269\cdots& \hspace{10mm}&c_{2}&=& -0.0001984090&\\
\frac{1}{5!}&=& 0.00833333333\cdots& \hspace{10mm}&c_{3}&=& 0.0083333315&\\
-\frac{1}{3!}&=& -0.16666666666\cdots& \hspace{10mm}&c_{4}&=& -0.1666666664&\\
\end{array}
$$

const double t_sin_Taylor[] = {
  -0.0000000239,
   0.0000027526,
  -0.0001984090,
   0.0083333315,
  -0.1666666664
};

avr-libcのfp_sinus.Sではアセンブラなので$${c_{0}~c_{4}}$$は5byteで用意して、最終的な計算結果を4byteに丸めて出力しているっぽいです。アセンブラなら(書き方を知ってれば)柔軟にできますね。

double sin_Taylor(double rad){
  double d;
  int i;
  double rad2;

  d = 0.0;
  rad2 = rad * rad;
  for (i = 0; i < 5; i++) {
    d += t_sin_Taylor[i];
    d *= rad2;
  }
  d += 1.0;
  d *= rad;
  return d;
}

double mysin(double rad){
  return sin_Taylor(rad);
}

double mycos(double rad){
  return sin_Taylor(PI/2-rad);
}

実際にmysin()、mycos()をコールしている部分は以下の通りになります。
sin、cosの正負の判定とは後の検証のためにテーブル型と同様にTiltLeftStick関数にて行っております。

//direction_deg:上0deg、右90deg、下180deg、左270deg
//power:スティックを倒す強さ(0~1.0)
//holdtime:スティックを倒し続ける時間(ms)
//delaytime:スティックを話した後の時間(ms)
void TiltLeftStick(int direction_deg, double power, unsigned long int holdtime, unsigned long int delaytime){
  int deg, cos_s,sin_s;
  int x, y;
  double rad;

  deg = direction_deg % 360;
  if(deg < 90){
    cos_s = 1;
    sin_s = 1;
  }else if(deg < 180){
    deg  = 180 - deg;
    cos_s = -1;
    sin_s = 1;
  }else if(deg < 270){
    deg = deg - 180;
    cos_s = -1;
    sin_s = -1;
  }else{
    deg  = 360 - deg;
    cos_s = 1;
    sin_s = -1;
  }

  rad = (double)deg * PI / 180.0;
  x = (double)sin_s * mysin(rad) * power * 100;
  y = -(double)cos_s * mycos(rad) * power * 100;
  if(x >= 100) x=100; if(x <= -100) x=-100;
  if(y >= 100) y=100; if(y <= -100) y=-100;

  setStickTiltRatio(x,y,0,0);
  if(holdtime> 0){ // holdtime=0のときは押しっぱなし。
    delay(holdtime);
    setStickTiltRatio(0,0,0,0); // 傾きを直す
  }
  if(delaytime>0) delay(delaytime);
  return;
}


sinf()、cosf()を使う

後述に記載する各方式を評価するためにMAPファイルの出力する先を調べる最中に判明したのですが、CH552の開発環境に"sin()とcos()はなかった"のですが、代用関数にsinf()、cosf()が用意されておりました。
はい。本記事はもともとCH552に三角関数の実装関数がない想定で作成していたので、その存在意義がなくなりかけました(笑)

処理内容

CH552はIntel 8051を魔改造した物らしく、Arduino IDEではコンパイラはSDCC(Small Device C Compiler)というものを使っています。


#define r1      -0.1666665668E+0
#define r2       0.8333025139E-2
#define r3      -0.1980741872E-3
#define r4       0.2601903036E-5

/* PI=C1+C2 */
#define C1       3.140625
#define C2       9.676535897E-4

/*A reasonable value for YMAX is the int part of PI*B**(t/2)=3.1416*2**(12)*/
#define YMAX     12867.0

float sincosf(float x, bool iscos)
{
    float y, f, r, g, XN;
    int N;
    bool sign;

    if(iscos)
    {
        y=fabsf(x)+HALF_PI;
        sign=0;
    }
    else
    {
        if(x<0.0)
            { y=-x; sign=1; }
        else
            { y=x; sign=0; }
    }

    if(y>YMAX)
    {
        errno=ERANGE;
        return 0.0;
    }

    /*Round y/PI to the nearest integer*/
    N=((y*iPI)+0.5); /*y is positive*/

    /*If N is odd change sign*/
    if(N&1) sign=!sign;

    XN=N;
    /*Cosine required? (is done here to keep accuracy)*/
    if(iscos) XN-=0.5;

    y=fabsf(x);
    r=(int)y;
    g=y-r;
    f=((r-XN*C1)+g)-XN*C2;

    g=f*f;
    if(g>EPS2) //Used to be if(fabsf(f)>EPS)
    {
        r=(((r4*g+r3)*g+r2)*g+r1)*g;
        f+=f*r;
    }
    return (sign?-f:f);
}

SDCCで三角関数の処理の中核を担うsincosf.cではk=4までの近似関数で実現してます。やることは自分が作成した近似関数と思想は同じですが、安全処理やcosの場合の処理など色々丁寧に処理してる感じですね。

$$
\def\arraystretch{1.5}
\begin{array}{rcrcrcr}
\frac{1}{3!}&=& -0.16666666666\cdots& \hspace{5mm}&r_{1}&=& -0.1666665668×10^{-0}&\\
-\frac{1}{5!}&=& 0.00833333333\cdots& \hspace{5mm}&r_{2}&=& 0.8333025139×10^{-2}&\\
\frac{1}{7!}&=& -0.00019841269\cdots& \hspace{5mm}&r_{3}&=& -0.1980741872×10^{-3}&\\
-\frac{1}{9!}&=& 0.00000275573\cdots& \hspace{5mm}&r_{4}&=& 0.2601903036×10^{-5}&\\
\end{array}
$$


ROMサイズと処理時間の比較

色々と三角関数の実装例を調べたので、ROMサイズと処理時間なのでメリット、デメリットを調べてみました。
自分の見立てではROMサイズは

$$
テーブル >> 近似関数 \fallingdotseq sinf()、cosf()
$$

となり、処理時間は

$$
テーブル < sinf()、cosf() < 近似関数
$$

となる想定ですが、実際に5msごとに角度を計算して200回で1回転、それを50回転Lスティックをぐるぐるする処理を作ってみました。ポケモンSVで動かすと本記事冒頭の画像のようにぐるぐる回ります。

void setup() {
  // USBコントローラーの初期化
  USBInit();

  delay(1000);
  pushButtonLoop(BUTTON_B, 500, 4);
}

void loop() {

  int i, j;
  int deg;

  for(i =0; i < 50; i++) {
    for(j = 0; j < 200; j++) {
      deg = ((long)360 * j) / 200;
      TiltLeftStick(deg, 100);
      delay(5);
    }
  }
  TiltLeftStick(0, 0.0);
  delay(1000);

  pushButtonLoop(BUTTON_B, 500, 2);
}

//direction_deg:上0deg、右90deg、下180deg、左270deg
//power:スティックを倒す強さ(0~100)
//holdtime:スティックを倒し続ける時間(ms)
//delaytime:スティックを話した後の時間(ms)
void TiltLeftStick(int direction_deg, int power){
#if IS_TABLE != 2
  int deg, cos_s,sin_s;
#endif
  int x, y;
#if IS_TABLE != 1
  double rad;
#endif

#if IS_TABLE != 2
 deg = direction_deg % 360;
  if(deg < 90){
    cos_s = 1;
    sin_s = 1;
  }else if(deg < 180){
    deg  = 180 - deg;
    cos_s = -1;
    sin_s = 1;
  }else if(deg < 270){
    deg = deg - 180;
    cos_s = -1;
    sin_s = -1;
  }else{
    deg  = 360 - deg;
    cos_s = 1;
    sin_s = -1;
  }
#endif

#if IS_TABLE == 1
  x = (long)sin_s * cos_data[90-deg] * power / 6553600L;
  y = -(long)cos_s * cos_data[deg] * power / 6553600L;
#elif IS_TABLE == 0
  rad = (double)deg * PI / 180.0;
  x = (double)sin_s * mysin(rad) * power;
  y = -(double)cos_s * mycos(rad) * power;
#else
  rad = (double)direction_deg * PI / 180.0;
  x = sinf(rad) * power;
  y = -cosf(rad) * power;
#endif
  if(x >= 100) x=100; if(x <= -100) x=-100;
  if(y >= 100) y=100; if(y <= -100) y=-100;

  setStickTiltRatio(x,y,0,0);
}

なお、これから記述する結果はCH552マイコンという前提で評価した結果となります。これが他のマイコンだと違う結果になる可能性があることを心に留めて読んでいただけると幸いです。

ROMサイズの比較

単純にROMサイズを見るだけならArduino IDEでのビルド時のメッセージで確認できます。

  • テーブルを使った場合

最大14336バイトのフラッシュメモリのうち、スケッチが7734バイト(53%)を使っています。
最大876バイトのRAMのうち、グローバル変数が173バイト(19%)を使っていて、ローカル変数で703バイト使うことができます。
  • 近似関数を使った場合

最大14336バイトのフラッシュメモリのうち、スケッチが8908バイト(62%)を使っています。
最大876バイトのRAMのうち、グローバル変数が189バイト(21%)を使っていて、ローカル変数で687バイト使うことができます。
  • sinf()、cosf()を使った場合

最大14336バイトのフラッシュメモリのうち、スケッチが9606バイト(67%)を使っています。
最大876バイトのRAMのうち、グローバル変数が187バイト(21%)を使っていて、ローカル変数で689バイト使うことができます。

近似関数を使うとテーブルを使うよりも1174byteも多くROMを使用しているし、sinf()、cosf()を使うと1872byteも多くROMを使用している。。。

なんでやΣ(・ω・ノ)ノビックリ


その謎を解明するため、我々調査隊はアマゾンの奥地へと向かった!!!


実際にアマゾンの奥地へは行きませんが、MAPファイルを解析して、使用されている関数と定数、及びそのROMサイズに違いがあるものだけまとめてみました。

$$
\footnotesize
\def\arraystretch{1.5}
\begin{array}{l|l|r|r|r}
関数名、定数名&ファイル名 & テーブル & 近似関数 & \text{sinf,cosf}\\ \hline
\text{\_sin\_Taylor}&\text{SV\_GuruGuru\_ino} & & 424 & \\
\text{\_mysin}&\text{SV\_GuruGuru\_ino} & & 43 & \\
\text{\_mycos}&\text{SV\_GuruGuru\_ino} & & 73 & \\
\text{\_TiltLeftStick}&\text{SV\_GuruGuru\_ino} & 737 & 769 & 439\\
\text{\_\_\_fssub}&\text{\_fssub} & & 11 & 11\\
\text{\_\_\_fsmul}&\text{\_fsmul} & & 163 & 163\\
\text{fsgetargs}&\text{\_fsget2args} & & 46 & 46\\
\text{fs\_normalize\_a}&\text{\_fsnormalize} & & 51 & 51\\
\text{\_\_\_fsadd}&\text{\_fsadd} & & 3 & 3\\
\text{fsadd\_direct\_entry}&\text{\_fsadd} & & 86 & 86\\
\text{\_\_\_sint2fs}&\text{\_sint2fs} & & 13 & 13\\
\text{\_\_\_fs2sint}&\text{\_fs2sint} & & 52 & 52\\
\text{fs\_round\_and\_return}&\text{\_fsreturnval} & & 24 & 24\\
\text{fs\_zerocheck\_return}&\text{\_fsreturnval} & & 9 & 9\\
\text{fs\_return\_zero}&\text{\_fsreturnval} & & 8 & 8\\
\text{fs\_direct\_return}&\text{\_fsreturnval} & & 14 & 14\\
\text{fs\_return\_inf}&\text{\_fsreturnval} & & 13 & 13\\
\text{fs\_return\_nan}&\text{\_fsreturnval} & & 11 & 11\\
\text{fs\_swap\_a\_b}&\text{\_fsswapargs} & & 26 & 26\\
\text{\_\_\_fsdiv}&\text{\_fsdiv} & & 195 & 195\\
\text{\_\_\_slong2fs}&\text{\_slong2fs} & & 9 & 9\\
\text{slong2fs\_doit}&\text{\_slong2fs} & & 36 & 36\\
\text{fs\_rshift\_a}&\text{\_fsrshift} & & 67 & 67\\
\text{\_\_\_fs2slong}&\text{\_fs2slong} & & 91 & 91\\
\text{fsgetarg}&\text{\_fsget1arg} & & 18 & 18\\
\text{\_t\_sin\_Taylor}&\text{SV\_GuruGuru\_ino} & & 20 & \\
\text{\_cos\_data}&\text{SV\_GuruGuru\_ino} & 364 & & \\
\text{\_cosf}&\text{cosf} & & & 47\\
\text{\_sinf}&\text{sinf} & & & 44\\
\text{\_sincosf}&\text{sincosf} & & & 1285\\
\text{\_\_\_fslt}&\text{\_fslt} & & & 48\\
\text{\_fabsf}&\text{fabsf} & & & 91\\
\text{fs\_compare\_uint32}&\text{\_fscmp} & & & 30\\
\text{fs\_check\_negative\_zeros}&\text{\_fscmp} & & & 43\\
\end{array}
$$

近似関数を使う場合とsinf(),cosf()を使う場合は明らかに"fs"という名称がついた関数がROMに追加されてます。試しに___fssubを見ると明らかにfloat型の二つの引数を差分を求めている関数っぽいので、"fs"という名称がついた関数はfloat型を演算するために追加された処理と思われます。
ちなみにcos_dataはちょうど364byte使われていますが、これが0.1degごとのテーブルの場合だと、やはりこの10倍ROMを使用するので、その場合はテーブル方式は7734byte-364byte+3604byte=10974byteとなり、ROM使用量的には他の2つの方式の優れていることになります。

処理時間の比較


50回転を1セットとして、その処理時間をストップウォッチで目押しで時間を測定しました(目押しなので少々の時間のムラはご勘弁ください)。

$$
\def\arraystretch{1.5}
\begin{array}{c|c|c|c}
& テーブル& 近似関数 & sinf(),cosf()\\\hline\hline
1セット目 & 59.66秒 & 62.63秒 & 64.06秒\\\hline
2セット目 & 59.44秒 & 62.90秒 & 64.47秒\\\hline
3セット目 & 59.75秒 & 62.70秒 & 65.16秒\\\hline
4セット目 & 59.53秒 & 62.56秒 & 63.06秒\\\hline
5セット目 & 59.80秒 & 62.91秒 & 64.20秒\\\hline
セット平均時間& 59.64秒 & 62.74秒 & 64.19秒\\\hline\hline
\begin{matrix}
\text{TiltLeftStick()}\\
平均処理時間
\end{matrix}& 754us & 1064us & 1209us\\
\end{array}
$$

1回転あたり待機処理は5ms×200回=1秒で、それを50回転なので、TiltLeftStick()の処理時間を考慮しなければ50秒で終わるはずなのですが、みんな+10秒くらいかかってますね(笑)
TiltLeftStick()の処理時間は1セット毎に1秒の待機処理(delay(1000))と1.1秒のBボタン2回入力(pushButtonLoop(BUTTON_B, 500, 2))があるので以下の式で計算しています。

$$
\text{TiltLeftStick()}平均処理時間 = \frac{セット平均時間- 2.1s}{200×50}-5ms
$$

ここまで処理時間に差が出た原因は、三角関数の中核処理でそれぞれ行なっている乗算処理の実行回数が原因と思われまます。
また、乗算処理の1回の実行がC言語1行でもマイコン内部では複数回、"乗算命令"や"加算命令"が実行されるものなのです。

例えば67×89=5963で考えてみます。

$$
\begin{array}{ccc}
&&6&7&\\
&×)&8&9\\
\hline
&6&0&3\\
5&3&6&\\\hline
5&9&6&3
\end{array}
$$

すごく頭のいい人以外は7×9=63、6×9=54、7×8=56、6×8=48と"1桁ずつ"を4回掛け算して、それぞれの桁の数字を"1桁ずつ"足し算していくと思います。
8bitマイコンの世界ではこの"1桁ずつ"が"1byte(8bit)ずつ"に置き換わるイメージで考えて頂ければとりあえず大丈夫です(実際には変数から演算レジスタへ、演算レジスタから変数に転送するという処理があるはずなのですが、自分がCH552マイコンのアセンブラを理解していないので割愛します)。

$$
\begin{array}{}
&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
\\ \\
\end{matrix}
&→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
&&6&3\\ \\
\end{matrix}
&→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
&  & 6 & 3\\ 
& 5 & 4&
\end{matrix}\\
\\\hdashline\hdashline\\
→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
& 1 & 0 & 3\\ 
& 5 & &
\end{matrix}
&→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
& 6 & 0 & 3\\ 
& & &
\end{matrix}
&→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
& 6 & 0 & 3\\ 
& 5 & 6 &
\end{matrix}\\
\\\hdashline\hdashline\\
→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
& 6 & 0 & 3\\ 
& 5 & 6 &\\ 
4 & 8 & &
\end{matrix}
&→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
& 6 & 6 & 3\\ 
& 5 & & \\ 
4 & 8 & &
\end{matrix}
&→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
1 & 1 & 6 & 3\\ 
& & &\\ 
4 & 8 & &
\end{matrix}\\
\\\hdashline\hdashline\\
→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
1 & 9 & 6 & 3\\ 
& & &\\ 
4 & & &
\end{matrix}
&→&
\begin{matrix}
& & 6 & 7 &\\
& ×) & 8 & 9\\ \hline
5 & 9 & 6 & 3\\ 
& & &\\ 
& & &
\end{matrix}
\end{array}
$$

これが4byte同士の乗算、除算だとさらに多くの四則命令使うことになり、それが処理時間に直結します。マイコンの処理速度が速くてもマイコンが行う命令数が多いと処理時間は増えていくものです。
ミリ秒単位での応答性はSwitchの自動化プログラムでは求められないと思いますが、例えばこれが10ms毎に入力をサンプリングするプログラムや、一定fpsごとに描画するプログラムは実装したプログラムがどんな命令になるか
乗算処理や除算処理は一度に何十回と実行するとそれなりの処理時間がかかることを留意しておくべきと思います。

注意事項

本章の冒頭でも述べましたが、CH552マイコンでの評価結果となります。
まず、ROMサイズにてfloat型、double型を使用するとその演算用関数が増えた件も、FPUといった浮動小数点を高速に演算する機能をマイコン内部で持っていれば話は別となります。
Raspberry Pi Picoだと高速な浮動小数点演算ライブラリがあるとのことですが、Raspberry Pi Pico 2だとマイコン内部にFPUが内蔵しているとのことです。

2.8.3.2. Fast Floating Point Library
The Bootrom contains an optimized single-precision floating point implementation. Additionally V2 onwards also
contain an optimized double-precision float point implementation. The function pointers for each precision are kept in a
table structure found via the rom_data_lookup table (see Section 2.8.3.3).

RP2040 Datasheet

3.6.4. Floating Point Unit
The Cortex-M33 cores on RP2350 are configured with the standard Arm single-precision floating point unit (FPU).
Coprocessor ports 10 and 11 access the FPU.
The Arm floating point extension is documented in the Armv8-M Architecture Reference Manual.
Applications built with the SDK use the FPU automatically by default. For example, calculations with the float data type
in C automatically use the standard FPU, while calculations with the double data type automatically use the RP2350
double-precision coprocessor (Section 3.6.2).

RP2350 Datasheet


次に処理時間ですが、CH552は8bitマイコンなので1byteずつしか乗除算してましたが、32bitマイコンだと4byteの乗除算が1回の演算命令で実行可能です。また、乗算よりさらに時間がかかる除算も、Raspberry Pi Picoだと整数のハードウェア除算器(Integer Divider)が周辺機器に搭載されていて、Raspberry Pi Pico 2ではマイコン内部に内蔵しているからさらに高速であることがデータシートに記載されております。

3.1.7. Integer Divider
RP2040’s memory-mapped integer divider peripheral is not present on RP2350, since the processors support divide
instructions. The address space previously allocated for the divider registers is now reserved.

RP2350 Datasheet

自分の中では今のとこ使い道はないですが、本記事を執筆しながらRaspberry Pi Pico 2のパフォーマンスが分かってきたので1~2個買っとくのもアリかな~と思いました(笑)

作ったもの

・CH552版ポケモンレジェンズアルセウス ガチグマライド(道具掘り)の自動化プログラム

ますたー様のレジェンズアルセウスにおけるピートブロック・くろのきせき自動収集プログラムをCH552向けに移植したものになります。スティックの角度からX方向倒し量とY方向倒し量への変換は、1deg単位で問題なかったので、処理が一番軽量で速いテーブル方式を採用してます。

あとがき

まず、長文となった本記事を読んで頂き、本当にありがとうございました。
三角関数の話をちょこっとして作ったプログラムを公開するだけのつもりだったのですが、評価プログラム作って結果追加してそれに対する考察書いたり、捕捉のためのプログラムや画像を作ったり、なんだかんだしてたら2万字超える記事になってしまいました。次に書く記事(ネタはいくつかあるのですが、文章力が追い付いておらず、いつになるのか。。。)はもう少しかるく読めるものにしたいです。

もうソフト開発は一線を遠ざかってしまったので、ソフト開発を最前線で行っている方にとっては当たり前のようなことや、もしかしたら初歩的なことを述べてしまっているかもしれませんが、おさらいレベルで眺めて頂けるだけでも幸いです。

また、三角関数の実装でテイラー展開を利用してましたが、高校時代に学んでいたおかげで実装内容も理解しやすかったので、真面目(笑)に数学勉強していてよかったな~と今回初めて思えました。学んだことは意外といつか生きてきますね。

また、本記事作成にあたり、ピートブロック・くろのきせき自動収集プログラムの移植版の公開を快く承諾して頂いたますたー様、CH552-MCUを販売元であるぼんじり様に感謝の意を申し上げます。

ちなみにCH552-MCU、ちょっとした作業を代わりで行うプログラムをちょこちょこ作ってたら10個じゃ全然足りなくなり、色々開発が面白くなったのも相まって、追加でも買っていたりします(笑)