BBBのDMTIMERモジュールの使い方

概要

BeagleBone Blackに搭載されているAM335xのTimer Moduleを使用する方法について説明する。
AM335xには、8個のDMTIMERタイマーモジュールが搭載されている。OSで実現する場合、数ms程度のジッタが発生するが、タイマーモジュールを使う事で、精度の高いタイマーを実現することが出来る。
DMTIMER0, DMTIMER1MSは他とは多少異なる構成となっているが、DMTMER2-7は同一の構成となっている。
レジスタはCortex-AとPRU何れからでもアクセスが可能となっている。クロックソースは(外部入力)TCLKIN、CLK_M_OSC(BBBでは24MHz)、CLK_32KHZ(32768Hz)から選択することが出来る。
BBBではこのうちDMTIMER4-7の入出力がピンヘッダに接続されている。
タイマーモジュールやレジスタの詳細についてはTexas Instruments提供のAM335x and AMIC110 Sitara Processors Technical Reference Manualを参照の事。

設定方法

ubootの設定

BBBの場合、ピンヘッダに入出力ピンが出ており、dtbの設定で割り当てを何通りか変更できるため、どのピンを使用するかを予め検討しておく必要がある。
dtbファイルで必要なtimerモジュールやtclkinを有効にしておく。あるいは、一時的にであれば、universal capeをenableにしておき、config-pinコマンドでtimerに割り当てる事でも設定可能。
以下に、関連するMux設定の抜粋を示す。詳細はBBBのリッファレンスマニュアルを参照の事。
PINOFFSETPROCNAMEMODE0MODE1MODE2MODE3MODE4MODE5MODE6MODE7
P8.790hR7TIMER4gpmc_advn_aletimer4gpio2[2]
P8.894hT7TIMER7gpmc_oen_rentimer7gpio2[3]
P8.99ChT6TIMER5gpmc_be0n_cletimer5gpio2[5]
P8.1098hU6TIMER6gpmc_wentimer6gpio2[4]
P9.1917ChD17I2C2_SCLuart1_rtsntimer5dcan0_rxI2C2_SCLspi1_cs1gpio0[13]
P9.20178hD18I2C2_SDAuart1_ctsntimer6dcan0_txI2C2_SDAspi1_cs0gpio0[12]
P9.411B4hD14CLKOUT2xdma_event_intr1tclkinclkout2timer7_mux1EMU3_mux0gpio0[20]

dtbファイルは次のようにした。
ポイントとしては、Mode設定に加えて、Inputに設定する必要がある。pullup/downは回路に合わせて設定すること。

/dts-v1/;
/plugin/;

/ {
    compatible = "ti,beaglebone", "ti,beaglebone-black";

    /* identification */
    part-number = "BB-DMTIMER";
    version = "00A0";

    exclusive-use =
        "P8.07",
        "P8.08",
        "P8.09",
        "P8.10";

    fragment@0 {
        target = <&am33xx_pinmux>;
        __overlay__ {
            timer_gpio: pinmux_timer_pins {
                pinctrl-single,pins = <
                    0x90 0x2a  /* P8.07 timer5 IN|PUPD_OFF|MODE2 1 01 010 */
                    0x94 0x2a  /* P8.08 timer6 IN|PUPD_OFF|MODE2 1 01 010 */
                    0x9C 0x2a  /* P8.09 timer5 IN|PUPD_OFF|MODE2 1 01 010 */
                    0x98 0x2a  /* P8.10 timer6 IN|PUPD_OFF|MODE2 1 01 010 */
                >;
            };
        };
    };

    fragment@1 {
        target = <&ocp>;
        __overlay__ {
            test_helper: helper {
                compatible = "bone-pinmux-helper";
                pinctrl-names = "default";
                pinctrl-0 = <&timer_gpio>;
                status = "okay";
            };
        };
    };
};

Timer moduleのenable

