PICのタイマー処理をデータシートを読みながら理解する

# はじめに
いつもはMPLAB X IDEのMCCを使ってなんとなくタイマーを構築し使っていたため、ちゃんと理解しようと、MCCの手を借りずにデータシートを読み解きながら理解したことをアウトプットしてみる。そんな投稿。

# 概要

今回利用するPICは`PIC12F1840`とする。

秋月へのリンクはこちら。

https://akizukidenshi.com/catalog/g/g110646/

データシートはこちら。

https://ww1.microchip.com/downloads/aemDocuments/documents/OTH/ProductDocuments/DataSheets/40001441F.pdf

種類にもよるが、１つのPIC内にいくつかタイマーが用意されている。
今回利用するPIC12F1840については３つ用意されている。
- Timer0
- Timer1
- Timer2

タイマーがいくつあるか、どんなタイマーがあるかはデータシートのここをチェックする。

（データシート P2）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38724/ec59ad8b-bb83-4d16-9fb5-5bfad6235815.png)
該当する行に`2/1`とあるが、表のタイトルにもある通りそれぞれ8bitタイマー、16bitタイマーの数となる。
8bitは256カウント、16bitは65,536カウントできる。
1カウントでどれぐらいの時間を持たせるかは設定次第。

用途によって選択するタイマーが異なってくるが、今回は一番シンプルなTimer0（8bitタイマー）について理解する。

# Timer0の仕組み

データシートを確認しながら進めていく。
説明箇所は重要な記載に絞る。

### Timer0概要
Timer0の説明はデータシートP143から始まる。

>20.0 TIMER0 MODULE
>The Timer0 module is an 8-bit timer/counter with the following features:
>・8-bit timer/counter register (TMR0)
>・8-bit prescaler (independent of Watchdog Timer)
>・Programmable internal or external clock source
>・Programmable external clock edge selection
>・Interrupt on overflow
>・TMR0 can be used to gate Timer1

>20.1 Timer0 Operation
>The Timer0 module can be used as either an 8-bit timer or an 8-bit counter.


ここではTimer0に関する主な機能が羅列されている。

- Timer0は8bitタイマーで、8bitのプリスケーラ（後述）を持っている
- Timer0をカウントするタイミングを決めるクロックソースは内部、または、外部を設定できる
- Timer0がオーバーフローした際に割り込みが発生する
- Timer0のオーバーフローをTimer1のゲート機能に入力できる

:::note
Timer1にはゲート機能と呼ばれる特定の入力と同時にカウントを始めることが可能な機能を持っている。例えばセンサーの入力がONからOFFまでの時間を計測したいときに利用する。
超音波距離計測センサー等。
:::

### タイマーモード

>20.1.1 8-BIT TIMER MODE
>The Timer0 module will increment every instruction cycle, if used without a prescaler. 8-bit Timer mode is selected by clearing the TMR0CS bit of the OPTION_REG register.

プリスケーラを利用しない場合、Timer0はPICの命令サイクルごとでカウンタがインクリメントされる。
`OPTION_REG`レジスタの`TMR0CS`ビットを0にすることでこのような動きとなる。
1にすると外部クロックによってカウンタがインクリメントする動きとなる。

ダイアグラム上では下記に該当する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38724/5780bc8d-bae4-4944-a8b5-563e3623b35b.png)

<!--
まずは理解することを優先するため小難しいことは除外
>When TMR0 is written, the increment is inhibited for two instruction cycles immediately following the write.
>
>Note:
>The value written to the TMR0 register can be adjusted, in order to account for the two instruction cycle delay when TMR0 is written.

`TMR0`レジスタへのカウント値書き込みを行った場合、2命令サイクルの間はカウントが止まる（知らなかった）。
このカウントされない分を考慮する必要があるらしい。

MCCの自動生成コードでは考慮しているようには見えないが、今度検証してみよう。
-->
<!--
### カウンターモード
>20.1.2 8-BIT COUNTER MODE
>In 8-Bit Counter mode, the Timer0 module will increment on every rising or falling edge of the T0CKI pin or the Capacitive Sensing Oscillator (CPSCLK) signal.
>
>8-Bit Counter mode using the T0CKI pin is selected by setting the TMR0CS bit in the OPTION_REG register to ‘1’ and resetting the T0XCS bit in the CPSCON0 register to ‘0’.
>
>8-Bit Counter mode using the Capacitive Sensing Oscillator (CPSCLK) signal is selected by setting the TMR0CS bit in the OPTION_REG register to ‘1’ and setting the T0XCS bit in the CPSCON0 register to ‘1’.
>
>The rising or falling transition of the incrementing edge for either input source is determined by the TMR0SE bit in the OPTION_REG register.
-->

### プリスケーラ

>20.1.3 SOFTWARE PROGRAMMABLE PRESCALER
>
>A software programmable prescaler is available for exclusive use with Timer0. The prescaler is enabled by clearing the PSA bit of the OPTION_REG register.

Timer0用に独立したプリスケーラが用意されている。
`OPTION_REG`レジスタの`PSA`ビットを0にすることでプリスケーラが有効となる。

ダイアグラム上では下記に該当する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38724/fd74982d-9022-4ba0-93b2-4be671c0bc03.png)

>There are eight prescaler options for the Timer0 module ranging from 1:2 to 1:256. The prescale values are selectable via the PS<2:0> bits of the OPTION_REG register. In order to have a 1:1 prescaler value for the Timer0 module, the prescaler must be disabled by setting the PSA bit of the OPTION_REG register.

