HALの内部構造~TIM3を追いかける~

STM32F303K8をターゲットとしてCubeMXを使って自動生成したソースコードを追いかけることで、HALライブラリの中身を理解する。この記事では、TIM3に関係する部分を見て行く。

TIM3の設定

まず、CubeMXでTIM3をInternal Clockを使うように設定する。そして、NVIC SettingsからTIM3 global interruptを有効にする。この状態でコードを生成し、main.cにタイマーを使うための設定を書いて行く。まず、TIM3による割り込みハンドラをmain.cに書く。

int main(void) 
{
  ...
  /* USER CODE BEGIN 2 */

  HAL_TIM_Base_Start_IT(&htim3);

  /* USER CODE END 2 */
  ...
}
...
/* USER CODE BEGIN 4 */

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if ( htim->Instance == htim3.Instance )
    {
        HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
    }
}

/* USER CODE END 4 */

以上のように、HAL_TIM_Base_Start_ITの呼び出しとHAL_TIM_PeriodElapsedCallbackの定義を追加する。HAL_TIM_PeriodElapsedCallbackはTIM3がover flowした時に呼ばれる。たとえば、このコードではLED3を割り込みごとにトグルして、Lチカを行なっている。ただし、Lチカをするためには、CubeMXで内部LEDが接続されているピンの設定を追加する必要がある。

htim3を追う

はじめに、TIM3のインスタンス?と思しきhtim3の構造を調べる。

// @main.c
TIM_HandleTypeDef htim3;

htim3はmain.cでこのように宣言されているので、とりあえずTIM_HandleTypeDefの定義を確認する。

// @stm32f3xx_hal_tim.h

typedef struct
{
    TIM_TypeDef              *Instance;
    TIM_Base_InitTypeDef     Init;
    HAL_TIM_ActiveChannel    Channel;
    DMA_HandleTypeDef        *hdma[7];
    HAL_LockTypeDef          Lock;
    HAL_TIM_StateTypeDef   State;
} TIM_HandleTypeDef;

// @stm32f303x8.h

typedef struct
{
    uint32_t CR1;
    uint32_t CR2;
    uint32_t SMCR;
    uint32_t DIER;
    uint32_t SR;
    uint32_t EGR;
    uint32_t CCMR1;
    uint32_t CCMR2;
    uint32_t CCER;
    uint32_t CNT;
    uint32_t PSC;
    uint32_t ARR;
    uint32_t RCR;
    uint32_t CCR1;
    uint32_t CCR2;
    uint32_t CCR3;
    uint32_t CCR4;
    uint32_t BDTR;
    uint32_t DCR;
    uint32_t DMAR;
    uint32_t OR;
    uint32_t CCMR3;
    uint32_t CCR5;
    uint32_t CCR6;
} TIM_TypeDef;

STM社のRM0316 Reference manualのTable127を参照すると、TIM_TypeDef構造体はTIM3の設定レジスタのアドレス割当に対応していることがわかる。するとこれより、TIM_HandleTypeDef型であるhtim3のInstanceフィールドはTIM3のregisterが割り当てられている領域の先頭番地を指していると予想できる。次にそれを確かめてみよう。

MX_TIM3_Initを追う

main.cのmain関数内を見て行くと、MX_TIM3_Init関数が呼ばれている。これは、いかにもTIM3の初期化ルーチンだろうから、中身を調べて行く。

static void MX_TIM3_Init(void)
{

    TIM_ClockConfigTypeDef sClockSourceConfig;
    ...

    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 999;
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 8000;
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

    HAL_TIM_Base_Init(&htim3);

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;

    HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig);
    ...
}

MX_TIM3_Init内では、どうやらTIM_ClockConfigTypeDef構造体のインスタンスにTIM3の設定を書き込んでいるようだ。ここで、htim3.Instance = TIM3となっているので、先ほどの予想が正しいなら、TIM3はTIM3のregister割当領域の先頭アドレスのはずだ。

// @stm32f303x8.h

#define TIM3                ((TIM_TypeDef *) TIM3_BASE)