デフォルトでタイマーモジュールはdisableになっているので、CM_PER registersのTIMERx_CLKCTRLでenableに設定します。
Offset
0x44E00000 +
Acronym
0hCM_PER_L4LS_CLKSTCTRL
4hCM_PER_L3S_CLKSTCTRL
ChCM_PER_L3_CLKSTCTRL
14hCM_PER_CPGMAC0_CLKCTRL
18hCM_PER_LCDC_CLKCTRL
1ChCM_PER_USB0_CLKCTRL
24hCM_PER_TPTC0_CLKCTRL
28hCM_PER_EMIF_CLKCTRL
2ChCM_PER_OCMCRAM_CLKCTRL
30hCM_PER_GPMC_CLKCTRL
34hCM_PER_MCASP0_CLKCTRL
38hCM_PER_UART5_CLKCTRL
3ChCM_PER_MMC0_CLKCTRL
40hCM_PER_ELM_CLKCTRL
44hCM_PER_I2C2_CLKCTRL
48hCM_PER_I2C1_CLKCTRL
4ChCM_PER_SPI0_CLKCTRL
50hCM_PER_SPI1_CLKCTRL
60hCM_PER_L4LS_CLKCTRL
68hCM_PER_MCASP1_CLKCTRL
6ChCM_PER_UART1_CLKCTRL
70hCM_PER_UART2_CLKCTRL
74hCM_PER_UART3_CLKCTRL
78hCM_PER_UART4_CLKCTRL
7ChCM_PER_TIMER7_CLKCTRL
80hCM_PER_TIMER2_CLKCTRL
84hCM_PER_TIMER3_CLKCTRL
88hCM_PER_TIMER4_CLKCTRL
AChCM_PER_GPIO1_CLKCTRL
B0hCM_PER_GPIO2_CLKCTRL
B4hCM_PER_GPIO3_CLKCTRL
BChCM_PER_TPCC_CLKCTRL
C0hCM_PER_DCAN0_CLKCTRL
C4hCM_PER_DCAN1_CLKCTRL
CChCM_PER_EPWMSS1_CLKCTRL
D4hCM_PER_EPWMSS0_CLKCTRL
D8hCM_PER_EPWMSS2_CLKCTRL
DChCM_PER_L3_INSTR_CLKCTRL
E0hCM_PER_L3_CLKCTRL
E4hCM_PER_IEEE5000_CLKCTRL
E8hCM_PER_PRU_ICSS_CLKCTRL
EChCM_PER_TIMER5_CLKCTRL
F0hCM_PER_TIMER6_CLKCTRL
F4hCM_PER_MMC1_CLKCTRL
F8hCM_PER_MMC2_CLKCTRL
FChCM_PER_TPTC1_CLKCTRL
100hCM_PER_TPTC2_CLKCTRL
10ChCM_PER_SPINLOCK_CLKCTRL
110hCM_PER_MAILBOX0_CLKCTRL
11ChCM_PER_L4HS_CLKSTCTRL
120hCM_PER_L4HS_CLKCTRL
12ChCM_PER_OCPWP_L3_CLKSTCTRL
130hCM_PER_OCPWP_CLKCTRL
140hCM_PER_PRU_ICSS_CLKSTCTRL
144hCM_PER_CPSW_CLKSTCTRL
148hCM_PER_LCDC_CLKSTCTRL
14ChCM_PER_CLKDIV32K_CLKCTRL
150hCM_PER_CLK_24MHZ_CLKSTCTRL
CM_PER_TIMERx_CLKCTRL Register
BitFieldTypeResetDescription
31-18ReservedR0h
17-16IDLESTR3h Module idle status.
0x0 = Func : Module is fully functional, including OCP
0x1 = Trans : Module is performing transition: wakeup, or sleep, or sleep abortion
0x2 = Idle : Module is in Idle mode (only OCP part). It is functional if
using separate functional clock
0x3 = Disable : Module is disabled and cannot be accessed
15-2ReservedR0h
1-0MODULEMODER/W0h Control the way mandatory clocks are managed.
0x0 = DISABLED : Module is disable by SW. Any OCP access to module results in an error, except if resulting from a module wakeup (asynchronous wakeup).
0x1 = RESERVED_1 : Reserved
0x2 = ENABLE : Module is explicitly enabled. Interface clock (if not used for functions) may be gated according to the clock domain state. Functional clocks are guarantied to stay present. As long as in this configuration, power domain sleep transition cannot happen.
0x3 = RESERVED : Reserved