プリスケーラには1:2から1:256までの8つの分周設定がある。
`OPTION_REG`レジスタの`PS`ビットに値を設定することで選択できる。

ダイアグラム上では下記に該当する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38724/5c6ca56c-cb65-481e-9937-0d3b7397bc79.png)

| `PS`ビット値 | プリスケーラ設定 |
|-----------|-------------|
| 000       | 1 : 2       |
| 001       | 1 : 4       |
| 010       | 1 : 8       |
| 011       | 1 : 16      |
| 100       | 1 : 32      |
| 101       | 1 : 64      |
| 110       | 1 : 128     |
| 111       | 1 : 256     |

設定値はデータシートP145に掲載されている。

>The prescaler is not readable or writable. All instructions writing to the TMR0 register will clear the prescaler.

プリスケーラの値（カウント値）は読み書きができない。
`TMR0`レジスタの設定時にクリアされる。

### 割り込み

>20.1.4 TIMER0 INTERRUPT
>
>Timer0 will generate an interrupt when the TMR0 register overflows from FFh to 00h. The TMR0IF interrupt flag bit of the INTCON register is set every time the TMR0 register overflows, regardless of whether or not the Timer0 interrupt is enabled. The TMR0IF bit can only be cleared in software. The Timer0 interrupt enable is the TMR0IE bit of the INTCON register.

Timer0は`TMR0`レジスタオーバーフロー時に割り込みを発生させ、`INTCON`レジスタの`TMR0IF`ビットに`1`をセットする。
割り込みを有効にするには`INTCON`レジスタの`TMR0IE`ビットに`1`をセットする必要がある。
尚、`TMR0IF`ビットのクリアは利用側のソフトウェア側で行う必要がある。

# Timer0のタイマー値設計

今回は内部クロックとプリスケーラを利用し、大体50ms間隔でLEDの点灯を切り替えるようなタイマー設計を行う。
PICの周波数は4MHzとする。

今回想定する入力ルートがわかりやすいようにダイアグラム上で薄く赤線を引いておいた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38724/a9a90c03-7048-4fe2-9489-4ddfa20343af.png)

尚、タイマー値を設計する上で必要な箇所は①と④となる。
図で◯がついた番号は後述に掲載するソース内の設定箇所とリンクするようにしてある。

まずは①の`FOSC/4`について、今回はPICの周波数を4MHzとしたためFOSCは4MHzとなる。
```math
1MHz=4MHz/4
```
---
次に④のプリスケーラについて、一旦`1:128`ぐらいで試算してみる。
```math
7,812.5Hz=1,000,000Hz/128
```
---
上記`7,812.5Hz`は1秒間の周波数なので、1周波がどれぐらいの時間なのかを計算し、Timer0のカウント値の最大で掛ける。
```math
0.000128秒/周波=1秒/7,812.5周波数
```
```math
0.032768秒=0.000128秒*256カウント
```
32msとなった。
今回50ms欲しいので、もう少し多めに分周してみる。

---
プリスケーラを`1:256`で再計算する。
```math
3,906.25Hz=1,000,000Hz/256
```
```math
0.000256秒/周波=1秒/3,906.25周波数
```
タイマーオーバーフローまでの時間
```math
0.065536秒=0.000256秒*256カウント
```
65msとなった。いい感じ。
`TMR0`レジスタの初期値に多少ゲタを履かせれば大体50msになる。

```math
0.050176秒=0.000256秒*(256-60)カウント
```
60カウントを`TMR0`レジスタに初期設定してあげればよさそう。

# ソースコード

<!--
`TMR0Setup`関数がTimer0関連のレジスタ設定箇所。
尚、割り込み関数にて`TMR0 = 60;`と記述し、設定直後は2カウントされない仕様を無視しているが、大体の時間をカウントできればいいため多めに見てほしい。
-->

```c
#include <xc.h>

#pragma config FCMEN = OFF
#pragma config IESO = OFF
#pragma config CLKOUTEN = OFF
#pragma config BOREN = ON
#pragma config CPD = OFF
#pragma config CP = OFF
#pragma config MCLRE = ON
#pragma config PWRTE = OFF
#pragma config WDTE = OFF
#pragma config FOSC = INTOSC
#pragma config LVP = OFF
#pragma config DEBUG = OFF
#pragma config BORV = LO
#pragma config STVREN = ON
#pragma config PLLEN = OFF
#pragma config WRT = OFF

#define _XTAL_FREQ 4000000

#define LED LATA2

void TMR0Setup() {
    TMR0IE = 1;
    
    OPTION_REG = 0b00000111; // ③1:256で分周
    TMR0CS = 0; // ②内部クロック利用
    PSA = 0;    // ④プリスケーラ利用
}

void __interrupt() isr (void) {
    if(TMR0IF) {
        // 大体50ms間隔で点滅
        TMR0IF = 0;
        TMR0 = 60;
        LED = ~LED;
    }
}

void Setup() {
    OSCCON = 0b01101010; // ①4MHz
    PORTA = 0x00; // 0で初期化
    TRISA = 0b11111011; // ra2を出力ピンに設定
    ANSELA = 0x00; // 全てデジタルIO
    nWPUEN = 0;
    
    INTCONbits.GIE = 1; // グローバル割り込み許可
    TMR0Setup();
}

void main() {
    Setup();
    
    while(1) {
        __delay_ms(100);
    }
}
```

# おわりに

今までMCCでいい感じに設定してきたが、データシートをしっかり読み解き仕組みを理解することができたのは大きな収穫。
ちなみに生成AIも活用したので理解が早かった。