#define PERIPH_BASE           ((uint32_t)0x40000000U)
#define APB1PERIPH_BASE       PERIPH_BASE
#define TIM3_BASE             (APB1PERIPH_BASE + 0x00000400U)

つまり、TIM3 = 0x40000400Uということだ。

f:id:babyron64:20180730235852p:plain
Table4

RM0316 Reference manualのTable4を見ると、確かに0x40000400UはTIM3設定レジスタの先頭アドレスとなっている。

話をMX_TIM3_Initに戻そう。CubeMXで設定した値がhtim3.Initに設定されている。そして、どうやらHAL_TIM_Base_Initでhtim3.Instanceに設定した値を反映させているようだ。ならば、次はHAL_TIM_Base_Initの中身を見てみよう。

// @stm32f3xx_hal_tim.c

HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim)
{ 
    if(htim->State == HAL_TIM_STATE_RESET)
    {  
        htim->Lock = HAL_UNLOCKED;
        HAL_TIM_Base_MspInit(htim);
    }

    htim->State = HAL_TIM_STATE_BUSY;

    TIM_Base_SetConfig(htim->Instance, &htim->Init); 

    htim->State = HAL_TIM_STATE_READY;

    return HAL_OK;
}

どうやら、HAL_TIM_Base_MspInitで何かしらの初期化をしてから、先ほど設定していたInitを使ってhtim3の初期化を行うようだ。まずは、HAL_TIM_Base_MspInitを見ていこう。

// @stm32f3xx_hal_msp.c

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{
    if(htim_base->Instance==TIM3)
    {
        __HAL_RCC_TIM3_CLK_ENABLE();
        HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0);
        HAL_NVIC_EnableIRQ(TIM3_IRQn);
    }
}

NVICの設定については今回はスルーするので、この関数については__HAL_RCC_TIM3_CLK_ENABLEだけを見る。

// @stm32f3xx_hal_rcc_ex.h

#define __HAL_RCC_TIM3_CLK_ENABLE() \
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM3EN);

// @stm32f3xx.h

#define SET_BIT(REG, BIT)     ((REG) |= (BIT))

// @stm32f303x8.h

#define RCC_APB1ENR_TIM3EN_Pos  (1U)
#define RCC_APB1ENR_TIM3EN_Msk  (0x1U << RCC_APB1ENR_TIM3EN_Pos)
#define RCC_APB1ENR_TIM3EN    RCC_APB1ENR_TIM3EN_Msk

先ほどと同じようにして、RCCRCC設定レジスタがマップされているアドレスの先頭番地であると予想されるが、今回は確認はしない。そのように仮定すると、ここではRCCのAPB1ENRレジスタの第二ビットを立てている。これは、RM0316 Reference manualの9.4.8 APB1 peripheral clock enable register (RCC_APB1ENR)によると、

Bit 1 TIM3EN: TIM3 timer clock enable
Set and cleared by software.
0: TIM3 clock disabled
1: TIM3 clock enabled

だそうで、ビットを立てていることより、TIM3 clockを有効化しているとわかる。

さて、HAL_TIM_Base_Initに話を戻して、次にTIM_Base_SetConfigを見ていこう。これは、TIM_Base_SetConfig(htim->Instance, &htim->Init)と呼ばれていた。

void TIM_Base_SetConfig(TIM_TypeDef *TIMx, TIM_Base_InitTypeDef *Structure)
{
    uint32_t tmpcr1 = 0U;
    tmpcr1 = TIMx->CR1;

    tmpcr1 &= ~(TIM_CR1_DIR | TIM_CR1_CMS);
    tmpcr1 |= Structure->CounterMode;

    tmpcr1 &= ~TIM_CR1_CKD;
    tmpcr1 |= (uint32_t)Structure->ClockDivision;

    MODIFY_REG(tmpcr1, TIM_CR1_ARPE, Structure->AutoReloadPreload);

    TIMx->CR1 = tmpcr1;

    TIMx->ARR = (uint32_t)Structure->Period ;

    TIMx->PSC = (uint32_t)Structure->Prescaler;

    TIMx->EGR = TIM_EGR_UG;
}