Clock sourceの設定

クロックソースの設定は、CM_DPLL registersのCLKSEL_TIMERSx_CLKで行う。
Offset
0x44E00500 +
AcrynymRegisterName
4hCLKSEL_TIMER7_CLKSelects the Mux select line for TIMER7 clock
8hCLKSEL_TIMER2_CLKSelects the Mux select line for TIMER2 clock
ChCLKSEL_TIMER3_CLKSelects the Mux select line for TIMER3 clock
10hCLKSEL_TIMER4_CLKSelects the Mux select line for TIMER4 clock
14hCM_MAC_CLKSELSelects the clock divide ration for MII clock
18hCLKSEL_TIMER5_CLKSelects the Mux select line for TIMER5 clock
1ChCLKSEL_TIMER6_CLKSelects the Mux select line for TIMER6 clock
20hCM_CPTS_RFT_CLKSELSelects the Mux select line for CPTS RFT clock
28hCLKSEL_TIMER1MS_CLKSelects the Mux select line for TIMER1 clock
2ChCLKSEL_GFX_FCLKSelects the divider value for GFX clock
30hCLKSEL_PRU_ICSS_OCP_CLKControls the Mux select line for PRU-ICSS OCP clock
34hCLKSEL_LCDC_PIXEL_CLKControls the Mux select line for LCDC PIXEL clock
38hCLKSEL_WDT1_CLKSelects the Mux select line for Watchdog1 clock
3ChCLKSEL_GPIO0_DBCLKSelects the Mux select line for GPIO0 debounce clock
CLKSEL_TIMERx_CLK Register Fiel Descriptions
BitFieldTypeResetDescription
31-2ReservedR0h
1-0CLKSELR/W1h Selects the Mux select line for TIMER2 clock
0x0 = SEL1 : Select TCLKIN clock
0x1 = SEL2 : Select CLK_M_OSC clock
0x2 = SEL3 : Select CLK_32KHZ clock
0x3 = SEL4 : Reserved
DMTIMER1MSは設定が異なるので注意

Timerの設定

各Timerの設定はDMTIMERx registersで行う。
Offset
DMTIMER0 0x44E05000
DMTIMER1MS 0x44E31000
DMTIMER2 0x48040000
DMTIMER3 0x48042000
DMTIMER4 0x48044000
DMTIMER5 0x48046000
DMTIMER6 0x48048000
DMTIMER7 0x4804A000
AcronymRegister Name
0hTIDRIdentification Register
10hTIOCP_CFGTimer OCP Configuration Register
20hIRQ_EOITimer IRQ End-of-Interrupt Register
24hIRQSTATUS_RAWTimer Status Raw Register
28hIRQSTATUSTimer Status Register
2ChIRQENABLE_SETTimer Interrupt Enable Set Register
30hIRQENABLE_CLRTimer Interrupt Enable Clear Register
34hIRQWAKEENTimer IRQ Wakeup Enable Register
38hTCLRTimer Control Register
3ChTCRRTimer Counter Register
40hTLDRTimer Load Register
44hTTGRTimer Trigger Register
48hTWPSTimer Write Posting Bits Register
4ChTMARTimer Match Register
50hTCAR1Timer Capture Register
54hTSICRTimer Synchronous Interface Control Register
58hTCAR2Timer Capture Register
使用するモードによって使用するレジスタは異なる。どのような挙動を行わせるかはTCLRで行う。
Timer Control Register Field Descriptions
BitFieldTypeResetDescription
31-15ReservedR0h
14GPO_CFGR/W0h General purpose output this register drives directly the PORGPOCFG output pin
0h = PORGPOCFG drives 0 and configures the timer pin as an output.
1h = PORGPOCFG drives 1 and configures the timer pin as an input.
13CAPT_MODER/W0h Capture mode.
0h = Single capture
1h = Capture on second event
12PTR/W0h Pulse or toggle mode on PORTIMERPWM output pin
0h = Pulse
1h = Toggle
11-10TRGR/W0h Trigger output mode on PORTIMERPWM output pin
0h = No trigger
1h = Trigger on overflow
2h = Trigger on overflow and match
3h = Reserved
9-8TCMR/W0h Transition Capture Mode on PIEVENTCAPT input pin
0h = No capture
1h = Capture on low to high transition
2h = Capture on high to low transition
3h = Capture on both edge transition
7SCPWMR/W0h This bit should be set or clear while the timer is stopped or the trigger is off
0h = Clear the PORTIMERPWM output pin and select positive pulse for pulse mode
1h = Set the PORTIMERPWM output pin and select negative pulse for pulse mode
6CER/W0h 0h = Compare mode is disabled
1h = Compare mode is enabled
5PRER/W0h Prescaler enable
0h = The TIMER clock input pin clocks the counter
1h = The divided input pin clocks the counter
4-2PTVR/W0h Pre-scale clock Timer value
1ARR/W0h 0h = One shot timer
1h = Auto-reload timer
0STR/W0h In the case of one-shot mode selected (AR = 0), this bit is automatically reset by internal logic when the counter is overflowed.
0h (R) = Stop timeOnly the counter is frozen
1h = Start timer

