鳥なき里のマイコン屋(141) ラズパイPico、ハードウエア割り算器の利用

Joseph Halfmoon

前回は浮動小数点数の計算でしたが、今回は整数の割り算です。組み込みMCUのプログラマには「割り算は避ける」習性が刷り込まれています(個人の感想です。)せいぜい2のべき乗の割り算に帰着させて右シフトで逃げます。しかしラズパイPicoでは割り算、あまり苦になりません。ハードウエアの割り算器を搭載。

(末尾に実験に使ったソース全文を掲げました。)

「近代的」(いつから?)なマイコンでは、乗算器の搭載が普通になり、もはや乗算はコストの高い(時間がかかるという意味です。表立って料金とられるわけではありませんよ)演算ではなくなりました。むしろ信号処理用に積和演算などの命令を充実させている機種では売りのポイントです。積極的に使って欲しい機能だと思います。

ラズパイPicoのマイコンRP2040のコアである Arm Cortex-M0+はCortex-Mシリーズの「もっともお手頃な機種」です。符号付きの乗算命令こそ持っていますが、上位機種の方々が売りにしている積和演算などのサポートはありませぬ。勿論、コアそのものに除算命令はありません。

しかしRP2040にはコア毎にハードウエア・ディバイダ(整数除算器)を搭載、「たったの」8クロックで32ビット整数の割り算が可能です。もはや割り算恐れるに足りず!本当か?

とは言え、CPUの命令として割り算がサポートされているわけでなく、「周辺装置」としての専用演算器なので、使うには多少の準備が必要です。

3レベルの使用方法

ハードウエア除算器を使うために、PicoのSDKには2レベルのAPIがありました。

  1. 低いレベルでハードウエアを制御するAPI
  2. 高いレベル?でハードウエアを制御するAPI

低いレベルの方が、hardware_divider と呼ばれていて、そのAPIについては以下に説明があります。

Hardware APIs hardware_divider

また、高いレベルの方は、pico_divider と呼ばれていて、説明は以下です。

Runtime Infrastructure pico_divider

高いレベルのAPIを使えるようにすると、自動的に低いレベルのAPIも使えるようになっていました。今回は高いレベル?の pico_divider を使えるように設定して実験しています。さらに、pico_divider の使い方として以下2つがあります。

  • 明示的に割り算用の関数を呼び出す
  • Cの “/” , “%” 演算子を使うと自動的にハードウエアdivider呼び出しになる

pico_divider を生かしてやりさえすれば Cの演算子は自動的にハードウエアdivider呼び出しになります。設定的にはこれが一番お楽かと思います。

ただしいろいろなレベルが共存しているのは、処理シーンを考えるといくつかの選択肢がついてまわるからです。

まずは割り込みの問題です。この手のハードウエア演算器につきものですが、プログラムのどこかでハードウエア演算器を使いはじめ、まだ結果が出ていないところで割り込み発生ってな状況を考えます。割り込みハンドラ内で、何も処置せず同じハードウエア演算器を使ってしまった後、割り込みから「戻る」と割り込まれた側の演算結果が失われてしまいます。

この問題への対処としては、ざっくり以下の2つかと思います。

  • ハードウエア演算器の使用中に割り込んでしまったら、ハードウエア演算器の処理結果をどこかに一時保存してから演算器を使用する。演算器の使用を終えたら演算結果を演算器レジスタに戻してから元のプログラムに戻れば、元のプログラムに影響はでない
  • 割り込み処理ハンドラ側ではハードウエア演算器を使わないようにする。例えばどうしても割り算必要ならソフトで処理する。

そのため、明示的なpico_dividerのAPIではsafe(何も書いてないのがsafe)とかunsafeとか関数の種類が増えてます。unsafeは何もチェックしないので微妙に速いはずですが退避はしてくれません。unsafeと書かれていない関数は割り算器が使用中かどうかを調べて退避してから使用し、終わったら戻してくれるみたい。安全ですが、微妙に遅くなる筈。

また、ハードウエアの割り算器は速い、とは言え処理に8サイクルかかります。その時間を単に待ち時間としてヒマ潰しをしても良いですが、それも許したくない場合、割り算スタートさせてから並行して別の仕事をやっておき、終わったら割り算結果を取り出して、といった具合の処理も可能です。低レベルのAPIを使うとそういう処理も可能になります。

実験につかったコード

末尾に掲げたソースでは、3レベルの使用方法で同じ計算をやっています。

  1. Cの演算子レベルでハードウエア除算器を呼び出す
  2. Pico_dividerの高レベルな関数で明示的に呼び出す
  3. hardware_dividerの低レベルな関数で明示的に呼び出す