TIMxには、先ほども説明したように、TIM3の設定レジスタがマップされているアドレスの先頭番地が格納されているので、TIMx->hoge = fugaという式で、hogeというレジスタにfugaという値を入れることができる!!各レジスタの詳細はRM0316 Reference manualの21.4.1 TIMx control register 1 (TIMx_CR1)を参照したもらうとして、最後のTIMx->EGR = TIM_EGR_UGという式で、TIM3の更新を行なっている。

HAL_TIM_Base_Start_ITを追う

ふぅ、とりあえずタイマーの初期化処理は一通り追い終わった。つぎは、初期化したタイマーを起動させる処理だ。

// @stm32f3xx_hal_tim.c

HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)
{
    __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);
    __HAL_TIM_ENABLE(htim);
      
    return HAL_OK;
}

まずは、__HAL_TIM_ENABLE_ITを見る。これは、名前からしてタイマー割り込みを有効化しているのだろう。

// @stm32f3xx_hal_tim.h

#define __HAL_TIM_ENABLE_IT(__HANDLE__, __INTERRUPT__)  ((__HANDLE__)->Instance->DIER |= (__INTERRUPT__))

#define TIM_IT_UPDATE           (TIM_DIER_UIE)

// @stm32f303x8.h

#define TIM_DIER_UIE_Pos          (0U) 
#define TIM_DIER_UIE_Msk          (0x1U << TIM_DIER_UIE_Pos)
#define TIM_DIER_UIE              TIM_DIER_UIE_Msk

つまり、TIM3のDIERレジスタの第一ビットを立てている。RM0316 Reference manualの21.4.4 TIMx DMA/Interrupt enable register (TIMx_DIER)によると、

Bit 0 UIE: Update interrupt enable
0: Update interrupt disabled.
1: Update interrupt enabled.

らしい。よって、第一ビットを立てることで、interruptを有効化している。

次に、__HAL_TIM_ENABLEをみる。

// @stm32f3xx_hal_tim.h

#define __HAL_TIM_ENABLE(__HANDLE__)                 ((__HANDLE__)->Instance->CR1|=(TIM_CR1_CEN))

// @stm32f303x8.h

#define TIM_CR1_CEN_Pos           (0U)
#define TIM_CR1_CEN_Msk           (0x1U << TIM_CR1_CEN_Pos)
#define TIM_CR1_CEN               TIM_CR1_CEN_Msk

よって、__HAL_TIM_ENABLEではCR1レジスタの第一ビットを立てている。RM0316 Reference manualの21.4.1 TIMx control register 1 (TIMx_CR1)によると、

Bit 0 CEN: Counter enable
0: Counter disabled
1: Counter enabled

だそうで、CR1の第一ビットを立てることでカウンタを有効化し、タイマーを起動させている。

TIM3の割り込み処理

HALとは直接関係はないが、ついでにTIM3の割り込み処理の流れも追っておこう。割り込み処理の始まりは、プロセッサが割り込みを検知し、対応する割り込みベクタテーブルエントリに入っているアドレスに制御を移すことから始まる。ただし、割り込みベクタの各エントリが割り込み処理の開始アドレスであるのは、基本的にNVICが使われているARMアーキテクチャのみ(eg. ARMv7-M)であり、それ以外のアーキテクチャ(eg. ARMv7)では各エントリは割り込み処理の命令(ジャンプや分岐が普通)であった。そのため、ネット上の情報ではこの二つの方式が混在しているので注意。Crtex-Mではアドレスを格納する前者の方式が使われている。話を戻そう。TIM3に対応する割り込みベクタエントリは46番目のTIM3_IRQHandlerである。

; @startup_stm32f303x8.s

    .section  .isr_vector,"a",%progbits
    .type g_pfnVectors, %object
    .size g_pfnVectors, .-g_pfnVectors

g_pfnVectors:    
    ...
    .word TIM3_IRQHandler
    ...

    .weak TIM3_IRQHandler
    .thumb_set TIM3_IRQHandler,Default_Handler