TCRRは基準クロックでカウントアップしてく読み書き可能なカウンタとなている。オーバーフローする際にイベントと、必要に応じ割込みを発生させることが出来る。
書き込みはいつでも直接行えるが、TLDRに初期値を設定しておき、オーバーフロー時や、TTGRに書き込みを行う事で初期値をロードさせることも可能となっている。

TMARは32bitのカウンタとなっていて、TCRRがTMARと同一の値となった場合に、イベントを発生させることが出来る。コンペアモードで使用される。

TCAR1, TCAR2はキャプチャモードでTCRRの値を保存するための32bitカウンタとなっている。

続いて代表的な設定例について見ていく。
簡単なライブラリを作ったので、レジスタのmmapとオフセット計算はライブラリに任せることにする。main関数のみ解説するので、詳細はソースを見てほしい。

任意周波数の出力

TCRRがオーバーフローした際に、出力の極性を反転させることが出来るので、この機能を利用する。
パルス幅は1/2周期となるから、 基準クロックをFcとすると、 出力周波数Foは次式で表される。(ただし数値はuint32、Fc>Foとする)
Fo = Fc/2(-TCRR)
TCRR=-Fc/2Fo

TCRRへの初期値のロードはオーバーフローした際にTLDRの値を自動的にロードする機能を使用する。

この場合の設定は次のようになる。
TLDR=前述の式から導かれる値
TCRR=同上

TCLRレジスタは次のような設定となる。

プリスケーラーを使用する必要がある場合は、PRE, PTVも適切に設定します。

#include <stdint.h>
#include <stdio.h>
#include <bbb_dmtimer.h>

int main(void){
    // initialize DMTIMER lib
    if(bbb_dmtimer_init()){
        fprintf(stderr, "Can not initialize bbb_dmtimer lib\n");
        return(1);
    }

    /*
     * DMTIMER6 configuration
     * 1MHz, 50% duty cycle
     */

    // Timer6 enable
    CM_PER[CM_PER_TIMER6_CLKCTRL] = MODULE_ENABLED;         // Set 0x44E000F0 to 0x02
    // Set TCRR clock source
    CM_DPLL[CLKSEL_TIMER6_CLK] = CLK_M_OSC;                 // Set 0x44E0051C to 0x1
    // Set TLDR value(1/2 wave length)
    DMTIMER[6][TLDR] = (uint32_t)(-12);                     // Set 0x48048040 to 0xFFFFFFF4
    // Set initial TCRR value
    DMTIMER[6][TCRR] = DMTIMER[6][TLDR];                    // Set 0x4804803C to 0xFFFFFFF4

    // Set free run cuonter
    // GPO_CFG[14]   = 0(Output)
    // CAPT_MODE[13] = X(Don't care)
    // PT[12]        = 1(Toggle)
    // TRG[11-10]    = 1(Trigger on overflow)
    // TCM[9-8]      = X(Don't care)
    // SCPWM[7]      = X(Don't care)
    // CE[6]         = 0(Compare mode is disabled)
    // PRE[5]        = 0(Prescaler disable)
    // PTV[4-2]      = X(Don't care)
    // AR[1]         = 1(Auto-reload timer)
    // ST[0]         = 1(Start timer)
    DMTIMER[6][TCLR] = 0x1403;                              // Set 0x48048038 to 0x1403

    bbb_dmtimer_free();
    return(0);
}