まず第1のケースのために、本当にCの演算子がハードウエア除算器呼び出しに向いているのかどうかを確かめました。以下 objdumpの結果の一部です。/ や % している部分と推定できる箇所で、ラッパ関数 __warp__aeabi_idivが呼び出されていました。

1000035c <main>:
1000035c:	b530      	push	{r4, r5, lr}
1000035e:	b083      	sub	sp, #12
10000360:	f003 ffcc 	bl	100042fc <stdio_init_all>
10000364:	481f      	ldr	r0, [pc, #124]	; (100003e4 <hw_divider_result_loop_90+0x34>)
10000366:	f003 ff8d 	bl	10004284 <__wrap_puts>
1000036a:	24fa      	movs	r4, #250	; 0xfa
1000036c:	00a4      	lsls	r4, r4, #2
1000036e:	e02f      	b.n	100003d0 <hw_divider_result_loop_90+0x20>
10000370:	210b      	movs	r1, #11
10000372:	0020      	movs	r0, r4
10000374:	f002 ff38 	bl	100031e8 <__wrap___aeabi_idiv>
10000378:	0005      	movs	r5, r0
1000037a:	210b      	movs	r1, #11
1000037c:	0020      	movs	r0, r4
1000037e:	f002 ff33 	bl	100031e8 <__wrap___aeabi_idiv>

ラッパが呼びしている先を追いかけていくと、pico_divider 内の

divmod_s32s32_savestate

が呼び出されていました。savestateなので安全な方の関数みたいです。

第2の方法では高水準の関数を呼び出すので書きやすいです。Cの関数レベルで被除数と除数を渡せば、商と余りが返ってきます。

第3の方法では低水準の関数なので処理手順を細かく制御できます。被除数と除数を渡してスタートをかけ、その後、計算終了を待って返り値を取り出しています。しかし返り値には商と余りが混載されているので、別途分けるひと手間が必要です(実際には高水準の関数は内部でそのような処理をしている。)

実験結果

3通りの方法で同じ割り算するだけなので、なにも波乱はありません。結果の例が以下です。

Operators : 1034 / 11 = 94, remainder=0
divmod_s32s32_rem : 1034 / 11 = 94, remainder=0
hw_divider_divmod_s32: 1034 / 11 = 94, remainder=0
Operators : 1035 / 11 = 94, remainder=1
divmod_s32s32_rem : 1035 / 11 = 94, remainder=1
hw_divider_divmod_s32: 1035 / 11 = 94, remainder=1
Operators : 1036 / 11 = 94, remainder=2
divmod_s32s32_rem : 1036 / 11 = 94, remainder=2
hw_divider_divmod_s32: 1036 / 11 = 94, remainder=2
Operators : 1037 / 11 = 94, remainder=3
divmod_s32s32_rem : 1037 / 11 = 94, remainder=3
hw_divider_divmod_s32: 1037 / 11 = 94, remainder=3

割り算も出来ました。けれども実性能は測っていません。ううむ、性能と言い始めると他とも比べたくなるしなあ。どうしますか。

鳥なき里のマイコン屋(140) ラズパイPico、float計算でROMを呼んでいるよね へ戻る

鳥なき里のマイコン屋(142) ラズパイPico、SDKでUart入出力 へ進む

実験で使用したCソースコード全文
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/divider.h"


int main()
{
    int32_t a0, b0, q0, q1, q2, r0, r1, r2;
    stdio_init_all();

    puts("Hardware Divider Test.");

    for (int i=1000; i<2000; i++) {
        a0 = i;
        b0 = 11;
        // Operators
        q0 = a0 / b0;
        r0 = a0 % b0;
        printf("Operators            : %d / %d = %d, remainder=%d\n", a0, b0, q0, r0);
        // pico_
        q1 = divmod_s32s32_rem(a0, b0, &r1);
        printf("divmod_s32s32_rem    : %d / %d = %d, remainder=%d\n", a0, b0, q1, r1);
        // hardware_divider, primitive
        hw_divider_divmod_s32_start(a0, b0);
        divmod_result_t result = hw_divider_result_wait();
        q2 = to_quotient_s32(result);
        r2 = to_remainder_s32(result);
        printf("hw_divider_divmod_s32: %d / %d = %d, remainder=%d\n", a0, b0, q2, r2);

        sleep_ms(1000);
    }

    puts("End of divider.");
    return 0;
}
CMakeLists.txtファイル
# Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# initalize pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/home/pi/pico/pico-sdk")

# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)

project(divider0 C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1

add_executable(divider0 divider0.c )

pico_set_program_name(divider0 "divider0")
pico_set_program_version(divider0 "0.1")

pico_enable_stdio_uart(divider0 0)
pico_enable_stdio_usb(divider0 1)

# Add the standard library to the build
target_link_libraries(divider0 
    pico_stdlib
    pico_divider
    )

pico_add_extra_outputs(divider0)