TIM3_IRQHandlerはweakであるので、TIM3_IRQHandlerの定義がない場合には、Default_Handlerが使われる。しかし、CubeMXでTIM3 global interruptを有効にしたため、TIM3_IRQHandlerの定義は自動的に生成されている。

// @stm32f3xx_it.c

void TIM3_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim3);
}

その中では、HAL_TIM_IRQHandlerが呼ばれている。

void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim)
{
    ...
    /* TIM Update event */
    if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE) != RESET)
    {
        if(__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_UPDATE) !=RESET)
        { 
            __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE);
            HAL_TIM_PeriodElapsedCallback(htim);
        }
    }
    ...
}

さらに、HAL_TIM_IRQHandlerからmain.cで定義したHAL_TIM_PeriodElapsedCallbackが呼ばれる。このようにして、割り込みが処理されて行く。さあ、もう少しで終わりだ。

外側のifでは、割り込みがupdateによるもの、すなわち、タイマーのカウントが終わったことによるものであることを確認していると思われる。実際に中を覗いてみると、

// @stm32f3xx_hal_tim.h

#define __HAL_TIM_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR &(__FLAG__)) == (__FLAG__))

#define TIM_FLAG_UPDATE                    (TIM_SR_UIF)

// @stm32f303x8.h

#define TIM_SR_UIF_Pos            (0U)
#define TIM_SR_UIF_Msk            (0x1U << TIM_SR_UIF_Pos)
#define TIM_SR_UIF                TIM_SR_UIF_Msk

SRレジスタの第一ビットが立っているかを確認している。RM0316 Reference manualの21.4.5 TIMx status register (TIMx_SR)には、

Bit 0 UIF: Update interrupt flag
This bit is set by hardware on an update event. It is cleared by software.
0: No update occurred
1: Update interrupt pending. This bit is set by hardware when the registers are updated.

と書いてあるので、SRレジスタの第一ビットが立っていればupdateが起こったとわかる。

内側のifでは、__HAL_TIM_GET_IT_SOURCEが使われている。

// @stm32f3xx_hal_tim.h

#define __HAL_TIM_GET_IT_SOURCE(__HANDLE__, __INTERRUPT__) ((((__HANDLE__)->Instance->DIER & (__INTERRUPT__)) == (__INTERRUPT__)) ? SET : RESET)

#define TIM_IT_UPDATE           (TIM_DIER_UIE)

// @stm32f303x8.h

#define TIM_DIER_UIE_Pos          (0U)
#define TIM_DIER_UIE_Msk          (0x1U << TIM_DIER_UIE_Pos)
#define TIM_DIER_UIE              TIM_DIER_UIE_Msk

ここでは、DIERレジスタの第一ビットが立っているかを確認している。RM0316 Reference manualの21.4.4 TIMx DMA/Interrupt enable register (TIMx_DIER)によると、

Bit 0 UIE: Update interrupt enable
0: Update interrupt disabled.
1: Update interrupt enabled.

なので、こちらも割り込みがupdateによるものであることの確認だ。

割り込みの原因の確認が終わると、__HAL_TIM_CLEAR_FLAGでflagを戻している。これを行わないと、updateフラグが立ったままになる。そののち、HAL_TIM_PeriodElapsedCallbackを呼んでいる。

さあ、最後だ。HAL_TIM_PeriodElapsedCallbackをみよう。

// @main.c
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if ( htim->Instance == htim3.Instance )
    {
        HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
    }
}

まずは、割り込みの原因がTIM3であるかを確認している。htim3.InstanceはTIM3の設定レジスタの先頭アドレスであるから、他のタイマーと値がかぶることがないので、タイマーの種類の確認に使っているのだと思う。

確認が終わると、LED3ピンをトグルしている。ここでは、GPIOに関するHALライブラリ関数を使っており、これも当然追いかけていける。楽しそうだけど、今日はここまでにしよう。

まとめ

f:id:babyron64:20180731010133p:plain

今日見てきたHALライブラリ関数の概要を図にして見た。こうして見ると、HALがユーザーコードとハードウェアを繋いでいることがよくわかる。(まあ、そうなるように図を描いたのだけど。。。)

おわり。