任意のデューティー比の信号出力(PWM)

前述の任意周波数出力の設定に加えてCompare Modeを利用し、TCRR==TMARとなった際にもイベントを発生させて極性を反転させる。
TCRR~0xffffffffの間の任意の値にTMARを設定する。

TCLRレジスタは次のような設定となる。

ドキュメントのSCPWMの説明がわかりにくいが、1に設定すると極性が反転するので、都合の良い方を選択すればよい。

#include <stdint.h>
#include <stdio.h>
#include <bbb_dmtimer.h>

int main(void){
    // initialize DMTIMER lib
    if(bbb_dmtimer_init()){
        fprintf(stderr, "Can not initialize bbb_dmtimer lib\n");
        return(1);
    }

    /*
     * DMTIMER5 configuration
     * 1KHz, 25% duty cycle
     */

    // Timer5 enable
    CM_PER[CM_PER_TIMER5_CLKCTRL] = MODULE_ENABLED;         // Set 0x44E000EC to 0x02

    // Set TCRR clock source
    CM_DPLL[CLKSEL_TIMER5_CLK] = CLK_M_OSC;                 // Set 0x44E00518 to 0x1

    // Set TLDR value(wave length)
    DMTIMER[5][TLDR] = (uint32_t)(-24000);                  // Set 0x48046040 to 0xFFFFFFA240

    // Set initial TCRR value
    DMTIMER[5][TCRR] = DMTIMER[5][TLDR];                    // Set 0x4804603C to 0xFAAAAAAB

    // Set match value of falling edge
    DMTIMER[5][TMAR] = (uint32_t)(-18000);                  // Set 0x4804604C to 0xFFFFFF6960

    // Set PWM
    // GPO_CFG[14]   = 0(Output)
    // CAPT_MODE[13] = X(Don't care)
    // PT[12]        = 1(Toggle)
    // TRG[11-10]    = 2(Trigger on overflow and match)
    // TCM[9-8]      = X(Don't care)
    // SCPWM[7]      = 0
    // CE[6]         = 1(Compare mode is enableed)
    // PRE[5]        = 0(Prescaler disable)
    // PTV[4-2]      = X(Don't care)
    // AR[1]         = 1(Auto-reload timer)
    // ST[0]         = 1(Start timer)
    DMTIMER[5][TCLR] = 0x1843;                                // Set 0x48046038 to 0x18C3

    bbb_dmtimer_free();
    return(0);
}

時間測定(周波数測定)

タイマーI/Oを入力に設定し、Capture modeを使用して入力信号でTCRRの値をキャプチャさせる。
2回キャプチャして、それぞれの値をTCAR1, TCAR2に保存する機能があるので、これを利用する。
キャプチャが完了すると、割り込みを発生し、キャプチャは停止するので、測定値を読み込んだ後、プログラムで割込みフラグをリセットし次の測定を開始させる。

TCRRがオーバーフローした際は0に戻ってほしいので、TLDR=0, AR=1とする必要がある。TCRRの初期値は何でも構わないので初期化は不要である。

TCLRレジスタは次のような設定となる。

割込みはIRQENABLE_SETレジスタで有効に設定し、発生はIRQSTATUSで読み取れる。クリアはIRQSTATUSの該当フラグに1を書き込むことにより行う。0を書き込むわけではないので注意。

割込みと言っても、OS上のプログラムに割込みがかかるわけではないので、発生はIRQSTATUSレジスタをポーリングする事で確認する。
IRQSTATUSにTCAR_EN_FLAGがセットされていれば、キャプチャが完了しているから、TCAR2-TCAR2 Clockが周期となる。

この方法は比較的測定周期が長い場合に適当である。周波数が高くなると分解能が下がるので、数百khz程度までが実用範囲だろう。より高い周波数を測定したい場合は、tclkinに測定対象の信号を入力してTCRRを回しておき、別のタイマ信号でキャプチャを行わせればよいだろう。

#include <stdint.h>
#include <stdio.h>
#include <bbb_dmtimer.h>

int main(void){
    // initialize DMTIMER lib
    if(bbb_dmtimer_init()){
        fprintf(stderr, "Can not initialize bbb_dmtimer lib\n");
        return(1);
    }

    // Timer6 enable
    CM_PER[CM_PER_TIMER6_CLKCTRL] = MODULE_ENABLED;         // Set 0x44E000F0 to 0x02
    // Set TCRR clock source
    CM_DPLL[CLKSEL_TIMER6_CLK] = CLK_M_OSC;                 // Set 0x44E0051C to 0x1
    // Set TLDR value
    DMTIMER[6][TLDR] = 0;                                   // Set 0x48048040 to 0x0
    // Omit TCRR initialization because any value is OK.
    // Set interrupt
    DMTIMER[6][IRQENABLE_SET] |= TCAR_EN_FLAG;              // Set 0x48048040 to 0x4
    // Clear interrupt flag
    DMTIMER[6][IRQSTATUS] |= TCAR_EN_FLAG;                  // Set 0x48048040 to 0x4

    // Set free run cuonter
    // GPO_CFG[14]   = 1(Input)
    // CAPT_MODE[13] = 1(Capture on second event)
    // PT[12]        = X(Don't care)
    // TRG[11-10]    = 00(No trigger)
    // TCM[9-8]      = 01(Capture on low to high transition)
    // SCPWM[7]      = X(Don't care)
    // CE[6]         = 0(Compare mode is disabled)
    // PRE[5]        = 0(Prescaler disable)
    // PTV[4-2]      = X(Don't care)
    // AR[1]         = 1(Auto-reload timer)
    // ST[0]         = 1(Start timer)
    DMTIMER[6][TCLR] = 0x6103;                              // Set 0x48048038 to 0x6103

    while(1){
        if(DMTIMER[6][IRQSTATUS]){
            uint32_t cycle = DMTIMER[6][TCAR2] - DMTIMER[6][TCAR1];
            printf("%.2f Hz\n", (float)24000000/cycle);
            // Clear interrupt flag
            DMTIMER[6][IRQSTATUS] |= TCAR_EN_FLAG;
        }
    }

    // not reach
    bbb_dmtimer_free();
    return(0);
}

ソースファイル

上記のテストプログラムとライブラリ、dtsファイルが含まれる。
dmtimer.tar.xz
dmtimer_cpp.tar.xz - C++版

おまけ

Debianではdevmem2が無くなっていしまったようだが、busyboxコマンドが同様の機能を持っているので、これを使ってシェルからレジスタの変更や状態の確認が行える。
使用例

#!/bin/sh
#
# shell script example
#

# Mux is set to timer, universal cape draiver is required
config-pin p9.19 timer
config-pin p9.20 timer

# CM_PER_TIMER6_CLKCTRL
busybox devmem 0x44E000F0 32 0x02
# CLKSEL_TIMER6_CLK
busybox devmem 0x44E0051C 32 0x1
# DMTIMER6 TLDR
busybox devmem 0x48048040 32 0xFFFFFFF4
# DMTIMER6 TCRR
busybox devmem 0x4804803C 32 0xFFFFFFF4
# DMTIMER6 TLCR
busybox devmem 0x48048038 32 0x1403

busybox devmem 0x44E000F0 32
busybox devmem 0x44E0051C 32
busybox devmem 0x48048040 32
busybox devmem 0x4804803C 32
busybox devmem 0x48048038 32